mirror of
https://github.com/4ian/GDevelop.git
synced 2025-10-15 10:19:04 +00:00
Compare commits
47 Commits
v5.3.196
...
refactor/p
Author | SHA1 | Date | |
---|---|---|---|
![]() |
ae0f2687a5 | ||
![]() |
56bcb79dcd | ||
![]() |
4ca859c8b9 | ||
![]() |
701b5a3250 | ||
![]() |
1e87b74d48 | ||
![]() |
b7a4bab53c | ||
![]() |
2223b6516b | ||
![]() |
7d89208e7b | ||
![]() |
70508292e4 | ||
![]() |
c899db1dba | ||
![]() |
2fb27f430e | ||
![]() |
6cd2400848 | ||
![]() |
fadccba757 | ||
![]() |
29a43514ce | ||
![]() |
e64ab94f76 | ||
![]() |
ed969d2909 | ||
![]() |
77bf67fdf0 | ||
![]() |
580c7a370c | ||
![]() |
266f0f2b6e | ||
![]() |
58af6aacb4 | ||
![]() |
1c4ee1c928 | ||
![]() |
0275476bec | ||
![]() |
3f0194acf0 | ||
![]() |
017d8b28c2 | ||
![]() |
4b518ba0fc | ||
![]() |
e4938b25b5 | ||
![]() |
c56ad2d277 | ||
![]() |
f069f9d942 | ||
![]() |
9f7c60a69e | ||
![]() |
774bf0db61 | ||
![]() |
ccd9c5c0ff | ||
![]() |
5b130a34e3 | ||
![]() |
3e6d147660 | ||
![]() |
433d85db94 | ||
![]() |
b75f707b30 | ||
![]() |
67799eb3eb | ||
![]() |
1173210386 | ||
![]() |
007cc48291 | ||
![]() |
12813c9ad9 | ||
![]() |
e503e34059 | ||
![]() |
4bf576a889 | ||
![]() |
eff1c1bcb6 | ||
![]() |
abce34f2b1 | ||
![]() |
07276d5e16 | ||
![]() |
1fdd8cc792 | ||
![]() |
79e40605d5 | ||
![]() |
fcc91e3fea |
@@ -15,7 +15,7 @@
|
||||
|
||||
namespace gd {
|
||||
|
||||
gd::String* InitialInstance::badStringProperyValue = NULL;
|
||||
gd::String* InitialInstance::badStringPropertyValue = NULL;
|
||||
|
||||
InitialInstance::InitialInstance()
|
||||
: objectName(""),
|
||||
@@ -34,6 +34,7 @@ InitialInstance::InitialInstance()
|
||||
depth(0),
|
||||
locked(false),
|
||||
sealed(false),
|
||||
keepRatio(true),
|
||||
persistentUuid(UUID::MakeUuid4()) {}
|
||||
|
||||
void InitialInstance::UnserializeFrom(const SerializerElement& element) {
|
||||
@@ -58,6 +59,7 @@ void InitialInstance::UnserializeFrom(const SerializerElement& element) {
|
||||
SetLayer(element.GetStringAttribute("layer"));
|
||||
SetLocked(element.GetBoolAttribute("locked", false));
|
||||
SetSealed(element.GetBoolAttribute("sealed", false));
|
||||
SetShouldKeepRatio(element.GetBoolAttribute("keepRatio", false));
|
||||
|
||||
persistentUuid = element.GetStringAttribute("persistentUuid");
|
||||
if (persistentUuid.empty()) ResetPersistentUuid();
|
||||
@@ -120,6 +122,7 @@ void InitialInstance::SerializeTo(SerializerElement& element) const {
|
||||
if (HasCustomDepth()) element.SetAttribute("depth", GetCustomDepth());
|
||||
if (IsLocked()) element.SetAttribute("locked", IsLocked());
|
||||
if (IsSealed()) element.SetAttribute("sealed", IsSealed());
|
||||
if (ShouldKeepRatio()) element.SetAttribute("keepRatio", ShouldKeepRatio());
|
||||
|
||||
if (persistentUuid.empty()) persistentUuid = UUID::MakeUuid4();
|
||||
element.SetStringAttribute("persistentUuid", persistentUuid);
|
||||
@@ -188,10 +191,10 @@ double InitialInstance::GetRawDoubleProperty(const gd::String& name) const {
|
||||
|
||||
const gd::String& InitialInstance::GetRawStringProperty(
|
||||
const gd::String& name) const {
|
||||
if (!badStringProperyValue) badStringProperyValue = new gd::String("");
|
||||
if (!badStringPropertyValue) badStringPropertyValue = new gd::String("");
|
||||
|
||||
const auto& it = stringProperties.find(name);
|
||||
return it != stringProperties.end() ? it->second : *badStringProperyValue;
|
||||
return it != stringProperties.end() ? it->second : *badStringPropertyValue;
|
||||
}
|
||||
|
||||
void InitialInstance::SetRawDoubleProperty(const gd::String& name,
|
||||
|
@@ -206,6 +206,17 @@ class GD_CORE_API InitialInstance {
|
||||
*/
|
||||
void SetSealed(bool enable = true) { sealed = enable; }
|
||||
|
||||
/**
|
||||
* \brief Return true if the dimensions (width, height and depth) should keep
|
||||
* the same ratio.
|
||||
*/
|
||||
bool ShouldKeepRatio() const { return keepRatio; };
|
||||
|
||||
/**
|
||||
* \brief Define if instance's dimensions should keep the same ratio.
|
||||
*/
|
||||
void SetShouldKeepRatio(bool enable = true) { keepRatio = enable; }
|
||||
|
||||
///@}
|
||||
|
||||
/** \name Variable management
|
||||
@@ -340,11 +351,13 @@ class GD_CORE_API InitialInstance {
|
||||
gd::VariablesContainer initialVariables; ///< Instance specific variables
|
||||
bool locked; ///< True if the instance is locked
|
||||
bool sealed; ///< True if the instance is sealed
|
||||
bool keepRatio; ///< True if the instance's dimensions
|
||||
/// should keep the same ratio.
|
||||
mutable gd::String persistentUuid; ///< A persistent random version 4 UUID,
|
||||
///< useful for hot reloading.
|
||||
/// useful for hot reloading.
|
||||
|
||||
static gd::String*
|
||||
badStringProperyValue; ///< Empty string returned by GetRawStringProperty
|
||||
badStringPropertyValue; ///< Empty string returned by GetRawStringProperty
|
||||
};
|
||||
|
||||
} // namespace gd
|
||||
|
@@ -3134,14 +3134,51 @@ module.exports = {
|
||||
modelDepth < epsilon
|
||||
? Number.POSITIVE_INFINITY
|
||||
: originalDepth / modelDepth;
|
||||
let scaleRatio = Math.min(widthRatio, heightRatio, depthRatio);
|
||||
if (!Number.isFinite(scaleRatio)) {
|
||||
scaleRatio = 1;
|
||||
}
|
||||
const minScaleRatio = Math.min(widthRatio, heightRatio, depthRatio);
|
||||
if (!Number.isFinite(minScaleRatio)) {
|
||||
this._defaultWidth = modelWidth;
|
||||
this._defaultHeight = modelHeight;
|
||||
this._defaultDepth = modelDepth;
|
||||
} else {
|
||||
if (widthRatio === minScaleRatio) {
|
||||
this._defaultWidth = originalWidth;
|
||||
this._defaultHeight = Rendered3DInstance.applyRatio({
|
||||
oldReferenceValue: modelWidth,
|
||||
newReferenceValue: originalWidth,
|
||||
valueToApplyTo: modelHeight,
|
||||
});
|
||||
this._defaultDepth = Rendered3DInstance.applyRatio({
|
||||
oldReferenceValue: modelWidth,
|
||||
newReferenceValue: originalWidth,
|
||||
valueToApplyTo: modelDepth,
|
||||
});
|
||||
} else if (heightRatio === minScaleRatio) {
|
||||
this._defaultWidth = Rendered3DInstance.applyRatio({
|
||||
oldReferenceValue: modelHeight,
|
||||
newReferenceValue: originalHeight,
|
||||
valueToApplyTo: modelWidth,
|
||||
});
|
||||
|
||||
this._defaultWidth = scaleRatio * modelWidth;
|
||||
this._defaultHeight = scaleRatio * modelHeight;
|
||||
this._defaultDepth = scaleRatio * modelDepth;
|
||||
this._defaultHeight = originalHeight;
|
||||
this._defaultDepth = Rendered3DInstance.applyRatio({
|
||||
oldReferenceValue: modelHeight,
|
||||
newReferenceValue: originalHeight,
|
||||
valueToApplyTo: modelDepth,
|
||||
});
|
||||
} else {
|
||||
this._defaultWidth = Rendered3DInstance.applyRatio({
|
||||
oldReferenceValue: modelDepth,
|
||||
newReferenceValue: originalDepth,
|
||||
valueToApplyTo: modelWidth,
|
||||
});
|
||||
this._defaultHeight = Rendered3DInstance.applyRatio({
|
||||
oldReferenceValue: modelDepth,
|
||||
newReferenceValue: originalDepth,
|
||||
valueToApplyTo: modelHeight,
|
||||
});
|
||||
this._defaultDepth = originalDepth;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
146
Extensions/JsExtensionTypes.d.ts
vendored
146
Extensions/JsExtensionTypes.d.ts
vendored
@@ -4,6 +4,107 @@ type GDNamespace = typeof import('../GDevelop.js/types');
|
||||
// in this file and merge it with the other namespace declarations.
|
||||
declare namespace PIXI {}
|
||||
|
||||
/**
|
||||
* Expose functions to load PIXI textures or fonts, given the names of
|
||||
* resources and a gd.Project.
|
||||
*/
|
||||
declare class PixiResourcesLoader {
|
||||
burstCache();
|
||||
|
||||
async reloadResource(project: gd.Project, resourceName: string);
|
||||
|
||||
/**
|
||||
* Reload the given resources.
|
||||
*/
|
||||
async reloadResources(
|
||||
project: gd.Project,
|
||||
resourceNames: Array<string>
|
||||
): Promise<void>;
|
||||
|
||||
/**
|
||||
* Return the PIXI texture represented by the given resource.
|
||||
* If not loaded, it will load it.
|
||||
*/
|
||||
getPIXITexture(project: gd.Project, resourceName: string): PIXI.Texture;
|
||||
|
||||
/**
|
||||
* Return the three.js texture associated to the specified resource name.
|
||||
* Returns a placeholder texture if not found.
|
||||
* @param project The project
|
||||
* @param resourceName The name of the resource
|
||||
* @returns The requested texture, or a placeholder if not found.
|
||||
*/
|
||||
getThreeTexture(project: gd.Project, resourceName: string): THREE.Texture;
|
||||
|
||||
/**
|
||||
* Return the three.js material associated to the specified resource name.
|
||||
* @param project The project
|
||||
* @param resourceName The name of the resource
|
||||
* @param options Set if the material should be transparent or not.
|
||||
* @returns The requested material.
|
||||
*/
|
||||
getThreeMaterial(
|
||||
project: gd.Project,
|
||||
resourceName: string,
|
||||
{ useTransparentTexture }: { useTransparentTexture: boolean }
|
||||
): THREE.Material;
|
||||
|
||||
/**
|
||||
* Return the three.js material associated to the specified resource name.
|
||||
* @param project The project
|
||||
* @param resourceName The name of the resource
|
||||
* @param options
|
||||
* @returns The requested material.
|
||||
*/
|
||||
get3DModel(
|
||||
project: gd.Project,
|
||||
resourceName: string
|
||||
): Promise<THREE.THREE_ADDONS.GLTF>;
|
||||
|
||||
/**
|
||||
* Return the Pixi spine data for the specified resource name.
|
||||
* @param project The project
|
||||
* @param spineName The name of the spine json resource
|
||||
* @returns The requested spine skeleton.
|
||||
*/
|
||||
async getSpineData(
|
||||
project: gd.Project,
|
||||
spineName: string
|
||||
): Promise<SpineDataOrLoadingError>;
|
||||
|
||||
/**
|
||||
* Return the PIXI video texture represented by the given resource.
|
||||
* If not loaded, it will load it.
|
||||
* @returns The PIXI.Texture to be used. It can be loading, so you
|
||||
* should listen to PIXI.Texture `update` event, and refresh your object
|
||||
* if this event is triggered.
|
||||
*/
|
||||
getPIXIVideoTexture(project: gd.Project, resourceName: string): PIXI.Texture;
|
||||
|
||||
/**
|
||||
* Load the given font from its url/filename.
|
||||
* @returns a Promise that resolves with the font-family to be used
|
||||
* to render a text with the font.
|
||||
*/
|
||||
loadFontFamily(project: gd.Project, resourceName: string): Promise<string>;
|
||||
|
||||
/**
|
||||
* Get the font family name for the given font resource.
|
||||
* The font won't be loaded.
|
||||
* @returns The font-family to be used to render a text with the font.
|
||||
*/
|
||||
getFontFamily(project: gd.Project, resourceName: string): string;
|
||||
|
||||
/**
|
||||
* Get the data from a bitmap font file (fnt/xml) resource in the IDE.
|
||||
*/
|
||||
getBitmapFontData(project: gd.Project, resourceName: string): Promise<any>;
|
||||
|
||||
getInvalidPIXITexture();
|
||||
|
||||
getResourceJsonData(project: gd.Project, resourceName: string);
|
||||
}
|
||||
|
||||
/**
|
||||
* RenderedInstance is the base class used for creating 2D renderers of instances,
|
||||
* which display on the scene editor, using Pixi.js, the instance of an object (see InstancesEditor).
|
||||
@@ -14,17 +115,17 @@ class RenderedInstance {
|
||||
_instance: gd.InitialInstance;
|
||||
_associatedObjectConfiguration: gd.ObjectConfiguration;
|
||||
_pixiContainer: PIXI.Container;
|
||||
_pixiResourcesLoader: Class<PixiResourcesLoader>;
|
||||
_pixiResourcesLoader: PixiResourcesLoader;
|
||||
_pixiObject: PIXI.DisplayObject;
|
||||
wasUsed: boolean;
|
||||
|
||||
constructor(
|
||||
project: gdProject,
|
||||
layout: gdLayout,
|
||||
instance: gdInitialInstance,
|
||||
associatedObjectConfiguration: gdObjectConfiguration,
|
||||
project: gd.Project,
|
||||
layout: gd.Layout,
|
||||
instance: gd.InitialInstance,
|
||||
associatedObjectConfiguration: gd.ObjectConfiguration,
|
||||
pixiContainer: PIXI.Container,
|
||||
pixiResourcesLoader: Class<PixiResourcesLoader>
|
||||
pixiResourcesLoader: PixiResourcesLoader
|
||||
);
|
||||
|
||||
/**
|
||||
@@ -85,25 +186,25 @@ class RenderedInstance {
|
||||
* It can also display 2D artifacts on Pixi 2D plane (3D object shadow projected on the plane for instance).
|
||||
*/
|
||||
class Rendered3DInstance {
|
||||
_project: gdProject;
|
||||
_layout: gdLayout;
|
||||
_instance: gdInitialInstance;
|
||||
_associatedObjectConfiguration: gdObjectConfiguration;
|
||||
_project: gd.Project;
|
||||
_layout: gd.Layout;
|
||||
_instance: gd.InitialInstance;
|
||||
_associatedObjectConfiguration: gd.ObjectConfiguration;
|
||||
_pixiContainer: PIXI.Container;
|
||||
_threeGroup: THREE.Group;
|
||||
_pixiResourcesLoader: Class<PixiResourcesLoader>;
|
||||
_pixiResourcesLoader: PixiResourcesLoader;
|
||||
_pixiObject: PIXI.DisplayObject;
|
||||
_threeObject: THREE.Object3D | null;
|
||||
wasUsed: boolean;
|
||||
|
||||
constructor(
|
||||
project: gdProject,
|
||||
layout: gdLayout,
|
||||
instance: gdInitialInstance,
|
||||
associatedObjectConfiguration: gdObjectConfiguration,
|
||||
project: gd.Project,
|
||||
layout: gd.Layout,
|
||||
instance: gd.InitialInstance,
|
||||
associatedObjectConfiguration: gd.ObjectConfiguration,
|
||||
pixiContainer: PIXI.Container,
|
||||
threeGroup: THREE.Group,
|
||||
pixiResourcesLoader: Class<PixiResourcesLoader>
|
||||
pixiResourcesLoader: PixiResourcesLoader
|
||||
);
|
||||
|
||||
/**
|
||||
@@ -111,6 +212,19 @@ class Rendered3DInstance {
|
||||
*/
|
||||
static toRad(angleInDegrees: number): number;
|
||||
|
||||
/**
|
||||
* Applies ratio to value without intermediary value to avoid precision issues.
|
||||
*/
|
||||
static applyRatio({
|
||||
oldReferenceValue,
|
||||
newReferenceValue,
|
||||
valueToApplyTo,
|
||||
}: {
|
||||
oldReferenceValue: number;
|
||||
newReferenceValue: number;
|
||||
valueToApplyTo: number;
|
||||
}): number;
|
||||
|
||||
/**
|
||||
* Called when the scene editor is rendered.
|
||||
*/
|
||||
|
@@ -34,7 +34,9 @@ module.exports = {
|
||||
.addAction(
|
||||
'SavePlayerScore',
|
||||
_('Save player score'),
|
||||
_("Save the player's score to the given leaderboard."),
|
||||
_(
|
||||
"Save the player's score to the given leaderboard. If the player is connected, the score will be attached to the connected player (unless disabled)."
|
||||
),
|
||||
_(
|
||||
'Send to leaderboard _PARAM1_ the score _PARAM2_ with player name: _PARAM3_'
|
||||
),
|
||||
@@ -60,6 +62,12 @@ module.exports = {
|
||||
.getCodeExtraInformation()
|
||||
.setIncludeFile('Extensions/Leaderboards/sha256.js')
|
||||
.addIncludeFile('Extensions/Leaderboards/leaderboardstools.js')
|
||||
.addIncludeFile(
|
||||
'Extensions/PlayerAuthentication/playerauthenticationcomponents.js'
|
||||
)
|
||||
.addIncludeFile(
|
||||
'Extensions/PlayerAuthentication/playerauthenticationtools.js'
|
||||
)
|
||||
.setFunctionName('gdjs.evtTools.leaderboards.savePlayerScore')
|
||||
.setAsyncFunctionName('gdjs.evtTools.leaderboards.savePlayerScore');
|
||||
|
||||
@@ -87,11 +95,38 @@ module.exports = {
|
||||
.getCodeExtraInformation()
|
||||
.setIncludeFile('Extensions/Leaderboards/sha256.js')
|
||||
.addIncludeFile('Extensions/Leaderboards/leaderboardstools.js')
|
||||
.addIncludeFile(
|
||||
'Extensions/PlayerAuthentication/playerauthenticationcomponents.js'
|
||||
)
|
||||
.addIncludeFile(
|
||||
'Extensions/PlayerAuthentication/playerauthenticationtools.js'
|
||||
)
|
||||
.setFunctionName('gdjs.evtTools.leaderboards.saveConnectedPlayerScore')
|
||||
.setAsyncFunctionName(
|
||||
'gdjs.evtTools.leaderboards.saveConnectedPlayerScore'
|
||||
);
|
||||
|
||||
extension
|
||||
.addAction(
|
||||
'SetPreferSendConnectedPlayerScore',
|
||||
_('Always attach scores to the connected player'),
|
||||
_(
|
||||
'Set if the score sent to a leaderboard is always attached to the connected player - if any. This is on by default.'
|
||||
),
|
||||
_('Always attach the score to the connected player: _PARAM1_'),
|
||||
_('Setup'),
|
||||
'JsPlatform/Extensions/leaderboard.svg',
|
||||
'JsPlatform/Extensions/leaderboard.svg'
|
||||
)
|
||||
.addCodeOnlyParameter('currentScene', '')
|
||||
.addParameter('yesorno', _('Enable?'), '', false)
|
||||
.setHelpPath('/all-features/leaderboards')
|
||||
.getCodeExtraInformation()
|
||||
.setIncludeFile('Extensions/Leaderboards/leaderboardstools.js')
|
||||
.setFunctionName(
|
||||
'gdjs.evtTools.leaderboards.setPreferSendConnectedPlayerScore'
|
||||
);
|
||||
|
||||
extension
|
||||
.addCondition(
|
||||
'HasLastSaveErrored',
|
||||
@@ -273,6 +308,12 @@ module.exports = {
|
||||
.setHelpPath('/all-features/leaderboards')
|
||||
.getCodeExtraInformation()
|
||||
.setIncludeFile('Extensions/Leaderboards/leaderboardstools.js')
|
||||
.addIncludeFile(
|
||||
'Extensions/PlayerAuthentication/playerauthenticationcomponents.js'
|
||||
)
|
||||
.addIncludeFile(
|
||||
'Extensions/PlayerAuthentication/playerauthenticationtools.js'
|
||||
)
|
||||
.setFunctionName('gdjs.evtTools.leaderboards.displayLeaderboard');
|
||||
|
||||
extension
|
||||
|
@@ -6,6 +6,7 @@ namespace gdjs {
|
||||
export namespace evtTools {
|
||||
export namespace leaderboards {
|
||||
let _hasPlayerJustClosedLeaderboardView = false;
|
||||
let _preferSendConnectedPlayerScore = true;
|
||||
|
||||
gdjs.registerRuntimeScenePostEventsCallback(() => {
|
||||
// Set it back to false for the next frame.
|
||||
@@ -24,6 +25,14 @@ namespace gdjs {
|
||||
return shaObj.getHash('B64');
|
||||
};
|
||||
|
||||
const leaderboardHostBaseUrl = 'https://gd.games';
|
||||
// const leaderboardHostBaseUrl = 'http://localhost:4000';
|
||||
|
||||
type PublicLeaderboardEntry = {
|
||||
id: string;
|
||||
claimSecret?: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* Hold the state of the save of a score for a leaderboard.
|
||||
*/
|
||||
@@ -45,7 +54,7 @@ namespace gdjs {
|
||||
private _lastSavedPlayerId: string | null = null;
|
||||
|
||||
/** The id of the entry in the leaderboard, for the last score saved with success. */
|
||||
lastSavedLeaderboardEntryId: string | null = null;
|
||||
lastSavedLeaderboardEntry: PublicLeaderboardEntry | null = null;
|
||||
|
||||
/** Last error that happened when saving the score (useful if `hasScoreSavingErrored` is true). */
|
||||
lastSaveError: string | null = null;
|
||||
@@ -114,7 +123,7 @@ namespace gdjs {
|
||||
playerId?: string;
|
||||
score: number;
|
||||
}): {
|
||||
closeSaving: (leaderboardEntryId: string | null) => void;
|
||||
closeSaving: (leaderboardEntry: PublicLeaderboardEntry) => void;
|
||||
closeSavingWithError(errorCode: string);
|
||||
} {
|
||||
if (this._isAlreadySavingThisScore({ playerName, playerId, score })) {
|
||||
@@ -159,7 +168,7 @@ namespace gdjs {
|
||||
if (playerId) this._currentlySavingPlayerId = playerId;
|
||||
|
||||
return {
|
||||
closeSaving: (leaderboardEntryId) => {
|
||||
closeSaving: (leaderboardEntry) => {
|
||||
if (savingPromise !== this.lastSavingPromise) {
|
||||
logger.info(
|
||||
'Score saving result received, but another save was launched in the meantime - ignoring the result of this one.'
|
||||
@@ -174,7 +183,7 @@ namespace gdjs {
|
||||
this._lastSavedScore = this._currentlySavingScore;
|
||||
this._lastSavedPlayerName = this._currentlySavingPlayerName;
|
||||
this._lastSavedPlayerId = this._currentlySavingPlayerId;
|
||||
this.lastSavedLeaderboardEntryId = leaderboardEntryId;
|
||||
this.lastSavedLeaderboardEntry = leaderboardEntry;
|
||||
this.hasScoreBeenSaved = true;
|
||||
|
||||
resolveSavingPromise();
|
||||
@@ -214,7 +223,7 @@ namespace gdjs {
|
||||
let _leaderboardViewIframeLoading: boolean = false;
|
||||
let _leaderboardViewIframeLoaded: boolean = false;
|
||||
let _errorTimeoutId: NodeJS.Timeout | null = null;
|
||||
let _leaderboardViewClosingCallback:
|
||||
let _leaderboardMessageListener:
|
||||
| ((event: MessageEvent) => void)
|
||||
| null = null;
|
||||
|
||||
@@ -288,7 +297,7 @@ namespace gdjs {
|
||||
authenticatedPlayerData?: { playerId: string; playerToken: string };
|
||||
score: number;
|
||||
runtimeScene: gdjs.RuntimeScene;
|
||||
}) {
|
||||
}): Promise<PublicLeaderboardEntry> {
|
||||
const rootApi = runtimeScene
|
||||
.getGame()
|
||||
.isUsingGDevelopDevelopmentEnvironment()
|
||||
@@ -339,18 +348,18 @@ namespace gdjs {
|
||||
throw errorCode;
|
||||
}
|
||||
|
||||
let leaderboardEntryId: string | null = null;
|
||||
try {
|
||||
const leaderboardEntry = await response.json();
|
||||
leaderboardEntryId = leaderboardEntry.id;
|
||||
return leaderboardEntry;
|
||||
} catch (error) {
|
||||
logger.warn(
|
||||
'An error occurred when reading response but score has been saved:',
|
||||
error
|
||||
);
|
||||
}
|
||||
|
||||
return leaderboardEntryId;
|
||||
const errorCode = 'SAVED_ENTRY_CANT_BE_READ';
|
||||
throw errorCode;
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error('Error while submitting a leaderboard score:', error);
|
||||
const errorCode = 'REQUEST_NOT_SENT';
|
||||
@@ -359,13 +368,27 @@ namespace gdjs {
|
||||
}
|
||||
};
|
||||
|
||||
export const setPreferSendConnectedPlayerScore = (
|
||||
runtimeScene: gdjs.RuntimeScene,
|
||||
enable: boolean
|
||||
) => {
|
||||
_preferSendConnectedPlayerScore = enable;
|
||||
};
|
||||
|
||||
export const savePlayerScore = (
|
||||
runtimeScene: gdjs.RuntimeScene,
|
||||
leaderboardId: string,
|
||||
score: float,
|
||||
playerName: string
|
||||
) =>
|
||||
new gdjs.PromiseTask(
|
||||
) => {
|
||||
if (
|
||||
_preferSendConnectedPlayerScore &&
|
||||
gdjs.playerAuthentication.isAuthenticated()
|
||||
) {
|
||||
return saveConnectedPlayerScore(runtimeScene, leaderboardId, score);
|
||||
}
|
||||
|
||||
return new gdjs.PromiseTask(
|
||||
(async () => {
|
||||
const scoreSavingState = (_scoreSavingStateByLeaderboard[
|
||||
leaderboardId
|
||||
@@ -380,13 +403,13 @@ namespace gdjs {
|
||||
} = scoreSavingState.startSaving({ playerName, score });
|
||||
|
||||
try {
|
||||
const leaderboardEntryId = await saveScore({
|
||||
const leaderboardEntry = await saveScore({
|
||||
leaderboardId,
|
||||
playerName,
|
||||
score,
|
||||
runtimeScene,
|
||||
});
|
||||
closeSaving(leaderboardEntryId);
|
||||
closeSaving(leaderboardEntry);
|
||||
} catch (errorCode) {
|
||||
closeSavingWithError(errorCode);
|
||||
}
|
||||
@@ -395,6 +418,7 @@ namespace gdjs {
|
||||
}
|
||||
})()
|
||||
);
|
||||
};
|
||||
|
||||
export const saveConnectedPlayerScore = (
|
||||
runtimeScene: gdjs.RuntimeScene,
|
||||
@@ -551,7 +575,64 @@ namespace gdjs {
|
||||
displayLoader: boolean,
|
||||
event: MessageEvent
|
||||
) {
|
||||
switch (event.data) {
|
||||
const messageId =
|
||||
typeof event.data === 'string' ? event.data : event.data.id;
|
||||
switch (messageId) {
|
||||
case 'playerAuthenticated':
|
||||
gdjs.playerAuthentication.login({
|
||||
runtimeScene,
|
||||
userId: event.data.userId,
|
||||
username: event.data.username,
|
||||
userToken: event.data.userToken,
|
||||
});
|
||||
break;
|
||||
case 'openPlayerAuthentication':
|
||||
gdjs.playerAuthentication
|
||||
.openAuthenticationWindow(runtimeScene)
|
||||
.promise.then(({ status }) => {
|
||||
if (
|
||||
!_leaderboardViewIframe ||
|
||||
!_leaderboardViewIframe.contentWindow
|
||||
) {
|
||||
logger.warn(
|
||||
'Unable to transmit the new login status to the leaderboard view.'
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
if (status === 'errored') {
|
||||
_leaderboardViewIframe.contentWindow.postMessage(
|
||||
{
|
||||
id: 'onPlayerAuthenticationErrored',
|
||||
},
|
||||
leaderboardHostBaseUrl
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
const playerId = gdjs.playerAuthentication.getUserId();
|
||||
const playerToken = gdjs.playerAuthentication.getUserToken();
|
||||
if (status === 'dismissed' || !playerId || !playerToken) {
|
||||
_leaderboardViewIframe.contentWindow.postMessage(
|
||||
{
|
||||
id: 'onPlayerAuthenticationDismissed',
|
||||
},
|
||||
leaderboardHostBaseUrl
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
_leaderboardViewIframe.contentWindow.postMessage(
|
||||
{
|
||||
id: 'onPlayerAuthenticated',
|
||||
playerId,
|
||||
playerUsername: gdjs.playerAuthentication.getUsername(),
|
||||
playerToken: playerToken,
|
||||
},
|
||||
leaderboardHostBaseUrl
|
||||
);
|
||||
});
|
||||
break;
|
||||
case 'closeLeaderboardView':
|
||||
_hasPlayerJustClosedLeaderboardView = true;
|
||||
closeLeaderboardView(runtimeScene);
|
||||
@@ -599,7 +680,7 @@ namespace gdjs {
|
||||
'Leaderboard page did not send message in time. Closing leaderboard view.'
|
||||
);
|
||||
}
|
||||
}, 5000);
|
||||
}, 15000);
|
||||
};
|
||||
|
||||
const displayLoaderInLeaderboardView = function (
|
||||
@@ -701,15 +782,15 @@ namespace gdjs {
|
||||
});
|
||||
}
|
||||
|
||||
// If a save is being done for this leaderboard, wait for it to end so that the `lastSavedLeaderboardEntryId`
|
||||
// If a save is being done for this leaderboard, wait for it to end so that the `lastSavedLeaderboardEntry`
|
||||
// can be saved and then used to show the player score.
|
||||
const scoreSavingState = _scoreSavingStateByLeaderboard[leaderboardId];
|
||||
if (scoreSavingState && scoreSavingState.lastSavingPromise) {
|
||||
await scoreSavingState.lastSavingPromise;
|
||||
}
|
||||
|
||||
const lastSavedLeaderboardEntryId = scoreSavingState
|
||||
? scoreSavingState.lastSavedLeaderboardEntryId
|
||||
const lastSavedLeaderboardEntry = scoreSavingState
|
||||
? scoreSavingState.lastSavedLeaderboardEntry
|
||||
: null;
|
||||
|
||||
const gameId = gdjs.projectData.properties.projectUuid;
|
||||
@@ -720,13 +801,30 @@ namespace gdjs {
|
||||
const searchParams = new URLSearchParams();
|
||||
searchParams.set('inGameEmbedded', 'true');
|
||||
if (isDev) searchParams.set('dev', 'true');
|
||||
if (lastSavedLeaderboardEntryId)
|
||||
if (lastSavedLeaderboardEntry) {
|
||||
searchParams.set(
|
||||
'playerLeaderboardEntryId',
|
||||
lastSavedLeaderboardEntryId
|
||||
lastSavedLeaderboardEntry.id
|
||||
);
|
||||
if (lastSavedLeaderboardEntry.claimSecret) {
|
||||
searchParams.set(
|
||||
'playerLeaderboardEntryClaimSecret',
|
||||
lastSavedLeaderboardEntry.claimSecret
|
||||
);
|
||||
}
|
||||
}
|
||||
const playerId = gdjs.playerAuthentication.getUserId();
|
||||
const playerToken = gdjs.playerAuthentication.getUserToken();
|
||||
if (playerId && playerToken) {
|
||||
searchParams.set('playerId', playerId);
|
||||
searchParams.set('playerToken', playerToken);
|
||||
searchParams.set(
|
||||
'playerUsername',
|
||||
gdjs.playerAuthentication.getUsername()
|
||||
);
|
||||
}
|
||||
|
||||
const targetUrl = `https://gd.games/games/${gameId}/leaderboard/${leaderboardId}?${searchParams}`;
|
||||
const targetUrl = `${leaderboardHostBaseUrl}/games/${gameId}/leaderboard/${leaderboardId}?${searchParams}`;
|
||||
|
||||
try {
|
||||
const isAvailable = await checkLeaderboardAvailability(targetUrl);
|
||||
@@ -772,7 +870,7 @@ namespace gdjs {
|
||||
targetUrl
|
||||
);
|
||||
if (typeof window !== 'undefined') {
|
||||
_leaderboardViewClosingCallback = (event: MessageEvent) => {
|
||||
_leaderboardMessageListener = (event: MessageEvent) => {
|
||||
receiveMessageFromLeaderboardView(
|
||||
runtimeScene,
|
||||
displayLoader,
|
||||
@@ -781,7 +879,7 @@ namespace gdjs {
|
||||
};
|
||||
(window as any).addEventListener(
|
||||
'message',
|
||||
_leaderboardViewClosingCallback,
|
||||
_leaderboardMessageListener,
|
||||
true
|
||||
);
|
||||
}
|
||||
@@ -836,10 +934,10 @@ namespace gdjs {
|
||||
if (typeof window !== 'undefined') {
|
||||
(window as any).removeEventListener(
|
||||
'message',
|
||||
_leaderboardViewClosingCallback,
|
||||
_leaderboardMessageListener,
|
||||
true
|
||||
);
|
||||
_leaderboardViewClosingCallback = null;
|
||||
_leaderboardMessageListener = null;
|
||||
}
|
||||
domElementContainer.removeChild(_leaderboardViewIframe);
|
||||
_leaderboardViewIframe = null;
|
||||
|
10
Extensions/P2P/A_peer.js
vendored
10
Extensions/P2P/A_peer.js
vendored
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -422,6 +422,17 @@ namespace gdjs {
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Forces the usage of a relay (TURN) server, to avoid sharing IP addresses with the other peers.
|
||||
* @param shouldUseRelayServer Whether relay-only should be enabled or disabled.
|
||||
*/
|
||||
export const forceUseRelayServer = (shouldUseRelayServer: boolean) => {
|
||||
peerConfig.config = peerConfig.config || {};
|
||||
peerConfig.config.iceTransportPolicy = shouldUseRelayServer
|
||||
? 'relay'
|
||||
: 'all';
|
||||
};
|
||||
|
||||
/**
|
||||
* Overrides the default peer ID. Must be called before connecting to a broker.
|
||||
* Overriding the ID may have unwanted consequences. Do not use this feature
|
||||
|
@@ -174,6 +174,33 @@ module.exports = {
|
||||
.addIncludeFile('Extensions/P2P/B_p2ptools.js')
|
||||
.setFunctionName('gdjs.evtTools.p2p.useCustomICECandidate');
|
||||
|
||||
extension
|
||||
.addAction(
|
||||
'ForceRelayServer',
|
||||
_('Disable IP address sharing'),
|
||||
_(
|
||||
'Disables the sharing of IP addresses with the other peers. ' +
|
||||
'This action needs to be called BEFORE connecting to the broker server.'
|
||||
),
|
||||
_('Disable IP sharing: _PARAM0_'),
|
||||
'',
|
||||
'JsPlatform/Extensions/p2picon.svg',
|
||||
'JsPlatform/Extensions/p2picon.svg'
|
||||
)
|
||||
.addParameter(
|
||||
'yesorno',
|
||||
_('Disable sharing of IP addresses'),
|
||||
'Generally, it is recommended to keep sharing of IP addressed enabled ' +
|
||||
'to make connections faster and more often possible. ' +
|
||||
'Disabling IP address sharing will force all connections to pass messages through a ' +
|
||||
'TURN relay server, you can make P2P use one by adding one as an ICE candidate.',
|
||||
false
|
||||
)
|
||||
.getCodeExtraInformation()
|
||||
.setIncludeFile('Extensions/P2P/A_peer.js')
|
||||
.addIncludeFile('Extensions/P2P/B_p2ptools.js')
|
||||
.setFunctionName('gdjs.evtTools.p2p.forceUseRelayServer');
|
||||
|
||||
extension
|
||||
.addAction(
|
||||
'UseDefaultBroker',
|
||||
|
@@ -68,7 +68,10 @@ namespace gdjs {
|
||||
max: objectData.particleLifeTimeMax,
|
||||
},
|
||||
// A negative flow is "infinite flow" (all particles burst)
|
||||
frequency: objectData.flow < 0 ? 0.0001 : 1.0 / objectData.flow,
|
||||
frequency:
|
||||
objectData.flow < 0
|
||||
? ParticleEmitterObjectPixiRenderer.frequencyMinimumValue
|
||||
: 1.0 / objectData.flow,
|
||||
spawnChance: 1,
|
||||
particlesPerWave: objectData.flow < 0 ? objectData.maxParticleNb : 1,
|
||||
maxParticles: objectData.maxParticleNb,
|
||||
@@ -347,7 +350,10 @@ namespace gdjs {
|
||||
}
|
||||
|
||||
setFlow(flow: number, tank: number): void {
|
||||
this.emitter.frequency = flow < 0 ? 0.0001 : 1.0 / flow;
|
||||
this.emitter.frequency =
|
||||
flow < 0
|
||||
? ParticleEmitterObjectPixiRenderer.frequencyMinimumValue
|
||||
: 1.0 / flow;
|
||||
this.emitter.emitterLifetime = ParticleEmitterObjectPixiRenderer.computeLifetime(
|
||||
flow,
|
||||
tank
|
||||
@@ -420,11 +426,33 @@ namespace gdjs {
|
||||
return this.started;
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns `true` at the end of emission or at the start if it's paused.
|
||||
* Returns false if there is no limit.
|
||||
*/
|
||||
_mayHaveEndedEmission(): boolean {
|
||||
return (
|
||||
// No end can be reached if there is no flow.
|
||||
this.emitter.frequency >
|
||||
ParticleEmitterObjectPixiRenderer.frequencyMinimumValue &&
|
||||
// No end can be reached when there is no limit.
|
||||
this.emitter.emitterLifetime >= 0 &&
|
||||
// Pixi stops the emission at the end.
|
||||
!this.emitter.emit &&
|
||||
// Pixi reset `_emitterLife` to `emitterLifetime` at the end of emission
|
||||
// so there is no way to know if it is the end or the start.
|
||||
// @ts-ignore Use a private attribute.
|
||||
this.emitter._emitterLife === this.emitter.emitterLifetime
|
||||
);
|
||||
}
|
||||
|
||||
static computeLifetime(flow: number, tank: number): float {
|
||||
if (tank < 0) return -1;
|
||||
else if (flow < 0) return 0.001;
|
||||
else return (tank + 0.1) / flow;
|
||||
}
|
||||
|
||||
private static readonly frequencyMinimumValue = 0.0001;
|
||||
}
|
||||
|
||||
// @ts-ignore - Register the class to let the engine use it.
|
||||
|
@@ -113,6 +113,11 @@ namespace gdjs {
|
||||
_additiveRenderingDirty: boolean = true;
|
||||
// Don't mark texture as dirty if not using one.
|
||||
_textureDirty: boolean;
|
||||
/**
|
||||
* `true` only when the emission is paused by events.
|
||||
* It allows to tell the end of emission apart from it.
|
||||
*/
|
||||
_isEmissionPaused: boolean = false;
|
||||
|
||||
// @ts-ignore
|
||||
_renderer: gdjs.ParticleEmitterObjectRenderer;
|
||||
@@ -390,9 +395,11 @@ namespace gdjs {
|
||||
this._additiveRenderingDirty = this._maxParticlesCountDirty = this._particleRotationSpeedDirty = false;
|
||||
this._renderer.update(this.getElapsedTime() / 1000.0);
|
||||
if (
|
||||
this._renderer.hasStarted() &&
|
||||
this.destroyWhenNoParticles &&
|
||||
this.getParticleCount() === 0 &&
|
||||
this.destroyWhenNoParticles
|
||||
this._renderer.hasStarted() &&
|
||||
!this._isEmissionPaused &&
|
||||
this._renderer._mayHaveEndedEmission()
|
||||
) {
|
||||
this.deleteFromScene(instanceContainer);
|
||||
}
|
||||
@@ -803,10 +810,12 @@ namespace gdjs {
|
||||
}
|
||||
|
||||
startEmission(): void {
|
||||
this._isEmissionPaused = false;
|
||||
this._renderer.start();
|
||||
}
|
||||
|
||||
stopEmission(): void {
|
||||
this._isEmissionPaused = true;
|
||||
this._renderer.stop();
|
||||
}
|
||||
|
||||
|
@@ -586,7 +586,8 @@ module.exports = {
|
||||
.addParameter('behavior', _('Behavior'), 'Physics2Behavior')
|
||||
.addParameter('expression', _('Time scale (1 by default)'))
|
||||
.getCodeExtraInformation()
|
||||
.setIncludeFile('Extensions/Physics2Behavior/physics2tools.js')
|
||||
.addIncludeFile('Extensions/Physics2Behavior/physics2tools.js')
|
||||
.addIncludeFile('Extensions/Physics2Behavior/physics2runtimebehavior.js')
|
||||
.setFunctionName('gdjs.physics2.setTimeScale');
|
||||
|
||||
aut
|
||||
@@ -4094,7 +4095,8 @@ module.exports = {
|
||||
.addParameter('objectList', _('Object'), '', false)
|
||||
.addCodeOnlyParameter('conditionInverted', '')
|
||||
.getCodeExtraInformation()
|
||||
.setIncludeFile('Extensions/Physics2Behavior/physics2tools.js')
|
||||
.addIncludeFile('Extensions/Physics2Behavior/physics2tools.js')
|
||||
.addIncludeFile('Extensions/Physics2Behavior/physics2runtimebehavior.js')
|
||||
.setFunctionName('gdjs.physics2.objectsCollide');
|
||||
|
||||
extension
|
||||
@@ -4112,7 +4114,8 @@ module.exports = {
|
||||
.addParameter('objectList', _('Object'), '', false)
|
||||
.addCodeOnlyParameter('conditionInverted', '')
|
||||
.getCodeExtraInformation()
|
||||
.setIncludeFile('Extensions/Physics2Behavior/physics2tools.js')
|
||||
.addIncludeFile('Extensions/Physics2Behavior/physics2tools.js')
|
||||
.addIncludeFile('Extensions/Physics2Behavior/physics2runtimebehavior.js')
|
||||
.setFunctionName('gdjs.physics2.haveObjectsStartedColliding');
|
||||
|
||||
extension
|
||||
@@ -4130,7 +4133,8 @@ module.exports = {
|
||||
.addParameter('objectList', _('Object'), '', false)
|
||||
.addCodeOnlyParameter('conditionInverted', '')
|
||||
.getCodeExtraInformation()
|
||||
.setIncludeFile('Extensions/Physics2Behavior/physics2tools.js')
|
||||
.addIncludeFile('Extensions/Physics2Behavior/physics2tools.js')
|
||||
.addIncludeFile('Extensions/Physics2Behavior/physics2runtimebehavior.js')
|
||||
.setFunctionName('gdjs.physics2.haveObjectsStoppedColliding');
|
||||
|
||||
return extension;
|
||||
|
@@ -872,6 +872,5 @@ void DeclarePlatformBehaviorExtension(gd::PlatformExtension& extension) {
|
||||
.AddParameter("objectList", _("Object"), "", false)
|
||||
.AddParameter("behavior", _("Behavior"), "PlatformerObjectBehavior")
|
||||
.AddParameter("objectList", _("Platforms"), "", false)
|
||||
.AddCodeOnlyParameter("conditionInverted", "")
|
||||
.SetFunctionName("gdjs.evtTools.platform.isOnPlatform");
|
||||
.AddCodeOnlyParameter("conditionInverted", "");
|
||||
}
|
||||
|
@@ -41,6 +41,17 @@ class PlatformBehaviorJsExtension : public gd::PlatformExtension {
|
||||
.AddIncludeFile(
|
||||
"Extensions/PlatformBehavior/platformtools.js");
|
||||
|
||||
std::map<gd::String, gd::InstructionMetadata>& extConditions =
|
||||
GetAllConditions();
|
||||
extConditions["PlatformBehavior::IsObjectOnGivenFloor"].SetFunctionName(
|
||||
"gdjs.evtTools.platform.isOnPlatform")
|
||||
.AddIncludeFile(
|
||||
"Extensions/PlatformBehavior/platformruntimebehavior.js")
|
||||
.AddIncludeFile(
|
||||
"Extensions/PlatformBehavior/platformerobjectruntimebehavior.js")
|
||||
.AddIncludeFile(
|
||||
"Extensions/PlatformBehavior/platformtools.js");
|
||||
|
||||
{
|
||||
std::map<gd::String, gd::InstructionMetadata>& autActions =
|
||||
GetAllActionsForBehavior(
|
||||
|
@@ -262,6 +262,9 @@ namespace gdjs {
|
||||
const behavior1 = object1.getBehavior(
|
||||
behaviorName
|
||||
) as PlatformerObjectRuntimeBehavior;
|
||||
if (!behavior1) {
|
||||
return false;
|
||||
}
|
||||
return behavior1.isOnFloorObject(object2);
|
||||
}
|
||||
}
|
||||
|
@@ -100,7 +100,10 @@ module.exports = {
|
||||
.addIncludeFile(
|
||||
'Extensions/PlayerAuthentication/playerauthenticationtools.js'
|
||||
)
|
||||
.setFunctionName('gdjs.playerAuthentication.openAuthenticationWindow');
|
||||
.setFunctionName('gdjs.playerAuthentication.openAuthenticationWindow')
|
||||
.setAsyncFunctionName(
|
||||
'gdjs.playerAuthentication.openAuthenticationWindow'
|
||||
);
|
||||
|
||||
extension
|
||||
.addCondition(
|
||||
|
@@ -5,18 +5,25 @@ namespace gdjs {
|
||||
platform,
|
||||
isGameRegistered,
|
||||
}: {
|
||||
platform: 'cordova' | 'electron' | 'web';
|
||||
platform:
|
||||
| 'cordova'
|
||||
| 'cordova-websocket'
|
||||
| 'electron'
|
||||
| 'web-iframe'
|
||||
| 'web';
|
||||
isGameRegistered: boolean;
|
||||
}) =>
|
||||
isGameRegistered
|
||||
? {
|
||||
title: 'Logging in...',
|
||||
text1:
|
||||
platform === 'cordova'
|
||||
platform === 'cordova' || platform === 'cordova-websocket'
|
||||
? "One moment, we're opening a window for you to log in."
|
||||
: "One moment, we're opening a new page with your web browser for you to log in.",
|
||||
text2:
|
||||
'If the window did not open, please check your pop-up blocker and click the button below to try again.',
|
||||
platform === 'cordova' || platform === 'cordova-websocket'
|
||||
? ''
|
||||
: 'If the window did not open, please check your pop-up blocker and click the button below to try again.',
|
||||
}
|
||||
: {
|
||||
title: 'Publish your game!',
|
||||
@@ -166,9 +173,14 @@ namespace gdjs {
|
||||
*/
|
||||
export const addAuthenticationTextsToLoadingContainer = (
|
||||
loaderContainer: HTMLDivElement,
|
||||
platform,
|
||||
isGameRegistered,
|
||||
wikiOpenAction
|
||||
platform:
|
||||
| 'cordova'
|
||||
| 'cordova-websocket'
|
||||
| 'electron'
|
||||
| 'web-iframe'
|
||||
| 'web',
|
||||
isGameRegistered: boolean,
|
||||
wikiOpenAction: (() => void) | null
|
||||
) => {
|
||||
const textContainer: HTMLDivElement = document.createElement('div');
|
||||
textContainer.id = 'authentication-container-texts';
|
||||
|
@@ -15,7 +15,7 @@ namespace gdjs {
|
||||
|
||||
// Authentication display
|
||||
let _authenticationWindow: Window | null = null; // For Web.
|
||||
let _authenticationInAppWindow: Window | null = null; // For Cordova.
|
||||
let _authenticationInAppWindow: any | null = null; // For Cordova.
|
||||
let _authenticationRootContainer: HTMLDivElement | null = null;
|
||||
let _authenticationLoaderContainer: HTMLDivElement | null = null;
|
||||
let _authenticationIframeContainer: HTMLDivElement | null = null;
|
||||
@@ -28,11 +28,10 @@ namespace gdjs {
|
||||
let _authenticationMessageCallback:
|
||||
| ((event: MessageEvent) => void)
|
||||
| null = null;
|
||||
let _cordovaAuthenticationMessageCallback:
|
||||
| ((event: MessageEvent) => void)
|
||||
| null = null;
|
||||
let _websocket: WebSocket | null = null;
|
||||
|
||||
type AuthenticationWindowStatus = 'logged' | 'errored' | 'dismissed';
|
||||
|
||||
// Ensure that the condition "just logged in" is valid only for one frame.
|
||||
gdjs.registerRuntimeScenePostEventsCallback(() => {
|
||||
_justLoggedIn = false;
|
||||
@@ -43,13 +42,15 @@ namespace gdjs {
|
||||
// Then send a message to the parent iframe to say that the player auth is ready.
|
||||
gdjs.registerFirstRuntimeSceneLoadedCallback(
|
||||
(runtimeScene: RuntimeScene) => {
|
||||
if (getPlatform(runtimeScene) !== 'web') {
|
||||
if (getPlayerAuthPlatform(runtimeScene) !== 'web') {
|
||||
// Automatic authentication is only valid when the game is hosted on GDevelop games platform.
|
||||
return;
|
||||
}
|
||||
removeAuthenticationCallbacks(); // Remove any callback that could have been registered before.
|
||||
_authenticationMessageCallback = (event: MessageEvent) => {
|
||||
receiveAuthenticationMessage(runtimeScene, event, {
|
||||
receiveAuthenticationMessage({
|
||||
runtimeScene,
|
||||
event,
|
||||
checkOrigin: true,
|
||||
});
|
||||
};
|
||||
@@ -98,18 +99,40 @@ namespace gdjs {
|
||||
};
|
||||
|
||||
/**
|
||||
* Helper returning the platform.
|
||||
* Get the platform running the game, which changes how the authentication
|
||||
* window is opened.
|
||||
*/
|
||||
const getPlatform = (
|
||||
const getPlayerAuthPlatform = (
|
||||
runtimeScene: RuntimeScene
|
||||
): 'electron' | 'cordova' | 'web' => {
|
||||
): 'electron' | 'cordova' | 'cordova-websocket' | 'web-iframe' | 'web' => {
|
||||
const runtimeGame = runtimeScene.getGame();
|
||||
const electron = runtimeGame.getRenderer().getElectron();
|
||||
if (electron) {
|
||||
// This can be a:
|
||||
// - Preview in GDevelop desktop app.
|
||||
// - Desktop game running on Electron.
|
||||
return 'electron';
|
||||
}
|
||||
if (typeof cordova !== 'undefined') return 'cordova';
|
||||
|
||||
// This can be a:
|
||||
// - Preview in GDevelop mobile app (iOS only)
|
||||
if (shouldAuthenticationUseIframe(runtimeScene)) return 'web-iframe';
|
||||
|
||||
if (typeof cordova !== 'undefined') {
|
||||
if (cordova.platformId === 'ios') {
|
||||
// The game is an iOS app.
|
||||
return 'cordova-websocket';
|
||||
}
|
||||
|
||||
// The game is an Android app.
|
||||
return 'cordova';
|
||||
}
|
||||
|
||||
// This can be a:
|
||||
// - Preview in GDevelop web-app
|
||||
// - Preview in Gdevelop mobile app (Android only)
|
||||
// - Web game (gd.games or any website/server) accessed via a desktop browser...
|
||||
// - Or a web game accessed via a mobile browser (Android/iOS).
|
||||
return 'web';
|
||||
};
|
||||
|
||||
@@ -117,7 +140,8 @@ namespace gdjs {
|
||||
* Check if, in some exceptional cases, we allow authentication
|
||||
* to be done through a iframe.
|
||||
* This is usually discouraged as the user can't verify that the authentication
|
||||
* window is a genuine one. It's only to be used in trusted contexts.
|
||||
* window is a genuine one. It's only to be used in trusted contexts (e.g:
|
||||
* preview in the GDevelop mobile app).
|
||||
*/
|
||||
const shouldAuthenticationUseIframe = (runtimeScene: RuntimeScene) => {
|
||||
const runtimeGameOptions = runtimeScene.getGame().getAdditionalOptions();
|
||||
@@ -279,7 +303,10 @@ namespace gdjs {
|
||||
const cleanUpAuthWindowAndCallbacks = (runtimeScene: RuntimeScene) => {
|
||||
removeAuthenticationContainer(runtimeScene);
|
||||
clearAuthenticationWindowTimeout();
|
||||
|
||||
// If there is a websocket communication (electron, cordova iOS), close it.
|
||||
if (_websocket) {
|
||||
logger.info('Closing authentication websocket connection.');
|
||||
_websocket.close();
|
||||
_websocket = null;
|
||||
}
|
||||
@@ -334,16 +361,17 @@ namespace gdjs {
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* When the game receives the authentication result, close all the
|
||||
* authentication windows, display the notification and focus on the game.
|
||||
*/
|
||||
const handleLoggedInEvent = function (
|
||||
runtimeScene: gdjs.RuntimeScene,
|
||||
userId: string,
|
||||
username: string | null,
|
||||
userToken: string
|
||||
) {
|
||||
export const login = ({
|
||||
runtimeScene,
|
||||
userId,
|
||||
username,
|
||||
userToken,
|
||||
}: {
|
||||
runtimeScene: gdjs.RuntimeScene;
|
||||
userId: string;
|
||||
username: string | null;
|
||||
userToken: string;
|
||||
}) => {
|
||||
saveAuthKeyToStorage({ userId, username, userToken });
|
||||
cleanUpAuthWindowAndCallbacks(runtimeScene);
|
||||
removeAuthenticationBanner(runtimeScene);
|
||||
@@ -363,18 +391,23 @@ namespace gdjs {
|
||||
domElementContainer,
|
||||
_username || 'Anonymous'
|
||||
);
|
||||
focusOnGame(runtimeScene);
|
||||
};
|
||||
|
||||
/**
|
||||
* Reads the event sent by the authentication window and
|
||||
* display the appropriate banner.
|
||||
*/
|
||||
const receiveAuthenticationMessage = function (
|
||||
runtimeScene: gdjs.RuntimeScene,
|
||||
event: MessageEvent,
|
||||
{ checkOrigin }: { checkOrigin: boolean }
|
||||
) {
|
||||
const receiveAuthenticationMessage = function ({
|
||||
runtimeScene,
|
||||
event,
|
||||
checkOrigin,
|
||||
onDone,
|
||||
}: {
|
||||
runtimeScene: gdjs.RuntimeScene;
|
||||
event: MessageEvent;
|
||||
checkOrigin: boolean;
|
||||
onDone?: (status: 'logged' | 'errored' | 'dismissed') => void;
|
||||
}) {
|
||||
const allowedOrigins = ['https://liluo.io', 'https://gd.games'];
|
||||
|
||||
// Check origin of message.
|
||||
@@ -394,12 +427,14 @@ namespace gdjs {
|
||||
throw new Error('Malformed message.');
|
||||
}
|
||||
|
||||
handleLoggedInEvent(
|
||||
login({
|
||||
runtimeScene,
|
||||
event.data.body.userId,
|
||||
event.data.body.username,
|
||||
event.data.body.token
|
||||
);
|
||||
userId: event.data.body.userId,
|
||||
username: event.data.body.username,
|
||||
userToken: event.data.body.token,
|
||||
});
|
||||
focusOnGame(runtimeScene);
|
||||
if (onDone) onDone('logged');
|
||||
break;
|
||||
}
|
||||
case 'alreadyAuthenticated': {
|
||||
@@ -452,7 +487,7 @@ namespace gdjs {
|
||||
runtimeScene: gdjs.RuntimeScene
|
||||
) => {
|
||||
clearAuthenticationWindowTimeout();
|
||||
const time = 12 * 60 * 1000; // 12 minutes, in case the user needs time to authenticate.
|
||||
const time = 15 * 60 * 1000; // 15 minutes, in case the user needs time to authenticate.
|
||||
_authenticationTimeoutId = setTimeout(() => {
|
||||
logger.info(
|
||||
'Authentication window did not send message in time. Closing it.'
|
||||
@@ -487,6 +522,7 @@ namespace gdjs {
|
||||
const onOpenAuthenticationWindow = () => {
|
||||
openAuthenticationWindow(runtimeScene);
|
||||
};
|
||||
|
||||
return _userToken
|
||||
? authComponents.computeAuthenticatedBanner(
|
||||
onOpenAuthenticationWindow,
|
||||
@@ -558,6 +594,84 @@ namespace gdjs {
|
||||
);
|
||||
};
|
||||
|
||||
const setupWebsocketForAuthenticationWindow = (
|
||||
runtimeScene: gdjs.RuntimeScene,
|
||||
onOpenAuthenticationWindow: (options: {
|
||||
connectionId: string;
|
||||
resolve: (AuthenticationWindowStatus) => void;
|
||||
}) => void
|
||||
) =>
|
||||
new Promise<AuthenticationWindowStatus>((resolve) => {
|
||||
let hasFinishedAlready = false;
|
||||
const wsPlayApi = runtimeScene
|
||||
.getGame()
|
||||
.isUsingGDevelopDevelopmentEnvironment()
|
||||
? 'wss://api-ws-dev.gdevelop.io/play'
|
||||
: 'wss://api-ws.gdevelop.io/play';
|
||||
_websocket = new WebSocket(wsPlayApi);
|
||||
_websocket.onopen = () => {
|
||||
logger.info('Opened authentication websocket connection.');
|
||||
// When socket is open, ask for the connectionId, so that we can open the authentication window.
|
||||
if (_websocket) {
|
||||
_websocket.send(JSON.stringify({ action: 'getConnectionId' }));
|
||||
}
|
||||
};
|
||||
_websocket.onerror = () => {
|
||||
logger.info('Error in authentication websocket connection.');
|
||||
if (!hasFinishedAlready) {
|
||||
hasFinishedAlready = true;
|
||||
resolve('errored');
|
||||
}
|
||||
handleAuthenticationError(
|
||||
runtimeScene,
|
||||
'Error while connecting to the authentication server.'
|
||||
);
|
||||
};
|
||||
_websocket.onclose = () => {
|
||||
logger.info('Closing authentication websocket connection.');
|
||||
if (!hasFinishedAlready) {
|
||||
hasFinishedAlready = true;
|
||||
resolve('dismissed');
|
||||
}
|
||||
};
|
||||
_websocket.onmessage = (event) => {
|
||||
if (event.data) {
|
||||
const messageContent = JSON.parse(event.data);
|
||||
switch (messageContent.type) {
|
||||
case 'authenticationResult': {
|
||||
const messageData = messageContent.data;
|
||||
|
||||
login({
|
||||
runtimeScene,
|
||||
userId: messageData.userId,
|
||||
username: messageData.username,
|
||||
userToken: messageData.token,
|
||||
});
|
||||
focusOnGame(runtimeScene);
|
||||
|
||||
hasFinishedAlready = true;
|
||||
resolve('logged');
|
||||
break;
|
||||
}
|
||||
case 'connectionId': {
|
||||
const messageData = messageContent.data;
|
||||
const connectionId = messageData.connectionId;
|
||||
if (!connectionId) {
|
||||
logger.error('No WebSocket connectionId received');
|
||||
hasFinishedAlready = true;
|
||||
resolve('errored');
|
||||
return;
|
||||
}
|
||||
|
||||
logger.info('WebSocket connectionId received.');
|
||||
onOpenAuthenticationWindow({ connectionId, resolve });
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
/**
|
||||
* Helper to handle authentication window on Electron.
|
||||
* We open a new window, and create a websocket to know when the user is logged in.
|
||||
@@ -565,75 +679,67 @@ namespace gdjs {
|
||||
const openAuthenticationWindowForElectron = (
|
||||
runtimeScene: gdjs.RuntimeScene,
|
||||
gameId: string
|
||||
) => {
|
||||
const wsPlayApi = runtimeScene
|
||||
.getGame()
|
||||
.isUsingGDevelopDevelopmentEnvironment()
|
||||
? 'wss://api-ws-dev.gdevelop.io/play'
|
||||
: 'wss://api-ws.gdevelop.io/play';
|
||||
_websocket = new WebSocket(wsPlayApi);
|
||||
_websocket.onopen = () => {
|
||||
// When socket is open, ask for the connectionId, so that we can open the authentication window.
|
||||
if (_websocket) {
|
||||
_websocket.send(JSON.stringify({ action: 'getConnectionId' }));
|
||||
}
|
||||
};
|
||||
_websocket.onerror = () => {
|
||||
handleAuthenticationError(
|
||||
runtimeScene,
|
||||
'Error while connecting to the authentication server.'
|
||||
);
|
||||
};
|
||||
_websocket.onmessage = (event) => {
|
||||
if (event.data) {
|
||||
const messageContent = JSON.parse(event.data);
|
||||
switch (messageContent.type) {
|
||||
case 'authenticationResult': {
|
||||
const messageData = messageContent.data;
|
||||
handleLoggedInEvent(
|
||||
runtimeScene,
|
||||
messageData.userId,
|
||||
messageData.username,
|
||||
messageData.token
|
||||
);
|
||||
break;
|
||||
}
|
||||
case 'connectionId': {
|
||||
const messageData = messageContent.data;
|
||||
const connectionId = messageData.connectionId;
|
||||
if (!connectionId) {
|
||||
logger.error('No connectionId received');
|
||||
return;
|
||||
}
|
||||
) =>
|
||||
setupWebsocketForAuthenticationWindow(
|
||||
runtimeScene,
|
||||
({ connectionId }) => {
|
||||
const targetUrl = getAuthWindowUrl({
|
||||
runtimeGame: runtimeScene.getGame(),
|
||||
gameId,
|
||||
connectionId,
|
||||
});
|
||||
|
||||
const targetUrl = getAuthWindowUrl({
|
||||
runtimeGame: runtimeScene.getGame(),
|
||||
gameId,
|
||||
connectionId,
|
||||
});
|
||||
const electron = runtimeScene.getGame().getRenderer().getElectron();
|
||||
const openWindow = () => electron.shell.openExternal(targetUrl);
|
||||
|
||||
const electron = runtimeScene
|
||||
.getGame()
|
||||
.getRenderer()
|
||||
.getElectron();
|
||||
const openWindow = () => electron.shell.openExternal(targetUrl);
|
||||
openWindow();
|
||||
|
||||
openWindow();
|
||||
|
||||
// Add the link to the window in case a popup blocker is preventing the window from opening.
|
||||
if (_authenticationTextContainer) {
|
||||
authComponents.addAuthenticationUrlToTextsContainer(
|
||||
openWindow,
|
||||
_authenticationTextContainer
|
||||
);
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
// Add the link to the window in case a popup blocker is preventing the window from opening.
|
||||
if (_authenticationTextContainer) {
|
||||
authComponents.addAuthenticationUrlToTextsContainer(
|
||||
openWindow,
|
||||
_authenticationTextContainer
|
||||
);
|
||||
}
|
||||
}
|
||||
};
|
||||
};
|
||||
);
|
||||
|
||||
/**
|
||||
* Helper to handle authentication window on Cordova on iOS.
|
||||
* We open an InAppBrowser window, and listen to the websocket to know when the user is logged in.
|
||||
*/
|
||||
const openAuthenticationWindowForCordovaWithWebSocket = (
|
||||
runtimeScene: gdjs.RuntimeScene,
|
||||
gameId: string
|
||||
) =>
|
||||
setupWebsocketForAuthenticationWindow(
|
||||
runtimeScene,
|
||||
({ connectionId, resolve }) => {
|
||||
const targetUrl = getAuthWindowUrl({
|
||||
runtimeGame: runtimeScene.getGame(),
|
||||
gameId,
|
||||
connectionId,
|
||||
});
|
||||
|
||||
_authenticationInAppWindow = cordova.InAppBrowser.open(
|
||||
targetUrl,
|
||||
'authentication',
|
||||
'location=yes,toolbarcolor=#000000,hidenavigationbuttons=yes,closebuttoncolor=#FFFFFF' // location=yes is important to show the URL bar to the user.
|
||||
);
|
||||
if (!_authenticationInAppWindow) {
|
||||
resolve('errored');
|
||||
return;
|
||||
}
|
||||
|
||||
_authenticationInAppWindow.addEventListener(
|
||||
'exit',
|
||||
() => {
|
||||
resolve('dismissed');
|
||||
},
|
||||
true
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
/**
|
||||
* Helper to handle authentication window on Cordova.
|
||||
@@ -642,32 +748,52 @@ namespace gdjs {
|
||||
const openAuthenticationWindowForCordova = (
|
||||
runtimeScene: gdjs.RuntimeScene,
|
||||
gameId: string
|
||||
) => {
|
||||
const targetUrl = getAuthWindowUrl({
|
||||
runtimeGame: runtimeScene.getGame(),
|
||||
gameId,
|
||||
});
|
||||
) =>
|
||||
new Promise<AuthenticationWindowStatus>((resolve) => {
|
||||
const targetUrl = getAuthWindowUrl({
|
||||
runtimeGame: runtimeScene.getGame(),
|
||||
gameId,
|
||||
});
|
||||
|
||||
_authenticationInAppWindow = cordova.InAppBrowser.open(
|
||||
targetUrl,
|
||||
'authentication',
|
||||
'location=yes' // location=yes is important to show the URL bar to the user.
|
||||
);
|
||||
// Listen to messages posted on the authentication window, so that we can
|
||||
// know when the user is authenticated.
|
||||
if (_authenticationInAppWindow) {
|
||||
_cordovaAuthenticationMessageCallback = (event: MessageEvent) => {
|
||||
receiveAuthenticationMessage(runtimeScene, event, {
|
||||
checkOrigin: false, // For Cordova we don't check the origin, as the message is read from the InAppBrowser directly.
|
||||
});
|
||||
};
|
||||
_authenticationInAppWindow = cordova.InAppBrowser.open(
|
||||
targetUrl,
|
||||
'authentication',
|
||||
'location=yes,toolbarcolor=#000000,hidenavigationbuttons=yes,closebuttoncolor=#FFFFFF' // location=yes is important to show the URL bar to the user.
|
||||
);
|
||||
if (!_authenticationInAppWindow) {
|
||||
resolve('errored');
|
||||
return;
|
||||
}
|
||||
|
||||
// Listen to messages posted on the authentication window, so that we can
|
||||
// know when the user is authenticated.
|
||||
let isDoneAlready = false;
|
||||
_authenticationInAppWindow.addEventListener(
|
||||
'message',
|
||||
_cordovaAuthenticationMessageCallback,
|
||||
(event: MessageEvent) => {
|
||||
receiveAuthenticationMessage({
|
||||
runtimeScene,
|
||||
event,
|
||||
checkOrigin: false, // For Cordova we don't check the origin, as the message is read from the InAppBrowser directly.
|
||||
onDone: (status) => {
|
||||
if (isDoneAlready) return;
|
||||
isDoneAlready = true;
|
||||
resolve(status);
|
||||
},
|
||||
});
|
||||
},
|
||||
true
|
||||
);
|
||||
}
|
||||
};
|
||||
_authenticationInAppWindow.addEventListener(
|
||||
'exit',
|
||||
() => {
|
||||
if (isDoneAlready) return;
|
||||
isDoneAlready = true;
|
||||
resolve('dismissed');
|
||||
},
|
||||
true
|
||||
);
|
||||
});
|
||||
|
||||
/**
|
||||
* Helper to handle authentication window on web.
|
||||
@@ -676,37 +802,56 @@ namespace gdjs {
|
||||
const openAuthenticationWindowForWeb = (
|
||||
runtimeScene: gdjs.RuntimeScene,
|
||||
gameId: string
|
||||
) => {
|
||||
// If we're on a browser, open a new window.
|
||||
const targetUrl = getAuthWindowUrl({
|
||||
runtimeGame: runtimeScene.getGame(),
|
||||
gameId,
|
||||
});
|
||||
|
||||
// Listen to messages posted by the authentication window, so that we can
|
||||
// know when the user is authenticated.
|
||||
_authenticationMessageCallback = (event: MessageEvent) => {
|
||||
receiveAuthenticationMessage(runtimeScene, event, {
|
||||
checkOrigin: true,
|
||||
) =>
|
||||
new Promise<AuthenticationWindowStatus>((resolve) => {
|
||||
// If we're on a browser, open a new window.
|
||||
const targetUrl = getAuthWindowUrl({
|
||||
runtimeGame: runtimeScene.getGame(),
|
||||
gameId,
|
||||
});
|
||||
};
|
||||
window.addEventListener('message', _authenticationMessageCallback, true);
|
||||
|
||||
const left = screen.width / 2 - 500 / 2;
|
||||
const top = screen.height / 2 - 600 / 2;
|
||||
const windowFeatures = `left=${left},top=${top},width=500,height=600`;
|
||||
const openWindow = () =>
|
||||
window.open(targetUrl, 'authentication', windowFeatures);
|
||||
_authenticationWindow = openWindow();
|
||||
|
||||
// Add the link to the window in case a popup blocker is preventing the window from opening.
|
||||
if (_authenticationTextContainer) {
|
||||
authComponents.addAuthenticationUrlToTextsContainer(
|
||||
openWindow,
|
||||
_authenticationTextContainer
|
||||
// Listen to messages posted by the authentication window, so that we can
|
||||
// know when the user is authenticated.
|
||||
let isDoneAlready = false;
|
||||
_authenticationMessageCallback = (event: MessageEvent) => {
|
||||
receiveAuthenticationMessage({
|
||||
runtimeScene,
|
||||
event,
|
||||
checkOrigin: true,
|
||||
onDone: (status) => {
|
||||
if (isDoneAlready) return;
|
||||
isDoneAlready = true;
|
||||
resolve(status);
|
||||
},
|
||||
});
|
||||
};
|
||||
window.addEventListener(
|
||||
'message',
|
||||
_authenticationMessageCallback,
|
||||
true
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
const left = screen.width / 2 - 500 / 2;
|
||||
const top = screen.height / 2 - 600 / 2;
|
||||
const windowFeatures = `left=${left},top=${top},width=500,height=600`;
|
||||
const openWindow = () => {
|
||||
_authenticationWindow = window.open(
|
||||
targetUrl,
|
||||
'authentication',
|
||||
windowFeatures
|
||||
);
|
||||
};
|
||||
|
||||
openWindow();
|
||||
|
||||
// Add the link to the window in case a popup blocker is preventing the window from opening.
|
||||
if (_authenticationTextContainer) {
|
||||
authComponents.addAuthenticationUrlToTextsContainer(
|
||||
openWindow,
|
||||
_authenticationTextContainer
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* Helper to handle authentication iframe on web.
|
||||
@@ -715,143 +860,197 @@ namespace gdjs {
|
||||
const openAuthenticationIframeForWeb = (
|
||||
runtimeScene: gdjs.RuntimeScene,
|
||||
gameId: string
|
||||
) => {
|
||||
if (
|
||||
!_authenticationIframeContainer ||
|
||||
!_authenticationLoaderContainer ||
|
||||
!_authenticationTextContainer
|
||||
) {
|
||||
console.error(
|
||||
"Can't open an authentication iframe - no iframe container, loader container or text container was opened for it."
|
||||
);
|
||||
return;
|
||||
}
|
||||
) =>
|
||||
new Promise<AuthenticationWindowStatus>((resolve) => {
|
||||
if (
|
||||
!_authenticationIframeContainer ||
|
||||
!_authenticationLoaderContainer ||
|
||||
!_authenticationTextContainer
|
||||
) {
|
||||
console.error(
|
||||
"Can't open an authentication iframe - no iframe container, loader container or text container was opened for it."
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
const targetUrl = getAuthWindowUrl({
|
||||
runtimeGame: runtimeScene.getGame(),
|
||||
gameId,
|
||||
});
|
||||
|
||||
// Listen to messages posted by the authentication window, so that we can
|
||||
// know when the user is authenticated.
|
||||
_authenticationMessageCallback = (event: MessageEvent) => {
|
||||
receiveAuthenticationMessage(runtimeScene, event, {
|
||||
checkOrigin: true,
|
||||
const targetUrl = getAuthWindowUrl({
|
||||
runtimeGame: runtimeScene.getGame(),
|
||||
gameId,
|
||||
});
|
||||
};
|
||||
window.addEventListener('message', _authenticationMessageCallback, true);
|
||||
|
||||
authComponents.displayIframeInsideAuthenticationContainer(
|
||||
_authenticationIframeContainer,
|
||||
_authenticationLoaderContainer,
|
||||
_authenticationTextContainer,
|
||||
targetUrl
|
||||
);
|
||||
};
|
||||
// Listen to messages posted by the authentication window, so that we can
|
||||
// know when the user is authenticated.
|
||||
_authenticationMessageCallback = (event: MessageEvent) => {
|
||||
receiveAuthenticationMessage({
|
||||
runtimeScene,
|
||||
event,
|
||||
checkOrigin: true,
|
||||
onDone: resolve,
|
||||
});
|
||||
};
|
||||
window.addEventListener(
|
||||
'message',
|
||||
_authenticationMessageCallback,
|
||||
true
|
||||
);
|
||||
|
||||
authComponents.displayIframeInsideAuthenticationContainer(
|
||||
_authenticationIframeContainer,
|
||||
_authenticationLoaderContainer,
|
||||
_authenticationTextContainer,
|
||||
targetUrl
|
||||
);
|
||||
});
|
||||
|
||||
/**
|
||||
* Action to display the authentication window to the user.
|
||||
*/
|
||||
export const openAuthenticationWindow = function (
|
||||
export const openAuthenticationWindow = (
|
||||
runtimeScene: gdjs.RuntimeScene
|
||||
) {
|
||||
// Create the authentication container for the player to wait.
|
||||
const domElementContainer = runtimeScene
|
||||
.getGame()
|
||||
.getRenderer()
|
||||
.getDomElementContainer();
|
||||
if (!domElementContainer) {
|
||||
handleAuthenticationError(
|
||||
runtimeScene,
|
||||
"The div element covering the game couldn't be found, the authentication window cannot be displayed."
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
const onAuthenticationContainerDismissed = () => {
|
||||
cleanUpAuthWindowAndCallbacks(runtimeScene);
|
||||
displayAuthenticationBanner(runtimeScene);
|
||||
};
|
||||
|
||||
const _gameId = gdjs.projectData.properties.projectUuid;
|
||||
if (!_gameId) {
|
||||
handleAuthenticationError(
|
||||
runtimeScene,
|
||||
'The game ID is missing, the authentication window cannot be opened.'
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
// If the banner is displayed, hide it, so that it can be shown again if the user closes the window.
|
||||
if (_authenticationBanner) _authenticationBanner.style.opacity = '0';
|
||||
|
||||
const platform = getPlatform(runtimeScene);
|
||||
const {
|
||||
rootContainer,
|
||||
loaderContainer,
|
||||
iframeContainer,
|
||||
} = authComponents.computeAuthenticationContainer(
|
||||
onAuthenticationContainerDismissed
|
||||
);
|
||||
_authenticationRootContainer = rootContainer;
|
||||
_authenticationLoaderContainer = loaderContainer;
|
||||
_authenticationIframeContainer = iframeContainer;
|
||||
|
||||
// Display the authentication window right away, to show a loader
|
||||
// while the call for game registration is happening.
|
||||
domElementContainer.appendChild(_authenticationRootContainer);
|
||||
|
||||
// If the game is registered, open the authentication window.
|
||||
// Otherwise, open the window indicating that the game is not registered.
|
||||
checkIfGameIsRegistered(runtimeScene.getGame(), _gameId)
|
||||
.then((isGameRegistered) => {
|
||||
if (_authenticationLoaderContainer) {
|
||||
const electron = runtimeScene.getGame().getRenderer().getElectron();
|
||||
const wikiOpenAction = electron
|
||||
? () =>
|
||||
electron.shell.openExternal(
|
||||
'https://wiki.gdevelop.io/gdevelop5/publishing/web'
|
||||
)
|
||||
: null; // Only show a link if we're on electron.
|
||||
|
||||
_authenticationTextContainer = authComponents.addAuthenticationTextsToLoadingContainer(
|
||||
_authenticationLoaderContainer,
|
||||
platform,
|
||||
isGameRegistered,
|
||||
wikiOpenAction
|
||||
): gdjs.PromiseTask<{ status: 'logged' | 'errored' | 'dismissed' }> =>
|
||||
new gdjs.PromiseTask(
|
||||
new Promise((resolve) => {
|
||||
// Create the authentication container for the player to wait.
|
||||
const domElementContainer = runtimeScene
|
||||
.getGame()
|
||||
.getRenderer()
|
||||
.getDomElementContainer();
|
||||
if (!domElementContainer) {
|
||||
handleAuthenticationError(
|
||||
runtimeScene,
|
||||
"The div element covering the game couldn't be found, the authentication window cannot be displayed."
|
||||
);
|
||||
resolve({ status: 'errored' });
|
||||
return;
|
||||
}
|
||||
if (isGameRegistered) {
|
||||
|
||||
const _gameId = gdjs.projectData.properties.projectUuid;
|
||||
if (!_gameId) {
|
||||
handleAuthenticationError(
|
||||
runtimeScene,
|
||||
'The game ID is missing, the authentication window cannot be opened.'
|
||||
);
|
||||
resolve({ status: 'errored' });
|
||||
return;
|
||||
}
|
||||
|
||||
let isDimissedAlready = false;
|
||||
const onAuthenticationContainerDismissed = () => {
|
||||
cleanUpAuthWindowAndCallbacks(runtimeScene);
|
||||
displayAuthenticationBanner(runtimeScene);
|
||||
|
||||
isDimissedAlready = true;
|
||||
resolve({ status: 'dismissed' });
|
||||
};
|
||||
|
||||
// If the banner is displayed, hide it, so that it can be shown again if the user closes the window.
|
||||
if (_authenticationBanner) _authenticationBanner.style.opacity = '0';
|
||||
|
||||
const playerAuthPlatform = getPlayerAuthPlatform(runtimeScene);
|
||||
const {
|
||||
rootContainer,
|
||||
loaderContainer,
|
||||
iframeContainer,
|
||||
} = authComponents.computeAuthenticationContainer(
|
||||
onAuthenticationContainerDismissed
|
||||
);
|
||||
_authenticationRootContainer = rootContainer;
|
||||
_authenticationLoaderContainer = loaderContainer;
|
||||
_authenticationIframeContainer = iframeContainer;
|
||||
|
||||
// Display the authentication window right away, to show a loader
|
||||
// while the call for game registration is happening.
|
||||
domElementContainer.appendChild(_authenticationRootContainer);
|
||||
|
||||
// If the game is registered, open the authentication window.
|
||||
// Otherwise, open the window indicating that the game is not registered.
|
||||
(async () => {
|
||||
const isGameRegistered = await checkIfGameIsRegistered(
|
||||
runtimeScene.getGame(),
|
||||
_gameId
|
||||
);
|
||||
|
||||
if (_authenticationLoaderContainer) {
|
||||
const electron = runtimeScene
|
||||
.getGame()
|
||||
.getRenderer()
|
||||
.getElectron();
|
||||
const wikiOpenAction = electron
|
||||
? () =>
|
||||
electron.shell.openExternal(
|
||||
'https://wiki.gdevelop.io/gdevelop5/publishing/web'
|
||||
)
|
||||
: null; // Only show a link if we're on electron.
|
||||
|
||||
_authenticationTextContainer = authComponents.addAuthenticationTextsToLoadingContainer(
|
||||
_authenticationLoaderContainer,
|
||||
playerAuthPlatform,
|
||||
isGameRegistered,
|
||||
wikiOpenAction
|
||||
);
|
||||
}
|
||||
if (!isGameRegistered) return;
|
||||
|
||||
startAuthenticationWindowTimeout(runtimeScene);
|
||||
|
||||
// Based on which platform the game is running, we open the authentication window
|
||||
// with a different window, with or without a websocket.
|
||||
switch (platform) {
|
||||
let status: AuthenticationWindowStatus;
|
||||
switch (playerAuthPlatform) {
|
||||
case 'electron':
|
||||
openAuthenticationWindowForElectron(runtimeScene, _gameId);
|
||||
// This can be a:
|
||||
// - Preview in GDevelop desktop app.
|
||||
// - Desktop game running on Electron.
|
||||
status = await openAuthenticationWindowForElectron(
|
||||
runtimeScene,
|
||||
_gameId
|
||||
);
|
||||
break;
|
||||
case 'cordova':
|
||||
openAuthenticationWindowForCordova(runtimeScene, _gameId);
|
||||
// The game is an Android app.
|
||||
status = await openAuthenticationWindowForCordova(
|
||||
runtimeScene,
|
||||
_gameId
|
||||
);
|
||||
break;
|
||||
case 'cordova-websocket':
|
||||
// The game is an iOS app.
|
||||
status = await openAuthenticationWindowForCordovaWithWebSocket(
|
||||
runtimeScene,
|
||||
_gameId
|
||||
);
|
||||
break;
|
||||
case 'web-iframe':
|
||||
// This can be a:
|
||||
// - Preview in GDevelop mobile app (iOS only)
|
||||
status = await openAuthenticationIframeForWeb(
|
||||
runtimeScene,
|
||||
_gameId
|
||||
);
|
||||
break;
|
||||
case 'web':
|
||||
default:
|
||||
if (shouldAuthenticationUseIframe(runtimeScene)) {
|
||||
openAuthenticationIframeForWeb(runtimeScene, _gameId);
|
||||
} else {
|
||||
openAuthenticationWindowForWeb(runtimeScene, _gameId);
|
||||
}
|
||||
// This can be a:
|
||||
// - Preview in GDevelop web-app
|
||||
// - Preview in Gdevelop mobile app (Android only)
|
||||
// - Web game (gd.games or any website/server) accessed via a desktop browser...
|
||||
// - Or a web game accessed via a mobile browser (Android/iOS).
|
||||
status = await openAuthenticationWindowForWeb(
|
||||
runtimeScene,
|
||||
_gameId
|
||||
);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (isDimissedAlready) return;
|
||||
if (status === 'dismissed') {
|
||||
onAuthenticationContainerDismissed();
|
||||
}
|
||||
|
||||
resolve({ status });
|
||||
})();
|
||||
})
|
||||
.catch((error) => {
|
||||
handleAuthenticationError(
|
||||
runtimeScene,
|
||||
'Error while checking if the game is registered.'
|
||||
);
|
||||
logger.error(error);
|
||||
});
|
||||
};
|
||||
);
|
||||
|
||||
/**
|
||||
* Condition to check if the window is open, so that the game can be paused in the background.
|
||||
@@ -901,8 +1100,6 @@ namespace gdjs {
|
||||
true
|
||||
);
|
||||
_authenticationMessageCallback = null;
|
||||
// No need to detach the callback from the InAppBrowser, as it's destroyed when the window is closed.
|
||||
_cordovaAuthenticationMessageCallback = null;
|
||||
}
|
||||
};
|
||||
|
||||
|
@@ -1290,6 +1290,7 @@ module.exports = {
|
||||
manager.getOrLoadTextureCache(
|
||||
this._loadTileMapWithCallback.bind(this),
|
||||
(textureName) =>
|
||||
// @ts-ignore
|
||||
this._pixiResourcesLoader.getPIXITexture(
|
||||
this._project,
|
||||
mapping[textureName] || textureName
|
||||
|
@@ -88,7 +88,7 @@ namespace gdjs {
|
||||
* @param callback A function called when the tiles textures are split.
|
||||
*/
|
||||
getOrLoadTextureCache(
|
||||
getTexture: (textureName: string) => PIXI.BaseTexture<PIXI.Resource>,
|
||||
getTexture: (textureName: string) => PIXI.Texture,
|
||||
atlasImageResourceName: string,
|
||||
tileMapJsonResourceName: string,
|
||||
tileSetJsonResourceName: string,
|
||||
|
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -58,7 +58,7 @@ export declare class TileMapManager {
|
||||
tileSetJsonResourceName: string,
|
||||
callback: (tileMapFileContent: TileMapFileContent | null) => void
|
||||
) => void,
|
||||
getTexture: (textureName: string) => PIXI.BaseTexture<PIXI.Resource>,
|
||||
getTexture: (textureName: string) => PIXI.Texture,
|
||||
atlasImageResourceName: string,
|
||||
tileMapJsonResourceName: string,
|
||||
tileSetJsonResourceName: string,
|
||||
|
@@ -1 +1 @@
|
||||
{"version":3,"file":"TileMapManager.d.ts","sourceRoot":"","sources":["../../src/render/TileMapManager.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,eAAe,EAAE,MAAM,uBAAuB,CAAC;AACxD,OAAO,EAAE,gBAAgB,EAAE,MAAM,oBAAoB,CAAC;AAGtD,OAAO,EAAE,kBAAkB,EAAE,MAAM,4BAA4B,CAAC;AAEhE;;;;;;;GAOG;AACH,qBAAa,cAAc;IACzB,OAAO,CAAC,aAAa,CAAiC;IACtD,OAAO,CAAC,mBAAmB,CAAkC;;IAO7D;;;OAGG;IACH,MAAM,CAAC,UAAU,CAAC,cAAc,EAAE,MAAM,GAAG,cAAc;IAWzD;;;OAGG;IACH,MAAM,CAAC,QAAQ,CAAC,IAAI,EAAE,GAAG,GAAG,kBAAkB,GAAG,IAAI;IAwBrD;;;;;;;OAOG;IACH,gBAAgB,CACd,WAAW,EAAE,CACX,uBAAuB,EAAE,MAAM,EAC/B,uBAAuB,EAAE,MAAM,EAC/B,QAAQ,EAAE,CAAC,kBAAkB,EAAE,kBAAkB,GAAG,IAAI,KAAK,IAAI,KAC9D,IAAI,EACT,uBAAuB,EAAE,MAAM,EAC/B,uBAAuB,EAAE,MAAM,EAC/B,UAAU,EAAE,MAAM,EAClB,IAAI,EAAE,GAAG,EACT,QAAQ,EAAE,CAAC,OAAO,EAAE,eAAe,GAAG,IAAI,KAAK,IAAI,GAClD,IAAI;IAiCP;;;;;;;;OAQG;IACH,qBAAqB,CACnB,WAAW,EAAE,CACX,uBAAuB,EAAE,MAAM,EAC/B,uBAAuB,EAAE,MAAM,EAC/B,QAAQ,EAAE,CAAC,kBAAkB,EAAE,kBAAkB,GAAG,IAAI,KAAK,IAAI,KAC9D,IAAI,EACT,UAAU,EAAE,CAAC,WAAW,EAAE,MAAM,KAAK,IAAI,CAAC,WAAW,CAAC,IAAI,CAAC,QAAQ,CAAC,EACpE,sBAAsB,EAAE,MAAM,EAC9B,uBAAuB,EAAE,MAAM,EAC/B,uBAAuB,EAAE,MAAM,EAC/B,UAAU,EAAE,MAAM,EAClB,QAAQ,EAAE,CAAC,YAAY,EAAE,gBAAgB,GAAG,IAAI,KAAK,IAAI,GACxD,IAAI;IAwCP,WAAW,IAAI,IAAI;CAIpB"}
|
||||
{"version":3,"file":"TileMapManager.d.ts","sourceRoot":"","sources":["../../src/render/TileMapManager.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,eAAe,EAAE,MAAM,uBAAuB,CAAC;AACxD,OAAO,EAAE,gBAAgB,EAAE,MAAM,oBAAoB,CAAC;AAGtD,OAAO,EAAE,kBAAkB,EAAE,MAAM,4BAA4B,CAAC;AAEhE;;;;;;;GAOG;AACH,qBAAa,cAAc;IACzB,OAAO,CAAC,aAAa,CAAiC;IACtD,OAAO,CAAC,mBAAmB,CAAkC;;IAO7D;;;OAGG;IACH,MAAM,CAAC,UAAU,CAAC,cAAc,EAAE,MAAM,GAAG,cAAc;IAWzD;;;OAGG;IACH,MAAM,CAAC,QAAQ,CAAC,IAAI,EAAE,GAAG,GAAG,kBAAkB,GAAG,IAAI;IAwBrD;;;;;;;OAOG;IACH,gBAAgB,CACd,WAAW,EAAE,CACX,uBAAuB,EAAE,MAAM,EAC/B,uBAAuB,EAAE,MAAM,EAC/B,QAAQ,EAAE,CAAC,kBAAkB,EAAE,kBAAkB,GAAG,IAAI,KAAK,IAAI,KAC9D,IAAI,EACT,uBAAuB,EAAE,MAAM,EAC/B,uBAAuB,EAAE,MAAM,EAC/B,UAAU,EAAE,MAAM,EAClB,IAAI,EAAE,GAAG,EACT,QAAQ,EAAE,CAAC,OAAO,EAAE,eAAe,GAAG,IAAI,KAAK,IAAI,GAClD,IAAI;IAiCP;;;;;;;;OAQG;IACH,qBAAqB,CACnB,WAAW,EAAE,CACX,uBAAuB,EAAE,MAAM,EAC/B,uBAAuB,EAAE,MAAM,EAC/B,QAAQ,EAAE,CAAC,kBAAkB,EAAE,kBAAkB,GAAG,IAAI,KAAK,IAAI,KAC9D,IAAI,EACT,UAAU,EAAE,CAAC,WAAW,EAAE,MAAM,KAAK,IAAI,CAAC,OAAO,EACjD,sBAAsB,EAAE,MAAM,EAC9B,uBAAuB,EAAE,MAAM,EAC/B,uBAAuB,EAAE,MAAM,EAC/B,UAAU,EAAE,MAAM,EAClB,QAAQ,EAAE,CAAC,YAAY,EAAE,gBAAgB,GAAG,IAAI,KAAK,IAAI,GACxD,IAAI;IAwCP,WAAW,IAAI,IAAI;CAIpB"}
|
@@ -15,8 +15,8 @@ export declare namespace PixiTileMapHelper {
|
||||
function parseAtlas(
|
||||
tileMap: TileMapFileContent,
|
||||
levelIndex: number,
|
||||
atlasTexture: PIXI.BaseTexture<PIXI.Resource> | null,
|
||||
getTexture: (textureName: string) => PIXI.BaseTexture<PIXI.Resource>
|
||||
atlasTexture: PIXI.Texture | null,
|
||||
getTexture: (textureName: string) => PIXI.Texture
|
||||
): TileTextureCache | null;
|
||||
/**
|
||||
* Re-renders the tile map whenever its rendering settings have been changed
|
||||
|
@@ -1 +1 @@
|
||||
{"version":3,"file":"TileMapPixiHelper.d.ts","sourceRoot":"","sources":["../../src/render/TileMapPixiHelper.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,KAAK,EAAE,MAAM,sBAAsB,CAAC;AACtD,OAAO,EAEL,eAAe,EAEhB,MAAM,uBAAuB,CAAC;AAG/B,OAAO,EAAE,kBAAkB,EAAE,MAAM,4BAA4B,CAAC;AAChE,OAAO,EAAE,gBAAgB,EAAE,MAAM,oBAAoB,CAAC;AAGtD,yBAAiB,iBAAiB,CAAC;IACjC;;;;;;;;OAQG;IACH,SAAgB,UAAU,CACxB,OAAO,EAAE,kBAAkB,EAC3B,UAAU,EAAE,MAAM,EAClB,YAAY,EAAE,IAAI,CAAC,WAAW,CAAC,IAAI,CAAC,QAAQ,CAAC,GAAG,IAAI,EACpD,UAAU,EAAE,CAAC,WAAW,EAAE,MAAM,KAAK,IAAI,CAAC,WAAW,CAAC,IAAI,CAAC,QAAQ,CAAC,GACnE,gBAAgB,GAAG,IAAI,CAuBzB;IAED;;;;;;;;;;;;OAYG;IACH,SAAgB,iBAAiB,CAC/B,kBAAkB,EAAE,GAAG,EACvB,OAAO,EAAE,eAAe,EACxB,YAAY,EAAE,gBAAgB,EAC9B,WAAW,EAAE,OAAO,GAAG,SAAS,GAAG,KAAK,EACxC,UAAU,EAAE,MAAM,GACjB,IAAI,CA0GN;IAED;;OAEG;IACH,SAAgB,uBAAuB,CACrC,YAAY,EAAE,IAAI,CAAC,QAAQ,EAC3B,OAAO,EAAE,eAAe,EACxB,UAAU,EAAE,MAAM,EAClB,WAAW,EAAE,OAAO,EACpB,YAAY,EAAE,OAAO,EACrB,cAAc,EAAE,KAAK,EACrB,SAAS,EAAE,OAAO,EAClB,WAAW,EAAE,KAAK,GACjB,IAAI,CAgEN;CACF"}
|
||||
{"version":3,"file":"TileMapPixiHelper.d.ts","sourceRoot":"","sources":["../../src/render/TileMapPixiHelper.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,KAAK,EAAE,MAAM,sBAAsB,CAAC;AACtD,OAAO,EAEL,eAAe,EAEhB,MAAM,uBAAuB,CAAC;AAG/B,OAAO,EAAE,kBAAkB,EAAE,MAAM,4BAA4B,CAAC;AAChE,OAAO,EAAE,gBAAgB,EAAE,MAAM,oBAAoB,CAAC;AAGtD,yBAAiB,iBAAiB,CAAC;IACjC;;;;;;;;OAQG;IACH,SAAgB,UAAU,CACxB,OAAO,EAAE,kBAAkB,EAC3B,UAAU,EAAE,MAAM,EAClB,YAAY,EAAE,IAAI,CAAC,OAAO,GAAG,IAAI,EACjC,UAAU,EAAE,CAAC,WAAW,EAAE,MAAM,KAAK,IAAI,CAAC,OAAO,GAChD,gBAAgB,GAAG,IAAI,CAuBzB;IAED;;;;;;;;;;;;OAYG;IACH,SAAgB,iBAAiB,CAC/B,kBAAkB,EAAE,GAAG,EACvB,OAAO,EAAE,eAAe,EACxB,YAAY,EAAE,gBAAgB,EAC9B,WAAW,EAAE,OAAO,GAAG,SAAS,GAAG,KAAK,EACxC,UAAU,EAAE,MAAM,GACjB,IAAI,CA0GN;IAED;;OAEG;IACH,SAAgB,uBAAuB,CACrC,YAAY,EAAE,IAAI,CAAC,QAAQ,EAC3B,OAAO,EAAE,eAAe,EACxB,UAAU,EAAE,MAAM,EAClB,WAAW,EAAE,OAAO,EACpB,YAAY,EAAE,OAAO,EACrB,cAAc,EAAE,KAAK,EACrB,SAAS,EAAE,OAAO,EAClB,WAAW,EAAE,KAAK,GACjB,IAAI,CAgEN;CACF"}
|
@@ -1,7 +1,5 @@
|
||||
import { TileTextureCache } from '../TileTextureCache';
|
||||
import { LDtkTileMap } from '../../load/ldtk/LDtkFormat';
|
||||
type Texture = PIXI.BaseTexture<PIXI.Resource>;
|
||||
type TextureLoader = (textureName: string) => PIXI.BaseTexture<PIXI.Resource>;
|
||||
export declare namespace LDtkPixiHelper {
|
||||
/**
|
||||
* Split an atlas image into Pixi textures.
|
||||
@@ -15,9 +13,8 @@ export declare namespace LDtkPixiHelper {
|
||||
function parseAtlas(
|
||||
tileMap: LDtkTileMap,
|
||||
levelIndex: number,
|
||||
atlasTexture: Texture | null,
|
||||
getTexture: TextureLoader
|
||||
atlasTexture: PIXI.Texture | null,
|
||||
getTexture: (textureName: string) => PIXI.Texture
|
||||
): TileTextureCache | null;
|
||||
}
|
||||
export {};
|
||||
//# sourceMappingURL=LDtkPixiHelper.d.ts.map
|
||||
|
@@ -1 +1 @@
|
||||
{"version":3,"file":"LDtkPixiHelper.d.ts","sourceRoot":"","sources":["../../../src/render/ldtk/LDtkPixiHelper.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,gBAAgB,EAAE,MAAM,qBAAqB,CAAC;AACvD,OAAO,EAAE,WAAW,EAAkB,MAAM,4BAA4B,CAAC;AAGzE,KAAK,OAAO,GAAG,IAAI,CAAC,WAAW,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC;AAC/C,KAAK,aAAa,GAAG,CAAC,WAAW,EAAE,MAAM,KAAK,IAAI,CAAC,WAAW,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC;AAmC9E,yBAAiB,cAAc,CAAC;IAC9B;;;;;;;;OAQG;IACH,SAAgB,UAAU,CACxB,OAAO,EAAE,WAAW,EACpB,UAAU,EAAE,MAAM,EAClB,YAAY,EAAE,OAAO,GAAG,IAAI,EAC5B,UAAU,EAAE,aAAa,GACxB,gBAAgB,GAAG,IAAI,CAoFzB;CACF"}
|
||||
{"version":3,"file":"LDtkPixiHelper.d.ts","sourceRoot":"","sources":["../../../src/render/ldtk/LDtkPixiHelper.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,gBAAgB,EAAE,MAAM,qBAAqB,CAAC;AACvD,OAAO,EAAE,WAAW,EAAkB,MAAM,4BAA4B,CAAC;AAoCzE,yBAAiB,cAAc,CAAC;IAC9B;;;;;;;;OAQG;IACH,SAAgB,UAAU,CACxB,OAAO,EAAE,WAAW,EACpB,UAAU,EAAE,MAAM,EAClB,YAAY,EAAE,IAAI,CAAC,OAAO,GAAG,IAAI,EACjC,UAAU,EAAE,CAAC,WAAW,EAAE,MAAM,KAAK,IAAI,CAAC,OAAO,GAChD,gBAAgB,GAAG,IAAI,CAoFzB;CACF"}
|
@@ -13,8 +13,8 @@ export declare namespace TiledPixiHelper {
|
||||
function parseAtlas(
|
||||
tileMap: TiledTileMap,
|
||||
levelIndex: number,
|
||||
atlasTexture: PIXI.BaseTexture<PIXI.Resource> | null,
|
||||
getTexture: (textureName: string) => PIXI.BaseTexture<PIXI.Resource>
|
||||
atlasTexture: PIXI.Texture | null,
|
||||
getTexture: (textureName: string) => PIXI.Texture
|
||||
): TileTextureCache | null;
|
||||
}
|
||||
//# sourceMappingURL=TiledPixiHelper.d.ts.map
|
||||
|
@@ -1 +1 @@
|
||||
{"version":3,"file":"TiledPixiHelper.d.ts","sourceRoot":"","sources":["../../../src/render/tiled/TiledPixiHelper.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,gBAAgB,EAAE,MAAM,qBAAqB,CAAC;AACvD,OAAO,EAAE,YAAY,EAAE,MAAM,8BAA8B,CAAC;AAG5D,yBAAiB,eAAe,CAAC;IAC/B;;;;;;;;OAQG;IACH,SAAgB,UAAU,CACxB,OAAO,EAAE,YAAY,EACrB,UAAU,EAAE,MAAM,EAClB,YAAY,EAAE,IAAI,CAAC,WAAW,CAAC,IAAI,CAAC,QAAQ,CAAC,GAAG,IAAI,EACpD,UAAU,EAAE,CAAC,WAAW,EAAE,MAAM,KAAK,IAAI,CAAC,WAAW,CAAC,IAAI,CAAC,QAAQ,CAAC,GACnE,gBAAgB,GAAG,IAAI,CA8FzB;CACF"}
|
||||
{"version":3,"file":"TiledPixiHelper.d.ts","sourceRoot":"","sources":["../../../src/render/tiled/TiledPixiHelper.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,gBAAgB,EAAE,MAAM,qBAAqB,CAAC;AACvD,OAAO,EAAE,YAAY,EAAE,MAAM,8BAA8B,CAAC;AAG5D,yBAAiB,eAAe,CAAC;IAC/B;;;;;;;;OAQG;IACH,SAAgB,UAAU,CACxB,OAAO,EAAE,YAAY,EACrB,UAAU,EAAE,MAAM,EAClB,YAAY,EAAE,IAAI,CAAC,OAAO,GAAG,IAAI,EACjC,UAAU,EAAE,CAAC,WAAW,EAAE,MAAM,KAAK,IAAI,CAAC,OAAO,GAChD,gBAAgB,GAAG,IAAI,CA8FzB;CACF"}
|
@@ -136,11 +136,7 @@ namespace gdjs {
|
||||
this._tilemapJsonFile,
|
||||
textureName
|
||||
);
|
||||
return (game
|
||||
.getImageManager()
|
||||
.getPIXITexture(mappedName) as unknown) as PIXI.BaseTexture<
|
||||
PIXI.Resource
|
||||
>;
|
||||
return game.getImageManager().getPIXITexture(mappedName);
|
||||
},
|
||||
this._tilemapAtlasImage,
|
||||
this._tilemapJsonFile,
|
||||
|
@@ -93,11 +93,11 @@ namespace gdjs {
|
||||
/**
|
||||
* A task that resolves with a promise.
|
||||
*/
|
||||
export class PromiseTask extends AsyncTask {
|
||||
export class PromiseTask<ResultType = void> extends AsyncTask {
|
||||
private isResolved: boolean = false;
|
||||
promise: Promise<void>;
|
||||
promise: Promise<ResultType>;
|
||||
|
||||
constructor(promise: Promise<void>) {
|
||||
constructor(promise: Promise<ResultType>) {
|
||||
super();
|
||||
this.promise = promise
|
||||
.catch((error) => {
|
||||
@@ -107,9 +107,14 @@ If you are using JavaScript promises in an asynchronous action, make sure to add
|
||||
Otherwise, report this as a bug on the GDevelop forums!
|
||||
${error ? 'The following error was thrown: ' + error : ''}`
|
||||
);
|
||||
|
||||
// @ts-ignore
|
||||
return undefined as ResultType;
|
||||
})
|
||||
.then(() => {
|
||||
.then((result) => {
|
||||
this.isResolved = true;
|
||||
|
||||
return result;
|
||||
});
|
||||
}
|
||||
|
||||
|
@@ -12,6 +12,10 @@
|
||||
<allow-intent href="sms:*" />
|
||||
<allow-intent href="mailto:*" />
|
||||
<allow-intent href="geo:*" />
|
||||
|
||||
<!-- Allow iframes on iOS like leaderboards, including those in development that are not served via https. -->
|
||||
<allow-navigation href="*" />
|
||||
|
||||
<platform name="android">
|
||||
<allow-intent href="market:*" />
|
||||
|
||||
|
@@ -8,7 +8,7 @@
|
||||
/* Prevent copy paste for all elements except text fields */
|
||||
* { -webkit-user-select:none; -webkit-tap-highlight-color:rgba(255, 255, 255, 0); }
|
||||
input, textarea { -webkit-user-select:text; }
|
||||
body { background-color:black; color:white }
|
||||
body { background-color:black; }
|
||||
/* GDJS_CUSTOM_STYLE */
|
||||
</style>
|
||||
<script src='cordova.js'></script>
|
||||
|
@@ -2,9 +2,9 @@ namespace gdjs {
|
||||
/** Represents a point in a coordinate system. */
|
||||
export type SpritePoint = {
|
||||
/** X position of the point. */
|
||||
x: number;
|
||||
x: float;
|
||||
/** Y position of the point. */
|
||||
y: number;
|
||||
y: float;
|
||||
};
|
||||
|
||||
/** Represents a custom point in a frame. */
|
||||
@@ -12,9 +12,9 @@ namespace gdjs {
|
||||
/** Name of the point. */
|
||||
name: string;
|
||||
/** X position of the point. */
|
||||
x: number;
|
||||
x: float;
|
||||
/** Y position of the point. */
|
||||
y: number;
|
||||
y: float;
|
||||
};
|
||||
|
||||
/** Represents the center point in a frame. */
|
||||
@@ -24,9 +24,9 @@ namespace gdjs {
|
||||
/** Is the center automatically computed? */
|
||||
automatic: boolean;
|
||||
/** X position of the point. */
|
||||
x: number;
|
||||
x: float;
|
||||
/** Y position of the point. */
|
||||
y: number;
|
||||
y: float;
|
||||
};
|
||||
|
||||
/** Represents a {@link gdjs.SpriteAnimationFrame}. */
|
||||
@@ -48,7 +48,7 @@ namespace gdjs {
|
||||
/** Represents the data of a {@link gdjs.SpriteAnimationDirection}. */
|
||||
export type SpriteDirectionData = {
|
||||
/** Time between each frame, in seconds. */
|
||||
timeBetweenFrames: number;
|
||||
timeBetweenFrames: float;
|
||||
/** Is the animation looping? */
|
||||
looping: boolean;
|
||||
/** The list of frames. */
|
||||
@@ -186,7 +186,7 @@ namespace gdjs {
|
||||
* Represents a direction of an animation of a {@link gdjs.SpriteRuntimeObject}.
|
||||
*/
|
||||
export class SpriteAnimationDirection<T> {
|
||||
timeBetweenFrames: number;
|
||||
timeBetweenFrames: float;
|
||||
loop: boolean;
|
||||
frames: SpriteAnimationFrame<T>[] = [];
|
||||
|
||||
@@ -347,6 +347,7 @@ namespace gdjs {
|
||||
|
||||
// Make sure to delete already existing animations which are not used anymore.
|
||||
this._animationFrame = null;
|
||||
this.invalidateFrame();
|
||||
}
|
||||
|
||||
updateFromObjectData(
|
||||
@@ -449,12 +450,13 @@ namespace gdjs {
|
||||
this._onFrameChange = callback;
|
||||
}
|
||||
|
||||
getAnimationIndex(): number {
|
||||
getAnimationIndex(): integer {
|
||||
return this._currentAnimation;
|
||||
}
|
||||
|
||||
setAnimationIndex(newAnimation: number): boolean {
|
||||
newAnimation = newAnimation || 0;
|
||||
setAnimationIndex(newAnimation: integer): boolean {
|
||||
// Truncate the index.
|
||||
newAnimation = newAnimation | 0;
|
||||
if (
|
||||
newAnimation < this._animations.length &&
|
||||
this._currentAnimation !== newAnimation &&
|
||||
@@ -476,16 +478,17 @@ namespace gdjs {
|
||||
return this._animations[this._currentAnimation].name;
|
||||
}
|
||||
|
||||
setAnimationName(newAnimationName: string): void {
|
||||
setAnimationName(newAnimationName: string): boolean {
|
||||
if (!newAnimationName) {
|
||||
return;
|
||||
return false;
|
||||
}
|
||||
for (let i = 0; i < this._animations.length; ++i) {
|
||||
if (this._animations[i].name === newAnimationName) {
|
||||
this.setAnimationIndex(i);
|
||||
return;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
hasAnimationEnded(): boolean {
|
||||
@@ -533,7 +536,7 @@ namespace gdjs {
|
||||
* Change the current frame displayed by the animation
|
||||
* @param newFrameIndex The index of the frame to be displayed
|
||||
*/
|
||||
setAnimationFrameIndex(newFrameIndex: number): boolean {
|
||||
setAnimationFrameIndex(newFrameIndex: integer): boolean {
|
||||
if (
|
||||
this._currentAnimation >= this._animations.length ||
|
||||
this._currentDirection >=
|
||||
@@ -562,7 +565,7 @@ namespace gdjs {
|
||||
* Get the index of the current frame displayed by the animation
|
||||
* @return newFrame The index of the frame being displayed
|
||||
*/
|
||||
getAnimationFrameIndex(): number {
|
||||
getAnimationFrameIndex(): integer {
|
||||
return this._currentFrameIndex;
|
||||
}
|
||||
|
||||
@@ -599,7 +602,7 @@ namespace gdjs {
|
||||
return direction.frames.length * direction.timeBetweenFrames;
|
||||
}
|
||||
|
||||
getAnimationFrameCount(): number {
|
||||
getAnimationFrameCount(): integer {
|
||||
if (this._currentAnimation >= this._animations.length) {
|
||||
return 0;
|
||||
}
|
||||
@@ -615,7 +618,7 @@ namespace gdjs {
|
||||
* @param The new angle (or direction index) to be applied
|
||||
* @deprecated
|
||||
*/
|
||||
setDirectionOrAngle(oldValue: float, newValue: float): number | null {
|
||||
setDirectionOrAngle(oldValue: float, newValue: float): float | null {
|
||||
if (this._currentAnimation >= this._animations.length) {
|
||||
return null;
|
||||
}
|
||||
|
@@ -26,9 +26,9 @@ namespace gdjs {
|
||||
gdjs.Animatable,
|
||||
gdjs.OpacityHandler {
|
||||
_animator: gdjs.SpriteAnimator<any>;
|
||||
_scaleX: number = 1;
|
||||
_scaleY: number = 1;
|
||||
_blendMode: number = 0;
|
||||
_scaleX: float = 1;
|
||||
_scaleY: float = 1;
|
||||
_blendMode: integer = 0;
|
||||
_flippedX: boolean = false;
|
||||
_flippedY: boolean = false;
|
||||
opacity: float = 255;
|
||||
@@ -133,6 +133,8 @@ namespace gdjs {
|
||||
const hasFrameChanged = this._animator.step(this.getElapsedTime() / 1000);
|
||||
if (hasFrameChanged) {
|
||||
this._updateAnimationFrame();
|
||||
// TODO: Hitboxes may not need an update if every frames has the same ones.
|
||||
this.invalidateHitboxes();
|
||||
}
|
||||
this._renderer.ensureUpToDate();
|
||||
}
|
||||
@@ -220,11 +222,11 @@ namespace gdjs {
|
||||
* @param newAnimation The index of the new animation to be played
|
||||
* @deprecated Use `setAnimationIndex` instead
|
||||
*/
|
||||
setAnimation(newAnimation: number): void {
|
||||
setAnimation(newAnimation: integer): void {
|
||||
this.setAnimationIndex(newAnimation);
|
||||
}
|
||||
|
||||
setAnimationIndex(newAnimation: number): void {
|
||||
setAnimationIndex(newAnimation: integer): void {
|
||||
const hasAnimationChanged = this._animator.setAnimationIndex(
|
||||
newAnimation
|
||||
);
|
||||
@@ -237,7 +239,15 @@ namespace gdjs {
|
||||
}
|
||||
|
||||
setAnimationName(newAnimationName: string): void {
|
||||
this._animator.setAnimationName(newAnimationName);
|
||||
const hasAnimationChanged = this._animator.setAnimationName(
|
||||
newAnimationName
|
||||
);
|
||||
if (hasAnimationChanged) {
|
||||
//TODO: This may be unnecessary.
|
||||
this._renderer.update();
|
||||
this._animationFrameDirty = true;
|
||||
this.invalidateHitboxes();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -245,11 +255,11 @@ namespace gdjs {
|
||||
* @return The index of the new animation being played
|
||||
* @deprecated Use `getAnimationIndex` instead
|
||||
*/
|
||||
getAnimation(): number {
|
||||
getAnimation(): integer {
|
||||
return this.getAnimationIndex();
|
||||
}
|
||||
|
||||
getAnimationIndex(): number {
|
||||
getAnimationIndex(): integer {
|
||||
return this._animator.getAnimationIndex();
|
||||
}
|
||||
|
||||
@@ -300,7 +310,7 @@ namespace gdjs {
|
||||
* Get the index of the current frame displayed by the animation
|
||||
* @return newFrame The index of the frame being displayed
|
||||
*/
|
||||
getAnimationFrame(): number {
|
||||
getAnimationFrame(): integer {
|
||||
return this._animator.getAnimationFrameIndex();
|
||||
}
|
||||
|
||||
@@ -316,11 +326,11 @@ namespace gdjs {
|
||||
}
|
||||
}
|
||||
|
||||
getAnimationDuration(): number {
|
||||
getAnimationDuration(): float {
|
||||
return this._animator.getAnimationDuration();
|
||||
}
|
||||
|
||||
getAnimationFrameCount(): number {
|
||||
getAnimationFrameCount(): integer {
|
||||
return this._animator.getAnimationFrameCount();
|
||||
}
|
||||
|
||||
@@ -446,7 +456,7 @@ namespace gdjs {
|
||||
* @param result Array that will be updated with the result
|
||||
* (x and y position of the point in global coordinates).
|
||||
*/
|
||||
private _transformToGlobal(x: float, y: float, result: number[]) {
|
||||
private _transformToGlobal(x: float, y: float, result: float[]) {
|
||||
const animationFrame = this._animator.getCurrentFrame() as SpriteAnimationFrame<
|
||||
any
|
||||
>;
|
||||
@@ -648,7 +658,7 @@ namespace gdjs {
|
||||
this._renderer.updateOpacity();
|
||||
}
|
||||
|
||||
getOpacity(): number {
|
||||
getOpacity(): float {
|
||||
return this.opacity;
|
||||
}
|
||||
|
||||
|
@@ -1175,6 +1175,8 @@ interface InitialInstance {
|
||||
void SetLocked(boolean lock);
|
||||
boolean IsSealed();
|
||||
void SetSealed(boolean seal);
|
||||
boolean ShouldKeepRatio();
|
||||
void SetShouldKeepRatio(boolean keepRatio);
|
||||
long GetZOrder();
|
||||
void SetZOrder(long zOrder);
|
||||
[Const, Ref] DOMString GetLayer();
|
||||
@@ -1855,7 +1857,7 @@ interface BehaviorMetadata {
|
||||
|
||||
[Ref] Behavior Get();
|
||||
BehaviorsSharedData GetSharedDataInstance();
|
||||
|
||||
|
||||
[Value] MapStringPropertyDescriptor GetProperties();
|
||||
[Value] MapStringPropertyDescriptor GetSharedProperties();
|
||||
};
|
||||
|
2
GDevelop.js/types.d.ts
vendored
2
GDevelop.js/types.d.ts
vendored
@@ -1025,6 +1025,8 @@ export class InitialInstance extends EmscriptenObject {
|
||||
setLocked(lock: boolean): void;
|
||||
isSealed(): boolean;
|
||||
setSealed(seal: boolean): void;
|
||||
shouldKeepRatio(): boolean;
|
||||
setShouldKeepRatio(keepRatio: boolean): void;
|
||||
getZOrder(): number;
|
||||
setZOrder(zOrder: number): void;
|
||||
getLayer(): string;
|
||||
|
@@ -19,6 +19,8 @@ declare class gdInitialInstance {
|
||||
setLocked(lock: boolean): void;
|
||||
isSealed(): boolean;
|
||||
setSealed(seal: boolean): void;
|
||||
shouldKeepRatio(): boolean;
|
||||
setShouldKeepRatio(keepRatio: boolean): void;
|
||||
getZOrder(): number;
|
||||
setZOrder(zOrder: number): void;
|
||||
getLayer(): string;
|
||||
|
@@ -132,7 +132,7 @@ export class TileMapManager {
|
||||
tileSetJsonResourceName: string,
|
||||
callback: (tileMapFileContent: TileMapFileContent | null) => void
|
||||
) => void,
|
||||
getTexture: (textureName: string) => PIXI.BaseTexture<PIXI.Resource>,
|
||||
getTexture: (textureName: string) => PIXI.Texture,
|
||||
atlasImageResourceName: string,
|
||||
tileMapJsonResourceName: string,
|
||||
tileSetJsonResourceName: string,
|
||||
|
@@ -23,8 +23,8 @@ export namespace PixiTileMapHelper {
|
||||
export function parseAtlas(
|
||||
tileMap: TileMapFileContent,
|
||||
levelIndex: number,
|
||||
atlasTexture: PIXI.BaseTexture<PIXI.Resource> | null,
|
||||
getTexture: (textureName: string) => PIXI.BaseTexture<PIXI.Resource>
|
||||
atlasTexture: PIXI.Texture | null,
|
||||
getTexture: (textureName: string) => PIXI.Texture
|
||||
): TileTextureCache | null {
|
||||
if (tileMap.kind === "ldtk") {
|
||||
return LDtkPixiHelper.parseAtlas(
|
||||
|
@@ -2,20 +2,17 @@ import { TileTextureCache } from "../TileTextureCache";
|
||||
import { LDtkTileMap, LDtkTilesetDef } from "../../load/ldtk/LDtkFormat";
|
||||
import { getLDtkTileId } from "../../load/ldtk/LDtkTileMapLoaderHelper";
|
||||
|
||||
type Texture = PIXI.BaseTexture<PIXI.Resource>;
|
||||
type TextureLoader = (textureName: string) => PIXI.BaseTexture<PIXI.Resource>;
|
||||
|
||||
function getAtlasTexture(
|
||||
atlasTextures: Record<number, Texture | null>,
|
||||
atlasTextures: Record<number, PIXI.Texture | null>,
|
||||
tilesetCache: Record<number, LDtkTilesetDef>,
|
||||
getTexture: TextureLoader,
|
||||
getTexture: (textureName: string) => PIXI.Texture,
|
||||
tilesetId: number
|
||||
): Texture | null {
|
||||
): PIXI.Texture | null {
|
||||
if (atlasTextures[tilesetId]) {
|
||||
return atlasTextures[tilesetId];
|
||||
}
|
||||
|
||||
let texture: Texture | null = null;
|
||||
let texture: PIXI.Texture | null = null;
|
||||
|
||||
const tileset = tilesetCache[tilesetId];
|
||||
if (tileset?.relPath) {
|
||||
@@ -51,8 +48,8 @@ export namespace LDtkPixiHelper {
|
||||
export function parseAtlas(
|
||||
tileMap: LDtkTileMap,
|
||||
levelIndex: number,
|
||||
atlasTexture: Texture | null,
|
||||
getTexture: TextureLoader
|
||||
atlasTexture: PIXI.Texture | null,
|
||||
getTexture: (textureName: string) => PIXI.Texture
|
||||
): TileTextureCache | null {
|
||||
const level = tileMap.levels[levelIndex > -1 ? levelIndex : 0];
|
||||
if (!level || !level.layerInstances) {
|
||||
@@ -68,7 +65,7 @@ export namespace LDtkPixiHelper {
|
||||
// List the tiles that have been loaded to Pixi by all the layers of the level.
|
||||
// The keys are a composition (getLDtkTileId) between the tileset's id and the tile's id.
|
||||
const levelTileCache: Record<number, boolean> = {};
|
||||
const atlasTextures: Record<number, Texture | null> = {};
|
||||
const atlasTextures: Record<number, PIXI.Texture | null> = {};
|
||||
|
||||
for (let iLayer = level.layerInstances.length - 1; iLayer >= 0; --iLayer) {
|
||||
const layer = level.layerInstances[iLayer];
|
||||
@@ -113,7 +110,7 @@ export namespace LDtkPixiHelper {
|
||||
const [x, y] = tile.src;
|
||||
const rect = new PIXI.Rectangle(x, y, gridSize, gridSize);
|
||||
|
||||
const texture = new PIXI.Texture(atlasTexture, rect);
|
||||
const texture = new PIXI.Texture(atlasTexture.baseTexture, rect);
|
||||
|
||||
textureCache.setTexture(tileId, texture);
|
||||
} catch (error) {
|
||||
@@ -131,7 +128,7 @@ export namespace LDtkPixiHelper {
|
||||
if (level.bgRelPath) {
|
||||
const atlasTexture = getTexture(level.bgRelPath);
|
||||
const rect = new PIXI.Rectangle(0, 0, level.pxWid, level.pxHei);
|
||||
const texture = new PIXI.Texture(atlasTexture!, rect);
|
||||
const texture = new PIXI.Texture(atlasTexture!.baseTexture, rect);
|
||||
|
||||
textureCache.setLevelBackgroundTexture(level.bgRelPath, texture);
|
||||
}
|
||||
|
@@ -15,8 +15,8 @@ export namespace TiledPixiHelper {
|
||||
export function parseAtlas(
|
||||
tileMap: TiledTileMap,
|
||||
levelIndex: number,
|
||||
atlasTexture: PIXI.BaseTexture<PIXI.Resource> | null,
|
||||
getTexture: (textureName: string) => PIXI.BaseTexture<PIXI.Resource>
|
||||
atlasTexture: PIXI.Texture | null,
|
||||
getTexture: (textureName: string) => PIXI.Texture
|
||||
): TileTextureCache | null {
|
||||
if (!tileMap.tiledversion) {
|
||||
console.warn(
|
||||
@@ -99,7 +99,7 @@ export namespace TiledPixiHelper {
|
||||
|
||||
try {
|
||||
const rect = new PIXI.Rectangle(x, y, tilewidth, tileheight);
|
||||
const texture = new PIXI.Texture(atlasTexture!, rect);
|
||||
const texture = new PIXI.Texture(atlasTexture!.baseTexture, rect);
|
||||
|
||||
textureCache.setTexture(tileId, texture);
|
||||
} catch (error) {
|
||||
|
161
newIDE/app/public/res/education-resources.svg
Normal file
161
newIDE/app/public/res/education-resources.svg
Normal file
@@ -0,0 +1,161 @@
|
||||
<svg width="164" height="77" viewBox="0 0 164 77" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g id="Education" clip-path="url(#clip0_610_2659)">
|
||||
<g id="Group 1114">
|
||||
<g id="Group 1116">
|
||||
<path id="Vector 233" d="M77.1067 64.4445C77.3739 66.2769 77.7481 70.7894 77.1067 74.1795H80.2563" stroke="#CF0A80" stroke-width="1.07481" stroke-linecap="round"/>
|
||||
<path id="Vector 234" d="M81.8596 63.7637C82.6422 65.5199 84.2647 69.662 84.4938 72.1817H88.2733" stroke="#CF0A80" stroke-width="1.07481" stroke-linecap="round"/>
|
||||
<path id="Vector 236" d="M84.0922 57.6966C85.9819 57.0476 89.8874 54.5471 90.3913 49.7368" stroke="#CF0A80" stroke-width="1.07481" stroke-linecap="round"/>
|
||||
<path id="Vector 240" d="M76.9343 49.2111C82.3745 48.0658 84.6651 55.7966 84.6651 58.0872C85.4095 58.0872 86.8411 59.0607 87.0702 60.1487C87.2992 61.2368 86.6693 62.7528 85.2377 63.3556C84.5764 63.634 81.9736 64.5009 79.8548 64.959C77.736 65.4171 75.0446 65.3598 73.5557 65.3598C72.0668 65.3598 70.5207 64.3863 70.4634 62.6684C70.4176 61.294 71.666 60.3396 72.2959 60.0342C71.2651 55.5675 71.4942 50.3564 76.9343 49.2111Z" fill="url(#paint0_linear_610_2659)"/>
|
||||
<ellipse id="Ellipse 79" cx="82.3517" cy="57.189" rx="0.744443" ry="0.801708" transform="rotate(-17.4862 82.3517 57.189)" fill="#3B314F"/>
|
||||
<ellipse id="Ellipse 80" cx="78.3428" cy="58.2274" rx="0.744443" ry="0.801708" transform="rotate(-17.4862 78.3428 58.2274)" fill="#3B314F"/>
|
||||
<path id="Vector 245" d="M73.0969 59.5256C73.8032 62.2743 74.792 68.4131 73.0969 70.9785" stroke="#CF0A80" stroke-width="1.07481" stroke-linecap="round"/>
|
||||
</g>
|
||||
<g id="Vector 237" opacity="0.2" filter="url(#filter0_f_610_2659)">
|
||||
<path d="M96.97 70.1445L79.0929 56.8318C78.1672 56.1425 77.6218 55.0563 77.6218 53.9022L77.6216 23.401C77.6216 22.2715 78.1441 21.2056 79.0368 20.5137L97.0232 6.57465C98.3196 5.56995 100.127 5.55255 101.442 6.5321L119.137 19.7077C120.063 20.397 120.608 21.4833 120.608 22.6375V53.1589C120.608 54.2768 120.096 55.3332 119.219 56.0259L101.415 70.0818C100.118 71.1058 98.2953 71.1315 96.97 70.1445Z" fill="#F9E05C"/>
|
||||
</g>
|
||||
<g id="Group 1110">
|
||||
<path id="Polygon 3" d="M99.8176 70.1777L75.5547 39.0942L99.7076 30.694L99.8176 70.1777Z" fill="#FFDAA4"/>
|
||||
<path id="Polygon 5" d="M99.8173 6.08455L124.08 39.091L99.7073 30.6902L99.8173 6.08455Z" fill="#FFA827"/>
|
||||
<path id="Polygon 4" d="M99.8173 70.1722L99.7072 30.6889L124.08 39.0892L99.8173 70.1722Z" fill="#FFBC57"/>
|
||||
<path id="Polygon 6" d="M99.8177 6.08644L99.7076 30.6916L75.5549 39.0925L99.8177 6.08644Z" fill="#FFBC57"/>
|
||||
</g>
|
||||
<g id="Group 1111">
|
||||
<path id="Vector" d="M86.4539 42.8609C86.5754 42.8337 86.6501 42.7052 86.6207 42.5739C86.5913 42.4426 86.4689 42.3583 86.3473 42.3855C86.2258 42.4128 86.151 42.5413 86.1805 42.6726C86.2099 42.8038 86.3323 42.8882 86.4539 42.8609Z" fill="#FEFAD1"/>
|
||||
<path id="Vector_2" d="M85.1342 37.8792C85.3625 37.828 85.5028 37.5867 85.4476 37.3402C85.3923 37.0936 85.1625 36.9352 84.9342 36.9864C84.7058 37.0376 84.5655 37.2789 84.6208 37.5254C84.676 37.772 84.9059 37.9303 85.1342 37.8792Z" fill="#FEFAD1"/>
|
||||
<path id="Vector_3" d="M86.4192 39.5035C86.5882 39.4656 86.692 39.287 86.6512 39.1045C86.6103 38.922 86.4401 38.8048 86.2711 38.8426C86.1021 38.8805 85.9982 39.0591 86.0391 39.2416C86.08 39.4241 86.2502 39.5414 86.4192 39.5035Z" fill="#FEFAD1"/>
|
||||
<path id="Vector_4" d="M82.5922 37.6347C82.6798 37.6151 82.7336 37.5224 82.7124 37.4278C82.6912 37.3332 82.603 37.2724 82.5154 37.292C82.4277 37.3117 82.3739 37.4043 82.3951 37.4989C82.4163 37.5935 82.5045 37.6543 82.5922 37.6347Z" fill="#FEFAD1"/>
|
||||
<path id="Vector_5" d="M82.3491 34.0652C82.4219 34.0489 82.4666 33.972 82.449 33.8934C82.4314 33.8148 82.3581 33.7643 82.2853 33.7806C82.2126 33.7969 82.1678 33.8738 82.1855 33.9524C82.2031 34.031 82.2763 34.0815 82.3491 34.0652Z" fill="#FEFAD1"/>
|
||||
<path id="Vector_6" d="M90.3807 33.7707C90.6303 33.7148 90.7837 33.451 90.7233 33.1815C90.6629 32.912 90.4116 32.7389 90.1621 32.7948C89.9125 32.8507 89.7592 33.1145 89.8195 33.384C89.8799 33.6535 90.1312 33.8266 90.3807 33.7707Z" fill="#FEFAD1"/>
|
||||
<path id="Vector_7" d="M89.751 35.5209C89.8716 35.4939 89.9457 35.3664 89.9165 35.2362C89.8873 35.106 89.7659 35.0224 89.6453 35.0494C89.5248 35.0764 89.4507 35.2039 89.4798 35.3341C89.509 35.4643 89.6304 35.5479 89.751 35.5209Z" fill="#FEFAD1"/>
|
||||
<path id="Vector_8" d="M90.671 32.2404C90.7542 32.2218 90.8053 32.1339 90.7852 32.044C90.765 31.9542 90.6813 31.8965 90.5981 31.9152C90.5149 31.9338 90.4638 32.0217 90.484 32.1115C90.5041 32.2014 90.5878 32.2591 90.671 32.2404Z" fill="#FEFAD1"/>
|
||||
<path id="Vector_9" d="M91.1259 41.471C91.458 41.3966 91.6621 41.0456 91.5818 40.687C91.5014 40.3284 91.1671 40.098 90.835 40.1724C90.5029 40.2469 90.2988 40.5979 90.3791 40.9565C90.4595 41.3151 90.7938 41.5455 91.1259 41.471Z" fill="#FEFAD1"/>
|
||||
<path id="Vector_10" d="M95.8254 54.7098C96.0315 54.6636 96.1582 54.4458 96.1083 54.2233C96.0584 54.0008 95.8509 53.8578 95.6449 53.904C95.4388 53.9502 95.3122 54.168 95.362 54.3905C95.4119 54.613 95.6194 54.756 95.8254 54.7098Z" fill="#FEFAD1"/>
|
||||
<path id="Vector_11" d="M96.2603 50.7987C96.3509 50.7784 96.4066 50.6826 96.3846 50.5848C96.3627 50.4869 96.2715 50.4241 96.1809 50.4444C96.0903 50.4647 96.0346 50.5605 96.0565 50.6583C96.0785 50.7561 96.1697 50.819 96.2603 50.7987Z" fill="#FEFAD1"/>
|
||||
<path id="Vector_12" d="M90.9321 51.5703C91.1619 51.5188 91.3031 51.2759 91.2475 51.0278C91.1919 50.7796 90.9606 50.6202 90.7308 50.6717C90.501 50.7232 90.3598 50.9661 90.4154 51.2142C90.471 51.4624 90.7023 51.6218 90.9321 51.5703Z" fill="#FEFAD1"/>
|
||||
<path id="Vector_13" d="M95.0181 58.1985C95.2741 58.1411 95.4315 57.8705 95.3695 57.5941C95.3076 57.3177 95.0498 57.1401 94.7938 57.1975C94.5378 57.2548 94.3805 57.5254 94.4425 57.8018C94.5044 58.0782 94.7621 58.2558 95.0181 58.1985Z" fill="#FEFAD1"/>
|
||||
<path id="Vector_14" d="M89.5071 50.0869C89.5987 50.0664 89.655 49.9696 89.6328 49.8707C89.6107 49.7718 89.5184 49.7082 89.4268 49.7288C89.3352 49.7493 89.279 49.8461 89.3011 49.945C89.3233 50.0439 89.4155 50.1075 89.5071 50.0869Z" fill="#FEFAD1"/>
|
||||
<path id="Vector_15" d="M99.3288 13.6004C99.6565 13.5269 99.8578 13.1806 99.7785 12.8268C99.6993 12.473 99.3694 12.2457 99.0417 12.3192C98.7141 12.3926 98.5128 12.7389 98.592 13.0927C98.6713 13.4465 99.0012 13.6738 99.3288 13.6004Z" fill="#FEFAD1"/>
|
||||
<path id="Vector_16" d="M110.714 23.4216C110.898 23.3802 111.012 23.1848 110.967 22.9852C110.923 22.7857 110.737 22.6575 110.552 22.6989C110.367 22.7403 110.253 22.9357 110.298 23.1352C110.343 23.3348 110.529 23.463 110.714 23.4216Z" fill="#FEFAD1"/>
|
||||
<path id="Vector_17" d="M109.761 19.9759C109.857 19.9546 109.915 19.8537 109.892 19.7507C109.869 19.6478 109.773 19.5816 109.678 19.603C109.582 19.6243 109.524 19.7252 109.547 19.8281C109.57 19.9311 109.666 19.9973 109.761 19.9759Z" fill="#FEFAD1"/>
|
||||
<path id="Vector_18" d="M106.424 16.6399C106.536 16.6149 106.605 16.4968 106.578 16.3762C106.551 16.2556 106.438 16.1781 106.326 16.2032C106.215 16.2282 106.146 16.3462 106.173 16.4668C106.2 16.5874 106.313 16.6649 106.424 16.6399Z" fill="#FEFAD1"/>
|
||||
<path id="Vector_19" d="M107.754 22.3923C107.886 22.3626 107.967 22.2227 107.935 22.0796C107.903 21.9366 107.77 21.8448 107.637 21.8744C107.505 21.9041 107.424 22.0441 107.456 22.1871C107.488 22.3301 107.621 22.422 107.754 22.3923Z" fill="#FEFAD1"/>
|
||||
<path id="Vector_20" d="M107.775 27.3356C107.898 27.308 107.974 27.178 107.944 27.0451C107.914 26.9122 107.79 26.8269 107.667 26.8544C107.544 26.882 107.469 27.0121 107.498 27.1449C107.528 27.2778 107.652 27.3632 107.775 27.3356Z" fill="#FEFAD1"/>
|
||||
<path id="Vector_21" d="M85.4837 19.6867C85.5535 19.6711 85.5964 19.5973 85.5795 19.5219C85.5626 19.4465 85.4924 19.3981 85.4226 19.4138C85.3527 19.4294 85.3098 19.5032 85.3267 19.5786C85.3436 19.654 85.4139 19.7024 85.4837 19.6867Z" fill="#FEFAD1"/>
|
||||
<path id="Vector_22" d="M88.333 25.6074C88.5347 25.5623 88.6586 25.3491 88.6098 25.1314C88.561 24.9137 88.358 24.7738 88.1564 24.819C87.9547 24.8642 87.8308 25.0773 87.8796 25.295C87.9284 25.5127 88.1314 25.6526 88.333 25.6074Z" fill="#FEFAD1"/>
|
||||
<path id="Vector_23" d="M86.5427 28.8341C86.7276 28.7927 86.8411 28.5973 86.7964 28.3977C86.7517 28.1982 86.5656 28.0699 86.3808 28.1114C86.196 28.1528 86.0824 28.3481 86.1271 28.5477C86.1718 28.7473 86.3579 28.8755 86.5427 28.8341Z" fill="#FEFAD1"/>
|
||||
<path id="Vector_24" d="M84.8425 26.5664C84.9143 26.5503 84.9585 26.4744 84.9411 26.3969C84.9237 26.3193 84.8514 26.2695 84.7796 26.2856C84.7079 26.3017 84.6637 26.3776 84.6811 26.4551C84.6985 26.5326 84.7708 26.5824 84.8425 26.5664Z" fill="#FEFAD1"/>
|
||||
<path id="Vector_25" d="M82.4104 28.6645C82.5547 28.6321 82.6434 28.4796 82.6085 28.3238C82.5736 28.168 82.4283 28.0679 82.284 28.1002C82.1397 28.1325 82.051 28.2851 82.0859 28.4409C82.1208 28.5967 82.2661 28.6968 82.4104 28.6645Z" fill="#FEFAD1"/>
|
||||
<path id="Vector_26" d="M85.3048 32.2755C85.5712 32.2158 85.7349 31.9342 85.6704 31.6466C85.606 31.359 85.3378 31.1742 85.0714 31.2339C84.805 31.2936 84.6414 31.5751 84.7058 31.8628C84.7703 32.1504 85.0384 32.3352 85.3048 32.2755Z" fill="#FEFAD1"/>
|
||||
<path id="Vector_27" d="M98.3039 16.3294C98.4275 16.3017 98.5034 16.1711 98.4735 16.0377C98.4436 15.9043 98.3192 15.8186 98.1956 15.8463C98.0721 15.874 97.9962 16.0046 98.026 16.138C98.0559 16.2714 98.1803 16.3571 98.3039 16.3294Z" fill="#FEFAD1"/>
|
||||
<path id="Vector_28" d="M88.597 15.0845C88.7991 15.0392 88.9234 14.8256 88.8745 14.6073C88.8255 14.3891 88.622 14.2489 88.4199 14.2942C88.2178 14.3394 88.0936 14.5531 88.1425 14.7713C88.1914 14.9896 88.3949 15.1298 88.597 15.0845Z" fill="#FEFAD1"/>
|
||||
<path id="Vector_29" d="M98.1907 9.79363C98.3404 9.76008 98.4324 9.6018 98.3962 9.44011C98.36 9.27842 98.2092 9.17455 98.0595 9.2081C97.9097 9.24166 97.8177 9.39993 97.8539 9.56162C97.8902 9.72331 98.0409 9.82718 98.1907 9.79363Z" fill="#FEFAD1"/>
|
||||
<path id="Vector_30" d="M94.0873 18.8032C94.3403 18.7465 94.4958 18.479 94.4346 18.2058C94.3734 17.9326 94.1186 17.7571 93.8656 17.8138C93.6126 17.8705 93.4571 18.1379 93.5183 18.4111C93.5795 18.6844 93.8343 18.8599 94.0873 18.8032Z" fill="#FEFAD1"/>
|
||||
<path id="Vector_31" d="M99.7758 24.7826C99.9864 24.7355 100.116 24.513 100.065 24.2856C100.014 24.0583 99.8019 23.9123 99.5914 23.9594C99.3809 24.0066 99.2515 24.2291 99.3024 24.4565C99.3534 24.6838 99.5653 24.8298 99.7758 24.7826Z" fill="#FEFAD1"/>
|
||||
<path id="Vector_32" d="M102.012 24.9224C102.09 24.905 102.137 24.8234 102.118 24.74C102.1 24.6566 102.022 24.603 101.945 24.6203C101.868 24.6376 101.82 24.7193 101.839 24.8027C101.857 24.8861 101.935 24.9397 102.012 24.9224Z" fill="#FEFAD1"/>
|
||||
<path id="Vector_33" d="M93.9287 26.5788C94.0478 26.5521 94.121 26.4262 94.0921 26.2976C94.0633 26.169 93.9434 26.0864 93.8243 26.1131C93.7052 26.1398 93.632 26.2657 93.6608 26.3943C93.6896 26.5229 93.8096 26.6055 93.9287 26.5788Z" fill="#FEFAD1"/>
|
||||
<path id="Vector_34" d="M92.2829 23.4413C92.4169 23.4113 92.4992 23.2698 92.4668 23.1252C92.4344 22.9805 92.2995 22.8876 92.1656 22.9176C92.0317 22.9477 91.9494 23.0892 91.9818 23.2338C92.0142 23.3784 92.149 23.4713 92.2829 23.4413Z" fill="#FEFAD1"/>
|
||||
<path id="Vector_35" d="M99.0961 18.0534C99.1852 18.0334 99.24 17.9392 99.2184 17.843C99.1969 17.7467 99.1072 17.6849 99.018 17.7049C98.9289 17.7248 98.8742 17.819 98.8957 17.9153C98.9173 18.0115 99.007 18.0733 99.0961 18.0534Z" fill="#FEFAD1"/>
|
||||
<path id="Vector_36" d="M97.1709 15.1179C97.2791 15.0936 97.3456 14.9792 97.3195 14.8624C97.2933 14.7455 97.1843 14.6704 97.0761 14.6947C96.9679 14.7189 96.9014 14.8333 96.9275 14.9502C96.9537 15.0671 97.0627 15.1421 97.1709 15.1179Z" fill="#FEFAD1"/>
|
||||
<path id="Vector_37" d="M81.7296 41.7882C81.827 41.7663 81.8868 41.6634 81.8632 41.5583C81.8397 41.4532 81.7416 41.3856 81.6443 41.4075C81.5469 41.4293 81.4871 41.5322 81.5107 41.6373C81.5342 41.7424 81.6322 41.81 81.7296 41.7882Z" fill="#FEFAD1"/>
|
||||
<path id="Vector_38" d="M89.5132 18.9328C89.6689 18.8979 89.7646 18.7334 89.7269 18.5653C89.6892 18.3972 89.5325 18.2892 89.3768 18.3241C89.2212 18.359 89.1255 18.5235 89.1632 18.6916C89.2008 18.8597 89.3576 18.9677 89.5132 18.9328Z" fill="#FEFAD1"/>
|
||||
<path id="Vector_39" d="M83.0178 42.3868C83.2308 42.339 83.3617 42.1139 83.3102 41.8839C83.2587 41.6539 83.0442 41.5062 82.8312 41.5539C82.6182 41.6016 82.4873 41.8268 82.5389 42.0568C82.5904 42.2867 82.8048 42.4345 83.0178 42.3868Z" fill="#FEFAD1"/>
|
||||
</g>
|
||||
<g id="Group 1115">
|
||||
<path id="Vector 235" d="M81.2968 18.8709C80.15 19.5007 74.8569 24.2177 69.264 18.2045" stroke="#CF0A80" stroke-width="1.07481" stroke-linecap="round"/>
|
||||
<path id="Vector 238" d="M81.403 7.58447C76.9882 10.4551 79.0933 16.5364 80.6977 19.2183C78.6363 21.0881 80.334 26.1007 83.8008 25.0718C87.2677 24.0429 93.0642 20.9453 94.7322 19.1714C96.4002 17.3975 96.3417 15.6196 95.3991 14.2156C94.645 13.0925 92.8152 12.8952 91.9946 12.937C91.5247 11.6595 86.9215 3.99621 81.403 7.58447Z" fill="url(#paint1_linear_610_2659)"/>
|
||||
<ellipse id="Ellipse 75" cx="83.4887" cy="20.1178" rx="0.744443" ry="0.801708" transform="rotate(-32.9344 83.4887 20.1178)" fill="#3B314F"/>
|
||||
<ellipse id="Ellipse 78" cx="88.8267" cy="18.2172" rx="0.744443" ry="0.801708" transform="rotate(-32.9344 88.8267 18.2172)" fill="#3B314F"/>
|
||||
<path id="Vector 241" d="M83.7052 24.6749C82.3123 25.7315 80.9002 27.7681 81.391 29.849C81.7837 31.5138 84.8901 32.0966 86.3942 32.1799L85.2754 35.4536" stroke="#CF0A80" stroke-width="1.07481" stroke-linecap="round"/>
|
||||
<path id="Vector 242" d="M90.053 21.9248C89.0077 23.005 87.5692 26.5401 88.5494 28.9185C89.3335 30.8212 91.9391 30.8315 93.1438 30.5988L93.0092 34.5245" stroke="#CF0A80" stroke-width="1.07481" stroke-linecap="round"/>
|
||||
<path id="Vector 243" d="M91.1505 13.1363C94.071 11.8155 97.6004 10.838 95.1866 2.61572" stroke="#CF0A80" stroke-width="1.07481" stroke-linecap="round"/>
|
||||
</g>
|
||||
<g id="Group 1117">
|
||||
<path id="Vector 230" d="M108.362 59.9908C108.878 61.4224 111.283 65.3737 110.252 72.4173L107.103 73.6198" stroke="#CF0A80" stroke-width="1.07481" stroke-linecap="round"/>
|
||||
<path id="Vector 231" d="M114.26 61.4778C114.718 62.4513 116.608 66.4598 117.009 72.8735L120.158 73.6752" stroke="#CF0A80" stroke-width="1.07481" stroke-linecap="round"/>
|
||||
<path id="Vector 232" d="M105.613 57.4143C105.346 58.8268 104.972 62.7399 105.613 67.0921" stroke="#CF0A80" stroke-width="1.07481" stroke-linecap="round"/>
|
||||
<path id="Vector 239" d="M115.349 47.0498C109.966 45.6182 107.274 52.2609 107.217 53.578C103.208 53.578 103.505 57.1429 104.354 58.331C104.926 59.1327 106.358 60.507 109.565 61.5951C112.772 62.6831 116.265 63.1412 117.238 63.0267C118.212 62.9122 119.472 63.0267 120.388 60.8506C121.121 59.1098 119.853 57.7202 119.243 57.2429C119.93 54.0361 120.731 48.4814 115.349 47.0498Z" fill="url(#paint2_linear_610_2659)"/>
|
||||
<ellipse id="Ellipse 76" cx="109.425" cy="54.2161" rx="0.744443" ry="0.801708" transform="rotate(23.8851 109.425 54.2161)" fill="#3B314F"/>
|
||||
<ellipse id="Ellipse 77" cx="114.464" cy="55.7024" rx="0.744443" ry="0.801708" transform="rotate(23.8851 114.464 55.7024)" fill="#3B314F"/>
|
||||
<path id="Vector 244" d="M119.128 58.1578C119.892 60.0858 121.27 64.8693 120.674 68.58" stroke="#CF0A80" stroke-width="1.07481" stroke-linecap="round"/>
|
||||
</g>
|
||||
<g id="Group 1112">
|
||||
<g id="Group 1103">
|
||||
<path id="Vector 192" d="M57.4325 76.3215C60.8968 76.7437 61.2898 67.4737 61.1172 66.2695C59.1375 64.5867 56.8946 65.4785 55.9675 66.5606C56.7479 71.1531 53.9682 75.8992 57.4325 76.3215Z" fill="#DE493C"/>
|
||||
</g>
|
||||
<path id="Vector 192_2" d="M69.7065 47.8851C72.112 50.3365 63.9324 56.16 62.847 56.6652C60.0484 55.5457 62.7189 51.7044 63.1123 50.3585C67.3173 48.5324 67.9965 46.1424 69.7065 47.8851Z" fill="url(#paint3_linear_610_2659)"/>
|
||||
<path id="Vector 197" d="M62.581 38.1822C57.838 30.6432 51.049 30.643 46.6805 34.4232C42.5412 38.815 45.517 43.8686 45.517 48.1417C45.517 52.2485 45.0692 54.0403 44.465 58.8275C43.8396 63.7822 45.8656 71.0815 54.1756 70.4294C62.4856 69.7773 64.1899 59.4036 64.5943 56.1669C64.9987 52.9302 66.5005 44.4122 62.581 38.1822Z" fill="url(#paint4_linear_610_2659)"/>
|
||||
<g id="Group 1105">
|
||||
<path id="Vector 204" d="M58.5885 46.1817C60.7291 45.6327 62.1037 44.1468 62.5693 43.1695C61.987 41.936 60.4163 39.219 58.7919 38.219C56.7614 36.969 54.0635 38.3704 52.9144 40.5213C51.995 42.2421 51.7658 44.128 51.467 45.4696C52.7881 46.1662 55.6839 46.9266 58.5885 46.1817Z" fill="#FEF9F5"/>
|
||||
<g id="Mask group">
|
||||
<mask id="mask0_610_2659" style="mask-type:alpha" maskUnits="userSpaceOnUse" x="53" y="37" width="8" height="8">
|
||||
<path id="Ellipse 62" d="M60.9304 40.8302C61.1389 42.9396 59.8365 44.795 58.0215 44.9743C56.2064 45.1537 54.0451 43.7131 53.8366 41.6037C53.6281 39.4944 55.4515 37.515 57.2665 37.3357C59.0816 37.1563 60.7219 38.7208 60.9304 40.8302Z" fill="black"/>
|
||||
</mask>
|
||||
<g mask="url(#mask0_610_2659)">
|
||||
<path id="Vector 205" d="M58.6884 47.0534C60.7556 46.2727 62.2035 44.123 62.6691 43.1457C62.0868 41.9122 60.5161 39.1952 58.8917 38.1952C56.8612 36.9452 54.1633 38.3466 53.0142 40.4975C52.0949 42.2183 51.501 44.7676 51.119 45.8936C52.4401 46.5902 56.1042 48.0293 58.6884 47.0534Z" fill="black"/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
<g id="Group 1080">
|
||||
<path id="Vector 192_3" d="M61.4523 47.9927C63.663 50.6211 57.0661 55.3808 55.9808 55.8859C52.3474 56.0342 52.2258 53.3793 52.6192 52.0333C56.8242 50.2073 59.2417 45.3643 61.4523 47.9927Z" fill="url(#paint5_linear_610_2659)"/>
|
||||
</g>
|
||||
<g id="Group 1104">
|
||||
<path id="Vector 192_4" d="M50.4922 76.3215C53.9564 76.7437 54.3495 67.4737 54.1769 66.2695C52.1972 64.5867 49.9543 65.4785 49.0271 66.5606C49.8076 71.1531 47.0279 75.8992 50.4922 76.3215Z" fill="url(#paint6_linear_610_2659)"/>
|
||||
</g>
|
||||
<path id="Vector 229" d="M61.3924 50.9563C57.5863 55.546 55.7952 56.0311 55.2355 56.3296C55.6459 56.3669 56.9605 56.1125 58.146 55.3221C59.1535 54.6505 61.1685 52.4116 61.3924 50.9563Z" fill="#DB422A"/>
|
||||
</g>
|
||||
<g id="Group 1113">
|
||||
<path id="Vector 233_2" d="M55.0122 24.3001C56.5794 24.0762 60.3631 22.3299 62.9603 17.1357" stroke="#CF0A80" stroke-width="1.07481" stroke-linecap="round"/>
|
||||
<path id="Vector 231_2" d="M55.6913 29.4511C57.1771 29.2236 60.8927 30.096 62.2653 32.2716C63.3633 34.0122 61.8641 36.1431 60.9772 36.991L64.2567 39.1529" stroke="#CF0A80" stroke-width="1.07481" stroke-linecap="round"/>
|
||||
<g id="Group 1106">
|
||||
<path id="Vector 230_2" d="M48.2948 17.3366C53.6777 15.905 56.3691 22.5477 56.4264 23.8648C60.4349 23.8648 60.1382 27.4297 59.2896 28.6178C58.717 29.4195 57.2853 30.7938 54.0785 31.8819C50.8717 32.9699 47.3785 33.428 46.405 33.3135C45.4315 33.199 44.1717 33.3135 43.2555 31.1374C42.5225 29.3966 43.7899 28.007 44.4008 27.5298C43.7136 24.3229 42.9119 18.7682 48.2948 17.3366Z" fill="url(#paint7_linear_610_2659)"/>
|
||||
<ellipse id="Ellipse 75_2" cx="0.744443" cy="0.801708" rx="0.744443" ry="0.801708" transform="matrix(-0.914359 0.404904 0.404904 0.914359 54.5737 23.4677)" fill="#3B314F"/>
|
||||
<ellipse id="Ellipse 76_2" cx="0.744443" cy="0.801708" rx="0.744443" ry="0.801708" transform="matrix(-0.914359 0.404904 0.404904 0.914359 49.5347 24.9547)" fill="#3B314F"/>
|
||||
</g>
|
||||
<path id="Vector 230_3" d="M48.7623 32.1714C50.0795 31.022 52.3603 30.0525 54.2984 30.9554C55.8488 31.6778 55.7891 34.8378 55.5654 36.3275L58.9981 35.8964" stroke="#CF0A80" stroke-width="1.07481" stroke-linecap="round"/>
|
||||
<path id="Vector 232_2" d="M45.8327 28.116C44.9371 29.4967 43.952 33.5117 47.176 38.5269" stroke="#CF0A80" stroke-width="1.07481" stroke-linecap="round"/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
<defs>
|
||||
<filter id="filter0_f_610_2659" x="67.0745" y="-4.73798" width="64.0807" height="86.1527" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
|
||||
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
|
||||
<feBlend mode="normal" in="SourceGraphic" in2="BackgroundImageFix" result="shape"/>
|
||||
<feGaussianBlur stdDeviation="5.27353" result="effect1_foregroundBlur_610_2659"/>
|
||||
</filter>
|
||||
<linearGradient id="paint0_linear_610_2659" x1="78.7891" y1="49.0961" x2="78.7891" y2="65.3621" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#F783F2"/>
|
||||
<stop offset="1" stop-color="#E040DA"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="paint1_linear_610_2659" x1="86.6058" y1="6.39613" x2="88.3916" y2="24.6866" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#F783F2"/>
|
||||
<stop offset="1" stop-color="#E040DA"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="paint2_linear_610_2659" x1="112.218" y1="46.8489" x2="112.218" y2="63.0438" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#F783F2"/>
|
||||
<stop offset="1" stop-color="#E040DA"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="paint3_linear_610_2659" x1="70.3013" y1="49.091" x2="61.2322" y2="55.0098" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#E04D40"/>
|
||||
<stop offset="1" stop-color="#F15F56"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="paint4_linear_610_2659" x1="52.3992" y1="31.1506" x2="52.2407" y2="72.3823" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#F46A6A"/>
|
||||
<stop offset="1" stop-color="#EA5749"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="paint5_linear_610_2659" x1="62.047" y1="49.1988" x2="52.9779" y2="55.1175" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#FF6F66"/>
|
||||
<stop offset="1" stop-color="#EF6058"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="paint6_linear_610_2659" x1="51.8488" y1="76.1581" x2="51.8512" y2="65.1538" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#DA4639"/>
|
||||
<stop offset="1" stop-color="#EC5B4F"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="paint7_linear_610_2659" x1="51.4253" y1="17.1357" x2="51.4253" y2="33.3306" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#F783F2"/>
|
||||
<stop offset="1" stop-color="#E040DA"/>
|
||||
</linearGradient>
|
||||
<clipPath id="clip0_610_2659">
|
||||
<rect width="163" height="76.3494" fill="white" transform="translate(0.328491 0.0872803)"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
After Width: | Height: | Size: 21 KiB |
89
newIDE/app/public/res/github-star-credits.svg
Normal file
89
newIDE/app/public/res/github-star-credits.svg
Normal file
@@ -0,0 +1,89 @@
|
||||
<svg width="95" height="95" viewBox="0 0 95 95" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g clip-path="url(#clip0_131_5)">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M13.9642 41C7.34833 41 2 46.5 2 53.3043C2 58.7433 5.42686 63.3473 10.1808 64.9768C10.7752 65.0993 10.9929 64.712 10.9929 64.3863C10.9929 64.101 10.9733 63.1233 10.9733 62.1045C7.64514 62.838 6.95208 60.6378 6.95208 60.6378C6.41722 59.2118 5.62473 58.8453 5.62473 58.8453C4.53543 58.0915 5.70408 58.0915 5.70408 58.0915C6.91241 58.173 7.54645 59.3545 7.54645 59.3545C8.61592 61.2285 10.3393 60.699 11.0326 60.373C11.1315 59.5785 11.4487 59.0285 11.7854 58.723C9.13094 58.4378 6.33812 57.3785 6.33812 52.6523C6.33812 51.3078 6.81323 50.2078 7.56604 49.3523C7.44727 49.0468 7.03118 47.7835 7.68506 46.0928C7.68506 46.0928 8.69527 45.7668 10.9731 47.3558C11.9483 47.0864 12.954 46.9494 13.9642 46.9483C14.9744 46.9483 16.0042 47.091 16.9552 47.3558C19.2332 45.7668 20.2434 46.0928 20.2434 46.0928C20.8973 47.7835 20.481 49.0468 20.3622 49.3523C21.1349 50.2078 21.5904 51.3078 21.5904 52.6523C21.5904 57.3785 18.7976 58.4173 16.1233 58.723C16.5592 59.11 16.9353 59.8433 16.9353 61.0045C16.9353 62.6545 16.9158 63.9788 16.9158 64.386C16.9158 64.712 17.1337 65.0993 17.7278 64.977C22.4818 63.347 25.9087 58.7433 25.9087 53.3043C25.9282 46.5 20.5603 41 13.9642 41Z" fill="#CAD1D8"/>
|
||||
</g>
|
||||
<ellipse cx="43.6223" cy="83.8794" rx="4.99631" ry="4.73335" transform="rotate(-135.137 43.6223 83.8794)" fill="#CE8C0C"/>
|
||||
<circle cx="43.0658" cy="84.4386" r="4.99631" transform="rotate(-135.137 43.0658 84.4386)" fill="url(#paint0_linear_131_5)"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M45.6381 81.854C44.2107 80.4334 41.9019 80.4389 40.4813 81.8664C39.0606 83.2938 39.0662 85.6026 40.4936 87.0232C41.921 88.4438 44.2298 88.4383 45.6504 87.0108C47.071 85.5834 47.0655 83.2746 45.6381 81.854ZM46.0338 81.4564C44.3868 79.8172 41.7228 79.8236 40.0836 81.4706C38.4445 83.1177 38.4508 85.7816 40.0979 87.4208C41.7449 89.06 44.4089 89.0536 46.048 87.4066C47.6872 85.7596 47.6808 83.0956 46.0338 81.4564Z" fill="#E2AB1A"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M40.4813 81.8664C38.7005 83.6557 39.5284 85.8388 39.6988 86.4301C39.8692 87.0213 40.0979 87.4208 40.0979 87.4208C38.4508 85.7816 38.4445 83.1177 40.0836 81.4706C41.7228 79.8236 44.3868 79.8172 46.0338 81.4564C46.0338 81.4564 45.9327 81.359 45.1401 80.9627C44.3475 80.5664 41.9019 80.4389 40.4813 81.8664Z" fill="#D89C00"/>
|
||||
<path d="M44.5016 83.2705C44.8527 83.6199 45.1 84.0474 45.2011 84.543C45.2716 84.8884 45.2018 85.4403 44.6751 85.9695C43.698 86.9513 42.819 86.7897 41.7691 85.7448L40.2605 84.2434C40.2605 84.2434 40.2118 84.1995 40.2134 84.1715C40.2154 84.1363 40.276 84.1087 40.276 84.1087L40.8339 83.8319C40.8339 83.8319 40.8914 83.8118 40.9235 83.8185C40.9611 83.8264 40.9977 83.864 40.9977 83.864L42.1816 85.0422C42.8745 85.7319 43.3382 86.0901 44.0638 85.361C44.3034 85.1202 44.4032 84.8784 44.3688 84.6218C44.3105 84.1882 43.8402 83.7041 43.5211 83.4403C43.5211 83.4403 43.0328 83.6911 42.9532 83.7316C42.86 83.779 42.8534 83.8058 42.9256 83.8777C43.0298 83.9814 43.2172 84.1679 43.416 84.3658C43.5554 84.5045 43.6253 84.7081 43.4644 84.8698C43.3089 85.026 43.0913 84.9899 42.9167 84.8161C42.6143 84.5152 41.9916 83.8954 41.875 83.7794C41.7222 83.6274 41.5872 83.4511 41.8979 83.2981L43.54 82.4908C43.54 82.4908 43.9728 82.7443 44.5016 83.2705Z" fill="#E2A60B"/>
|
||||
<ellipse cx="56.6615" cy="56.2395" rx="7.62914" ry="7.22761" transform="rotate(-25.1664 56.6615 56.2395)" fill="#CE8C0C"/>
|
||||
<circle cx="56.1492" cy="55.1492" r="7.62914" transform="rotate(-25.1664 56.1492 55.1492)" fill="url(#paint1_linear_131_5)"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M58.517 60.1886C61.3002 58.881 62.4963 55.5647 61.1886 52.7815C59.881 49.9983 56.5647 48.8022 53.7815 50.1098C50.9983 51.4175 49.8022 54.7338 51.1098 57.517C52.4175 60.3002 55.7338 61.4963 58.517 60.1886ZM58.8813 60.9639C62.0926 59.4551 63.4728 55.6286 61.9639 52.4172C60.4551 49.2058 56.6286 47.8257 53.4172 49.3345C50.2058 50.8434 48.8257 54.6699 50.3345 57.8813C51.8434 61.0926 55.6699 62.4728 58.8813 60.9639Z" fill="#E2AB1A"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M61.1886 52.7815C59.5494 49.2927 55.9846 49.3423 55.0472 49.2785C54.1098 49.2148 53.4172 49.3345 53.4172 49.3345C56.6286 47.8257 60.4551 49.2058 61.9639 52.4172C63.4728 55.6286 62.0926 59.4551 58.8813 60.9639C58.8813 60.9639 59.0738 60.8696 60.0559 59.9388C61.038 59.008 62.4963 55.5647 61.1886 52.7815Z" fill="#D89C00"/>
|
||||
<path d="M57.0768 57.8189C56.3923 58.1406 55.6498 58.2725 54.8858 58.1592C54.3534 58.0803 53.5978 57.6923 53.113 56.6604C52.2135 54.7461 52.9039 53.5688 54.9509 52.607L57.8923 51.225C57.8923 51.225 57.9808 51.178 58.0201 51.1949C58.0696 51.2161 58.0777 51.3175 58.0777 51.3175L58.1839 52.2626C58.1839 52.2626 58.1828 52.3556 58.1564 52.3982C58.1255 52.448 58.0525 52.4808 58.0525 52.4808L55.7441 53.5654C54.393 54.2002 53.6371 54.6788 54.305 56.1003C54.5257 56.5699 54.8206 56.8392 55.2068 56.9236C55.8595 57.0662 56.7995 56.6436 57.3445 56.3232C57.3445 56.3232 57.2392 55.4916 57.2227 55.3563C57.2033 55.1978 57.1681 55.1744 57.0274 55.2405C56.8242 55.336 56.4589 55.5076 56.0711 55.6898C55.7993 55.8175 55.4707 55.8116 55.3225 55.4963C55.1794 55.1917 55.3448 54.8983 55.6853 54.7383C56.2748 54.4613 57.489 53.8908 57.7163 53.784C58.0142 53.644 58.3375 53.5422 58.3951 54.0678L58.6973 56.8455C58.6973 56.8455 58.1078 57.3345 57.0768 57.8189Z" fill="#E2A60B"/>
|
||||
<ellipse cx="72.1334" cy="87.0926" rx="5.76419" ry="5.46081" transform="rotate(17.3995 72.1334 87.0926)" fill="#CE8C0C"/>
|
||||
<circle cx="72.4056" cy="86.2241" r="5.76419" transform="rotate(17.3995 72.4056 86.2241)" fill="url(#paint2_linear_131_5)"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M71.1476 90.2385C73.3647 90.9332 75.7252 89.6992 76.4199 87.4821C77.1147 85.265 75.8806 82.9045 73.6636 82.2098C71.4465 81.515 69.086 82.7491 68.3912 84.9661C67.6965 87.1832 68.9305 89.5437 71.1476 90.2385ZM70.954 90.8561C73.5122 91.6577 76.2359 90.2338 77.0375 87.6756C77.8392 85.1175 76.4152 82.3938 73.8571 81.5922C71.2989 80.7905 68.5753 82.2144 67.7736 84.7726C66.972 87.3307 68.3959 90.0544 70.954 90.8561Z" fill="#E2AB1A"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M76.4199 87.4821C77.2908 84.7029 75.2818 82.9086 74.7927 82.3941C74.3037 81.8795 73.8571 81.5922 73.8571 81.5922C76.4152 82.3938 77.8391 85.1175 77.0375 87.6756C76.2358 90.2338 73.5122 91.6577 70.954 90.8561C70.954 90.8561 71.1094 90.902 72.1316 90.886C73.1538 90.8699 75.7251 89.6992 76.4199 87.4821Z" fill="#D89C00"/>
|
||||
<path d="M71.5573 88.1838C71.012 88.0129 70.5314 87.7068 70.1642 87.2533C69.9082 86.9373 69.6861 86.3352 69.9436 85.5133C70.4215 83.9883 71.4074 83.686 73.038 84.1971L75.3811 84.9313C75.3811 84.9313 75.4544 84.9504 75.4676 84.9798C75.4843 85.0169 75.437 85.0775 75.437 85.0775L75.0131 85.6577C75.0131 85.6577 74.965 85.7089 74.9285 85.7191C74.8858 85.731 74.8285 85.712 74.8285 85.712L72.9896 85.1357C71.9133 84.7984 71.2481 84.6785 70.8932 85.8108C70.776 86.1849 70.8025 86.4855 70.9743 86.7298C71.2646 87.1427 72.0036 87.388 72.4707 87.4883C72.4707 87.4883 72.8371 86.9717 72.897 86.888C72.9673 86.7899 72.9597 86.7588 72.8476 86.7237C72.6857 86.673 72.3947 86.5818 72.0858 86.485C71.8693 86.4171 71.6894 86.2459 71.7681 85.9947C71.8442 85.7521 72.0862 85.6733 72.3574 85.7583C72.827 85.9055 73.7943 86.2086 73.9753 86.2653C74.2126 86.3397 74.4446 86.4482 74.208 86.7702L72.9565 88.4703C72.9565 88.4703 72.3786 88.4411 71.5573 88.1838Z" fill="#E2A60B"/>
|
||||
<ellipse cx="85.3832" cy="67.9554" rx="6.83536" ry="6.47561" transform="rotate(-22.9844 85.3832 67.9554)" fill="#CE8C0C"/>
|
||||
<circle cx="84.9618" cy="66.9618" r="6.83536" transform="rotate(-22.9844 84.9618 66.9618)" fill="url(#paint3_linear_131_5)"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M86.9098 71.5544C89.4462 70.4785 90.6302 67.5502 89.5544 65.0138C88.4786 62.4774 85.5503 61.2934 83.0139 62.3692C80.4774 63.445 79.2934 66.3733 80.3692 68.9097C81.4451 71.4462 84.3734 72.6302 86.9098 71.5544ZM87.2095 72.2609C90.1361 71.0196 91.5023 67.6408 90.2609 64.7141C89.0196 61.7875 85.6408 60.4213 82.7142 61.6627C79.7875 62.904 78.4213 66.2828 79.6627 69.2094C80.904 72.1361 84.2828 73.5023 87.2095 72.2609Z" fill="#E2AB1A"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M89.5544 65.0138C88.2058 61.8344 85.0124 61.7572 84.1754 61.6681C83.3383 61.5791 82.7141 61.6627 82.7141 61.6627C85.6408 60.4213 89.0196 61.7875 90.2609 64.7141C91.5023 67.6408 90.1361 71.0196 87.2094 72.2609C87.2094 72.2609 87.385 72.183 88.2961 71.3832C89.2071 70.5834 90.6302 67.5502 89.5544 65.0138Z" fill="#D89C00"/>
|
||||
<path d="M85.7012 69.3836C85.0774 69.6482 84.4081 69.7411 83.728 69.6135C83.254 69.5247 82.5907 69.1516 82.1919 68.2112C81.4519 66.4666 82.1101 65.4361 83.9757 64.6449L86.6563 63.5079C86.6563 63.5079 86.7371 63.4688 86.7717 63.4853C86.8153 63.506 86.8191 63.597 86.8191 63.597L86.8819 64.4467C86.8819 64.4467 86.8778 64.53 86.8527 64.5672C86.8233 64.6108 86.7569 64.6376 86.7569 64.6376L84.6531 65.53C83.4218 66.0522 82.7288 66.4549 83.2782 67.7504C83.4597 68.1783 83.7146 68.4295 84.0575 68.5182C84.637 68.6681 85.493 68.3219 85.9919 68.0536C85.9919 68.0536 85.926 67.3055 85.9158 67.1838C85.9038 67.0413 85.8732 67.019 85.7449 67.0734C85.5597 67.152 85.2268 67.2932 84.8734 67.4431C84.6257 67.5481 84.3317 67.5317 84.2098 67.2443C84.0921 66.9668 84.2501 66.7096 84.5604 66.578C85.0977 66.3501 86.2042 65.8808 86.4113 65.793C86.6829 65.6778 86.9758 65.5976 87.0094 66.0702L87.1853 68.5674C87.1853 68.5674 86.6408 68.9851 85.7012 69.3836Z" fill="#E2A60B"/>
|
||||
<rect x="12.3901" y="57.3901" width="70.2527" height="26.5275" rx="5.07143" fill="#22262C"/>
|
||||
<rect x="12.3901" y="57.3901" width="70.2527" height="26.5275" rx="5.07143" stroke="#31363C" stroke-width="0.78022"/>
|
||||
<path d="M32.2436 63.5008C32.4035 63.1163 32.9482 63.1163 33.1081 63.5008L34.7329 67.4074C34.8003 67.5694 34.9528 67.6802 35.1277 67.6942L39.3453 68.0323C39.7603 68.0656 39.9286 68.5836 39.6124 68.8545L36.3991 71.607C36.2658 71.7212 36.2075 71.9004 36.2483 72.0712L37.23 76.1868C37.3266 76.5918 36.886 76.9119 36.5306 76.6949L32.9198 74.4894C32.77 74.3979 32.5816 74.3979 32.4318 74.4894L28.821 76.6949C28.4657 76.9119 28.025 76.5918 28.1217 76.1868L29.1034 72.0712C29.1441 71.9004 29.0859 71.7212 28.9526 71.607L25.7393 68.8545C25.423 68.5836 25.5913 68.0656 26.0064 68.0323L30.2239 67.6942C30.3989 67.6802 30.5513 67.5694 30.6187 67.4074L32.2436 63.5008Z" stroke="#CAD1D8" stroke-width="1.3"/>
|
||||
<path d="M51.4986 68.4599C51.4513 68.0403 51.2563 67.7152 50.9135 67.4846C50.5706 67.2512 50.1392 67.1344 49.619 67.1344C49.2466 67.1344 48.9245 67.1935 48.6526 67.3118C48.3807 67.427 48.1694 67.5866 48.0187 67.7905C47.8709 67.9915 47.797 68.2205 47.797 68.4776C47.797 68.6934 47.8473 68.8796 47.9477 69.0362C48.0512 69.1928 48.1857 69.3244 48.3512 69.4308C48.5196 69.5342 48.6999 69.6214 48.892 69.6923C49.0841 69.7603 49.2688 69.8164 49.4461 69.8608L50.3327 70.0913C50.6224 70.1622 50.9194 70.2583 51.2238 70.3794C51.5282 70.5006 51.8104 70.6602 52.0705 70.8582C52.3306 71.0562 52.5404 71.3015 52.7 71.5941C52.8625 71.8867 52.9438 72.2369 52.9438 72.6447C52.9438 73.159 52.8108 73.6156 52.5448 74.0145C52.2818 74.4135 51.8991 74.7283 51.3967 74.9588C50.8972 75.1893 50.2928 75.3046 49.5835 75.3046C48.9038 75.3046 48.3157 75.1967 47.8192 74.981C47.3227 74.7652 46.934 74.4593 46.6533 74.0633C46.3725 73.6643 46.2174 73.1915 46.1878 72.6447H47.5621C47.5887 72.9728 47.6951 73.2462 47.8812 73.4648C48.0704 73.6806 48.3113 73.8417 48.6038 73.9481C48.8994 74.0515 49.223 74.1032 49.5747 74.1032C49.9618 74.1032 50.3061 74.0426 50.6076 73.9215C50.912 73.7973 51.1514 73.6259 51.3257 73.4072C51.5001 73.1856 51.5873 72.927 51.5873 72.6314C51.5873 72.3625 51.5105 72.1423 51.3568 71.9709C51.206 71.7995 51.0006 71.6576 50.7406 71.5453C50.4835 71.433 50.1924 71.334 49.8673 71.2483L48.7945 70.9557C48.0674 70.7577 47.4911 70.4666 47.0656 70.0824C46.6429 69.6982 46.4316 69.1899 46.4316 68.5574C46.4316 68.0343 46.5735 67.5777 46.8572 67.1876C47.1409 66.7975 47.5251 66.4946 48.0098 66.2788C48.4945 66.0601 49.0412 65.9508 49.65 65.9508C50.2648 65.9508 50.8071 66.0587 51.277 66.2744C51.7498 66.4902 52.1222 66.7872 52.3941 67.1655C52.666 67.5408 52.8079 67.9723 52.8197 68.4599H51.4986ZM57.6528 68.3447V69.4086H53.9335V68.3447H57.6528ZM54.9309 66.7133H56.2564V73.1545C56.2564 73.4117 56.2948 73.6052 56.3717 73.7353C56.4485 73.8623 56.5475 73.9495 56.6687 73.9968C56.7928 74.0411 56.9273 74.0633 57.0721 74.0633C57.1785 74.0633 57.2716 74.0559 57.3514 74.0411C57.4312 74.0264 57.4932 74.0145 57.5376 74.0057L57.777 75.1006C57.7001 75.1302 57.5908 75.1598 57.4489 75.1893C57.3071 75.2218 57.1297 75.2396 56.9169 75.2425C56.5682 75.2484 56.2431 75.1864 55.9417 75.0563C55.6402 74.9263 55.3964 74.7253 55.2102 74.4534C55.024 74.1815 54.9309 73.8402 54.9309 73.4294V66.7133ZM61.1007 75.3046C60.6692 75.3046 60.2791 75.2248 59.9303 75.0652C59.5816 74.9026 59.3053 74.6677 59.1013 74.3603C58.9004 74.053 58.7999 73.6762 58.7999 73.2299C58.7999 72.8457 58.8738 72.5295 59.0215 72.2812C59.1693 72.033 59.3688 71.8364 59.62 71.6916C59.8712 71.5468 60.152 71.4375 60.4623 71.3636C60.7726 71.2897 61.0888 71.2335 61.411 71.1951C61.8188 71.1478 62.1498 71.1094 62.404 71.0799C62.6581 71.0473 62.8428 70.9956 62.9581 70.9247C63.0734 70.8538 63.131 70.7385 63.131 70.5789V70.5479C63.131 70.1607 63.0216 69.8608 62.803 69.648C62.5872 69.4352 62.2651 69.3288 61.8365 69.3288C61.3903 69.3288 61.0386 69.4278 60.7815 69.6258C60.5273 69.8209 60.3515 70.0381 60.2539 70.2775L59.0082 69.9938C59.156 69.58 59.3718 69.246 59.6555 68.9919C59.9421 68.7348 60.2717 68.5486 60.644 68.4333C61.0164 68.3151 61.408 68.256 61.8188 68.256C62.0907 68.256 62.3789 68.2885 62.6833 68.3535C62.9906 68.4156 63.2773 68.5308 63.5433 68.6993C63.8122 68.8678 64.0324 69.1086 64.2038 69.4219C64.3752 69.7322 64.4609 70.1356 64.4609 70.6321V75.1538H63.1665V74.2229H63.1133C63.0276 74.3943 62.899 74.5628 62.7276 74.7283C62.5562 74.8938 62.336 75.0312 62.0671 75.1405C61.7981 75.2499 61.476 75.3046 61.1007 75.3046ZM61.3888 74.2406C61.7553 74.2406 62.0685 74.1682 62.3286 74.0234C62.5916 73.8786 62.7911 73.6895 62.9271 73.456C63.066 73.2196 63.1354 72.9669 63.1354 72.6979V71.8202C63.0881 71.8675 62.9965 71.9118 62.8606 71.9532C62.7276 71.9916 62.5754 72.0256 62.404 72.0551C62.2326 72.0817 62.0656 72.1069 61.903 72.1305C61.7405 72.1512 61.6045 72.1689 61.4952 72.1837C61.2381 72.2162 61.0031 72.2709 60.7903 72.3477C60.5805 72.4246 60.412 72.5354 60.285 72.6802C60.1608 72.8221 60.0988 73.0112 60.0988 73.2476C60.0988 73.5757 60.22 73.8239 60.4623 73.9924C60.7046 74.1579 61.0135 74.2406 61.3888 74.2406ZM66.2264 75.1538V68.3447H67.5075V69.4263H67.5785C67.7026 69.0599 67.9213 68.7717 68.2346 68.5619C68.5508 68.3491 68.9084 68.2427 69.3074 68.2427C69.3901 68.2427 69.4876 68.2457 69.6 68.2516C69.7152 68.2575 69.8053 68.2649 69.8704 68.2737V69.5416C69.8172 69.5268 69.7226 69.5106 69.5867 69.4928C69.4507 69.4721 69.3148 69.4618 69.1788 69.4618C68.8655 69.4618 68.5863 69.5283 68.341 69.6613C68.0986 69.7913 67.9065 69.9731 67.7647 70.2065C67.6228 70.4371 67.5519 70.7001 67.5519 70.9956V75.1538H66.2264Z" fill="#CAD1D8"/>
|
||||
<path d="M51.237 20.3049C51.406 19.4316 52.5456 19.197 53.0457 19.9326L57.3855 26.3152C57.5963 26.6253 57.9629 26.7913 58.3351 26.7453L65.995 25.7985C66.8778 25.6894 67.453 26.7007 66.908 27.4037L62.1789 33.5034C61.9491 33.7997 61.9044 34.1997 62.0632 34.5395L65.3307 41.5319C65.7073 42.3377 64.9232 43.1973 64.0862 42.8962L56.8237 40.2834C56.4708 40.1565 56.0766 40.2376 55.8026 40.4936L50.1621 45.7619C49.5121 46.3691 48.4523 45.889 48.48 45L48.7206 37.2855C48.7323 36.9107 48.5333 36.5609 48.2052 36.3793L41.4517 32.6429C40.6734 32.2123 40.8025 31.0561 41.6566 30.8077L49.0678 28.6527C49.4279 28.548 49.6992 28.2506 49.7704 27.8824L51.237 20.3049Z" fill="#FFA216"/>
|
||||
<path d="M31.8208 54.0985C32.095 53.2523 33.2547 53.1584 33.6614 53.9494L35.0197 56.5909C35.1912 56.9243 35.5349 57.1338 35.9098 57.1335L38.88 57.1311C39.7695 57.1304 40.2172 58.2043 39.5906 58.8356L37.4982 60.9437C37.234 61.2098 37.141 61.6014 37.2571 61.9579L38.1773 64.782C38.4528 65.6277 37.5698 66.3854 36.7758 65.9845L34.1243 64.6459C33.7896 64.4769 33.3884 64.5094 33.0852 64.7301L30.6837 66.4779C29.9645 67.0013 28.9711 66.3956 29.107 65.5166L29.5607 62.5812C29.6179 62.2106 29.4631 61.8391 29.1595 61.619L26.7552 59.8751C26.0351 59.3528 26.3042 58.2209 27.1822 58.0785L30.1141 57.6029C30.4842 57.5429 30.7897 57.2808 30.9053 56.9241L31.8208 54.0985Z" fill="#FFBE16" fill-opacity="0.5"/>
|
||||
<path d="M37.0362 37.808C37.2082 36.9353 38.3485 36.7047 38.8462 37.4419L41.2784 41.0456C41.4882 41.3565 41.8543 41.5238 42.2266 41.4791L46.5433 40.9605C47.4264 40.8545 47.9981 41.8677 47.4507 42.5688L44.775 45.9957C44.5442 46.2912 44.4982 46.6911 44.6558 47.0313L46.4829 50.9765C46.8567 51.7836 46.0697 52.6405 45.2337 52.3365L41.1478 50.8507C40.7953 50.7226 40.4009 50.8024 40.1259 51.0574L36.9384 54.0142C36.2863 54.6191 35.2282 54.1354 35.259 53.2464L35.4094 48.9013C35.4224 48.5266 35.2246 48.176 34.8971 47.9934L31.1001 45.8756C30.3232 45.4423 30.4563 44.2865 31.3112 44.0411L35.4902 42.8414C35.8506 42.738 36.1229 42.4416 36.1954 42.0736L37.0362 37.808Z" fill="#FA9704" fill-opacity="0.73"/>
|
||||
<ellipse cx="39" cy="19.1053" rx="7" ry="6.63158" fill="#CE8C0C"/>
|
||||
<circle cx="39" cy="18" r="7" fill="url(#paint4_linear_131_5)"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M39 23.1088C41.8215 23.1088 44.1088 20.8215 44.1088 18C44.1088 15.1785 41.8215 12.8912 39 12.8912C36.1785 12.8912 33.8912 15.1785 33.8912 18C33.8912 20.8215 36.1785 23.1088 39 23.1088ZM39 23.8947C42.2556 23.8947 44.8947 21.2556 44.8947 18C44.8947 14.7444 42.2556 12.1053 39 12.1053C35.7444 12.1053 33.1053 14.7444 33.1053 18C33.1053 21.2556 35.7444 23.8947 39 23.8947Z" fill="#E2AB1A"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M44.1088 18C44.1088 14.4632 41.129 13.1134 40.3754 12.6947C39.6219 12.2761 39 12.1053 39 12.1053C42.2556 12.1053 44.8947 14.7444 44.8947 18C44.8947 21.2556 42.2556 23.8947 39 23.8947C39 23.8947 39.1967 23.8915 40.3754 23.5018C41.5542 23.112 44.1088 20.8215 44.1088 18Z" fill="#D89C00"/>
|
||||
<path d="M38.7286 20.5789C38.0347 20.5789 37.3666 20.3988 36.7764 20.0066C36.365 19.7334 35.8889 19.1163 35.8889 18.0703C35.8889 16.1296 36.9216 15.4213 38.9968 15.4213L41.9787 15.4213C41.9787 15.4213 42.0705 15.4168 42.0966 15.4461C42.1294 15.4831 42.0966 15.5704 42.0966 15.5704L41.816 16.3967C41.816 16.3967 41.7788 16.4735 41.7402 16.4985C41.6951 16.5279 41.6217 16.5266 41.6217 16.5266H39.2815C37.9118 16.5266 37.0974 16.6292 37.0974 18.0703C37.0974 18.5463 37.2372 18.885 37.525 19.1057C38.0114 19.4788 38.9569 19.4947 39.5346 19.4413C39.5346 19.4413 39.7716 18.7096 39.8106 18.5908C39.8564 18.4516 39.8363 18.4184 39.6937 18.4184C39.4877 18.4184 39.1173 18.4184 38.7242 18.4184C38.4487 18.4184 38.178 18.2853 38.178 17.9656C38.178 17.6569 38.4299 17.4777 38.775 17.4777C39.3727 17.4777 40.6036 17.4777 40.834 17.4777C41.136 17.4777 41.4443 17.5193 41.287 17.9783L40.4542 20.4029C40.4542 20.4029 39.7738 20.5789 38.7286 20.5789Z" fill="#E2A60B"/>
|
||||
<ellipse cx="20.8487" cy="7.49671" rx="3.89322" ry="3.68832" transform="rotate(18.1576 20.8487 7.49671)" fill="#CE8C0C"/>
|
||||
<circle cx="21.0403" cy="6.9126" r="3.89322" transform="rotate(18.1576 21.0403 6.9126)" fill="url(#paint5_linear_131_5)"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M20.1548 9.61248C21.6459 10.1015 23.2512 9.28917 23.7402 7.79807C24.2292 6.30696 23.4169 4.70175 21.9258 4.21273C20.4347 3.7237 18.8295 4.53604 18.3404 6.02714C17.8514 7.51824 18.6637 9.12345 20.1548 9.61248ZM20.0186 10.0278C21.7391 10.5921 23.5913 9.65479 24.1556 7.93429C24.7198 6.21379 23.7825 4.36162 22.062 3.79736C20.3415 3.2331 18.4893 4.17041 17.9251 5.89092C17.3608 7.61142 18.2981 9.46358 20.0186 10.0278Z" fill="#E2AB1A"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M23.7402 7.79807C24.3532 5.92892 23.0124 4.69916 22.6867 4.34728C22.361 3.9954 22.062 3.79736 22.062 3.79736C23.7825 4.36163 24.7198 6.21379 24.1556 7.93429C23.5913 9.65479 21.7391 10.5921 20.0186 10.0278C20.0186 10.0278 20.1231 10.0602 20.8136 10.0586C21.5041 10.0569 23.2512 9.28917 23.7402 7.79807Z" fill="#D89C00"/>
|
||||
<path d="M20.4499 8.22849C20.0832 8.10821 19.7613 7.89723 19.5174 7.58766C19.3474 7.37195 19.2027 6.96334 19.384 6.41052C19.7204 5.38491 20.3889 5.18958 21.4856 5.54927L23.0614 6.06609C23.0614 6.06609 23.1107 6.07962 23.1194 6.09964C23.1303 6.12485 23.0979 6.16532 23.0979 6.16532L22.8064 6.55334C22.8064 6.55334 22.7734 6.58751 22.7487 6.59406C22.7198 6.60174 22.6812 6.58838 22.6812 6.58838L21.4445 6.18277C20.7206 5.94537 20.2724 5.8584 20.0226 6.61998C19.9401 6.87153 19.9553 7.07479 20.0692 7.24132C20.2616 7.5228 20.7585 7.69505 21.073 7.76693C21.073 7.76693 21.3251 7.42135 21.3663 7.36534C21.4146 7.2997 21.4098 7.27868 21.3344 7.25396C21.2255 7.21825 21.0298 7.15406 20.822 7.08592C20.6764 7.03817 20.5565 6.92093 20.6119 6.75198C20.6654 6.58882 20.8295 6.53776 21.0119 6.59758C21.3278 6.70117 21.9783 6.91452 22.1001 6.95445C22.2597 7.0068 22.4154 7.08219 22.2527 7.2975L21.3923 8.43452C21.3923 8.43452 21.0023 8.40965 20.4499 8.22849Z" fill="#E2A60B"/>
|
||||
<ellipse cx="81.1516" cy="42.773" rx="9.43425" ry="8.93771" transform="rotate(18.0514 81.1516 42.773)" fill="#CE8C0C"/>
|
||||
<circle cx="81.6132" cy="41.3567" r="9.43425" transform="rotate(18.0514 81.6132 41.3567)" fill="url(#paint6_linear_131_5)"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M79.4796 47.9031C83.0951 49.0815 86.9813 47.1058 88.1596 43.4903C89.3379 39.8748 87.3622 35.9886 83.7467 34.8103C80.1312 33.6319 76.2451 35.6076 75.0667 39.2231C73.8884 42.8386 75.8641 46.7248 79.4796 47.9031ZM79.1513 48.9103C83.3231 50.2699 87.8071 47.9902 89.1667 43.8185C90.5264 39.6468 88.2467 35.1627 84.075 33.8031C79.9032 32.4435 75.4192 34.7232 74.0596 38.8949C72.7 43.0666 74.9796 47.5507 79.1513 48.9103Z" fill="#E2AB1A"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M88.1596 43.4903C89.6367 38.9581 86.3821 35.9841 85.5913 35.1329C84.8005 34.2817 84.075 33.8031 84.075 33.8031C88.2467 35.1627 90.5263 39.6468 89.1667 43.8185C87.8071 47.9902 83.3231 50.2699 79.1513 48.9103C79.1513 48.9103 79.4047 48.9883 81.078 48.9811C82.7512 48.974 86.9812 47.1058 88.1596 43.4903Z" fill="#D89C00"/>
|
||||
<path d="M80.1884 44.5481C79.2991 44.2582 78.5183 43.7484 77.9258 42.9994C77.5128 42.4774 77.1604 41.4879 77.5972 40.1475C78.4077 37.6607 80.0268 37.1843 82.686 38.051L86.507 39.2963C86.507 39.2963 86.6266 39.3289 86.6477 39.3774C86.6743 39.4384 86.5958 39.5366 86.5958 39.5366L85.8912 40.4782C85.8912 40.4782 85.8115 40.5611 85.7516 40.5771C85.6815 40.5958 85.588 40.5636 85.588 40.5636L82.5892 39.5863C80.8341 39.0143 79.7476 38.8055 79.1458 40.6522C78.947 41.2621 78.9847 41.7546 79.2613 42.1576C79.7288 42.8388 80.9337 43.254 81.6962 43.4268C81.6962 43.4268 82.3055 42.5882 82.4052 42.4523C82.5219 42.293 82.5101 42.2421 82.3273 42.1826C82.0633 42.0965 81.5887 41.9418 81.085 41.7777C80.732 41.6626 80.4407 41.379 80.5742 40.9694C80.7032 40.5738 81.1007 40.4493 81.543 40.5935C82.3089 40.8431 83.8862 41.3571 84.1814 41.4533C84.5684 41.5795 84.946 41.7615 84.5528 42.284L82.4731 45.0431C82.4731 45.0431 81.5277 44.9846 80.1884 44.5481Z" fill="#E2A60B"/>
|
||||
<ellipse cx="65.7707" cy="11.0296" rx="4.54417" ry="4.305" transform="rotate(-96.9176 65.7707 11.0296)" fill="#CE8C0C"/>
|
||||
<circle cx="65.0584" cy="11.116" r="4.54417" transform="rotate(-96.9176 65.0584 11.116)" fill="url(#paint7_linear_131_5)"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M68.3507 10.7166C68.1301 8.89827 66.4772 7.60309 64.659 7.8237C62.8407 8.0443 61.5455 9.69715 61.7661 11.5154C61.9867 13.3337 63.6395 14.6289 65.4578 14.4083C67.2761 14.1877 68.5713 12.5348 68.3507 10.7166ZM68.8572 10.6551C68.6027 8.55708 66.6955 7.06265 64.5975 7.31719C62.4995 7.57173 61.005 9.47887 61.2596 11.5769C61.5141 13.6749 63.4213 15.1694 65.5193 14.9148C67.6173 14.6603 69.1117 12.7531 68.8572 10.6551Z" fill="#E2AB1A"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M64.659 7.8237C62.3797 8.10024 61.7428 10.126 61.5319 10.6444C61.321 11.1628 61.2596 11.5769 61.2596 11.5769C61.0051 9.47887 62.4995 7.57174 64.5975 7.3172C66.6955 7.06265 68.6027 8.55709 68.8572 10.6551C68.8572 10.6551 68.8398 10.5286 68.4964 9.79945C68.1531 9.07029 66.4773 7.6031 64.659 7.8237Z" fill="#D89C00"/>
|
||||
<path d="M66.7416 11.0892C66.7959 11.5365 66.732 11.9811 66.5254 12.3921C66.3815 12.6786 66.021 13.0336 65.3469 13.1154C64.0963 13.2672 63.5591 12.657 63.3968 11.3197L63.1637 9.39805C63.1637 9.39805 63.1536 9.33921 63.1705 9.32013C63.1917 9.29611 63.2506 9.31041 63.2506 9.31041L63.805 9.42665C63.805 9.42665 63.8574 9.44458 63.8765 9.46747C63.899 9.49427 63.9039 9.54164 63.9039 9.54164L64.0869 11.0498C64.194 11.9325 64.3237 12.4493 65.2524 12.3366C65.5592 12.2994 65.7666 12.1828 65.8863 11.9801C66.0887 11.6374 66.025 11.0269 65.9454 10.6588C65.9454 10.6588 65.4554 10.5633 65.3758 10.5474C65.2825 10.5288 65.2626 10.5443 65.2738 10.6362C65.2899 10.769 65.3189 11.0077 65.3496 11.261C65.3711 11.4386 65.3065 11.6234 65.1005 11.6484C64.9015 11.6725 64.7664 11.5243 64.7394 11.3018C64.6927 10.9167 64.5964 10.1234 64.5784 9.97493C64.5548 9.78029 64.5575 9.5784 64.8656 9.64386L66.4932 9.991C66.4932 9.991 66.6599 10.4157 66.7416 11.0892Z" fill="#E2A60B"/>
|
||||
<defs>
|
||||
<linearGradient id="paint0_linear_131_5" x1="43.0658" y1="79.4423" x2="43.0658" y2="89.4349" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#FEDC28"/>
|
||||
<stop offset="1" stop-color="#F1C34B"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="paint1_linear_131_5" x1="56.1492" y1="47.5201" x2="56.1492" y2="62.7784" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#FEDC28"/>
|
||||
<stop offset="1" stop-color="#F1C34B"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="paint2_linear_131_5" x1="72.4056" y1="80.4599" x2="72.4056" y2="91.9883" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#FEDC28"/>
|
||||
<stop offset="1" stop-color="#F1C34B"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="paint3_linear_131_5" x1="84.9618" y1="60.1264" x2="84.9618" y2="73.7972" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#FEDC28"/>
|
||||
<stop offset="1" stop-color="#F1C34B"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="paint4_linear_131_5" x1="39" y1="11" x2="39" y2="25" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#FEDC28"/>
|
||||
<stop offset="1" stop-color="#F1C34B"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="paint5_linear_131_5" x1="21.0403" y1="3.01938" x2="21.0403" y2="10.8058" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#FEDC28"/>
|
||||
<stop offset="1" stop-color="#F1C34B"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="paint6_linear_131_5" x1="81.6132" y1="31.9224" x2="81.6132" y2="50.7909" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#FEDC28"/>
|
||||
<stop offset="1" stop-color="#F1C34B"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="paint7_linear_131_5" x1="65.0584" y1="6.57184" x2="65.0584" y2="15.6602" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#FEDC28"/>
|
||||
<stop offset="1" stop-color="#F1C34B"/>
|
||||
</linearGradient>
|
||||
<clipPath id="clip0_131_5">
|
||||
<rect width="24" height="24" fill="white" transform="translate(2 41)"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
After Width: | Height: | Size: 26 KiB |
@@ -356,6 +356,55 @@
|
||||
"badge-color": {
|
||||
"value": "#6BAFFF"
|
||||
}
|
||||
},
|
||||
"text-field": {
|
||||
"active": {
|
||||
"error": {
|
||||
"value": "#FFC2B4",
|
||||
"comment": "Palette/Red/20"
|
||||
},
|
||||
"border-color": {
|
||||
"value": "#DDD1FF",
|
||||
"comment": "Palette/Purple/10"
|
||||
},
|
||||
"caret-color": {
|
||||
"value": "#C9B6FC",
|
||||
"comment": "Palette/Purple/20"
|
||||
}
|
||||
},
|
||||
"disabled": {
|
||||
"color": {
|
||||
"value": "#7F7F85",
|
||||
"comment": "Palette/Grey/50"
|
||||
}
|
||||
},
|
||||
"placeholder": {
|
||||
"color": {
|
||||
"value": "#A6A6AB",
|
||||
"comment": "Palette/Grey/40"
|
||||
}
|
||||
},
|
||||
"default": {
|
||||
"background-color": {
|
||||
"value": "#32323B",
|
||||
"comment": "Palette/Grey/80"
|
||||
},
|
||||
"error": {
|
||||
"value": "#FF8569",
|
||||
"comment": "Palette/Red/40"
|
||||
}
|
||||
},
|
||||
"hover": {
|
||||
"border-color": {
|
||||
"value": "#7F7F85",
|
||||
"comment": "Palette/Grey/50"
|
||||
}
|
||||
},
|
||||
"endAdornmentIcon": {
|
||||
"background-color": {
|
||||
"value": "rgba(221, 209, 255, 0.16)"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"input": {
|
||||
|
@@ -13,7 +13,10 @@ import {
|
||||
isPixelArt,
|
||||
isPrivateAsset,
|
||||
} from '../Utils/GDevelopServices/Asset';
|
||||
import { type PrivateAssetPackListingData } from '../Utils/GDevelopServices/Shop';
|
||||
import {
|
||||
type PrivateAssetPackListingData,
|
||||
type PrivateGameTemplateListingData,
|
||||
} from '../Utils/GDevelopServices/Shop';
|
||||
import PlaceholderLoader from '../UI/PlaceholderLoader';
|
||||
import PlaceholderError from '../UI/PlaceholderError';
|
||||
import { LineStackLayout, ResponsiveLineStackLayout } from '../UI/Layout';
|
||||
@@ -84,6 +87,9 @@ type Props = {|
|
||||
onOpenDetails: (assetShortHeader: AssetShortHeader) => void,
|
||||
onAssetLoaded?: () => void,
|
||||
onPrivateAssetPackSelection: (assetPack: PrivateAssetPackListingData) => void,
|
||||
onPrivateGameTemplateSelection: (
|
||||
assetPack: PrivateGameTemplateListingData
|
||||
) => void,
|
||||
|};
|
||||
|
||||
const getObjectAssetResourcesByName = (
|
||||
@@ -111,6 +117,7 @@ export const AssetDetails = React.forwardRef<Props, AssetDetailsInterface>(
|
||||
onOpenDetails,
|
||||
onAssetLoaded,
|
||||
onPrivateAssetPackSelection,
|
||||
onPrivateGameTemplateSelection,
|
||||
}: Props,
|
||||
ref
|
||||
) => {
|
||||
@@ -574,6 +581,10 @@ export const AssetDetails = React.forwardRef<Props, AssetDetailsInterface>(
|
||||
onPrivateAssetPackSelection(assetPack);
|
||||
setSelectedAuthorPublicProfile(null);
|
||||
}}
|
||||
onGameTemplateOpen={gameTemplate => {
|
||||
onPrivateGameTemplateSelection(gameTemplate);
|
||||
setSelectedAuthorPublicProfile(null);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</Column>
|
||||
|
@@ -28,6 +28,10 @@ import {
|
||||
import { useDebounce } from '../Utils/UseDebounce';
|
||||
import PromotionsSlideshow from '../Promotions/PromotionsSlideshow';
|
||||
import { ColumnStackLayout } from '../UI/Layout';
|
||||
import {
|
||||
GithubStarCard,
|
||||
shouldDisplayGithubStarCard,
|
||||
} from '../Profile/GithubStarCard';
|
||||
|
||||
const cellSpacing = 2;
|
||||
|
||||
@@ -160,6 +164,7 @@ type Props = {|
|
||||
openedShopCategory: string | null,
|
||||
hideGameTemplates?: boolean,
|
||||
displayPromotions?: boolean,
|
||||
onOpenProfile?: () => void,
|
||||
|};
|
||||
|
||||
export const AssetsHome = React.forwardRef<Props, AssetsHomeInterface>(
|
||||
@@ -175,13 +180,17 @@ export const AssetsHome = React.forwardRef<Props, AssetsHomeInterface>(
|
||||
openedShopCategory,
|
||||
hideGameTemplates,
|
||||
displayPromotions,
|
||||
onOpenProfile,
|
||||
}: Props,
|
||||
ref
|
||||
) => {
|
||||
const { windowSize, isLandscape } = useResponsiveWindowSize();
|
||||
const { receivedAssetPacks, receivedGameTemplates } = React.useContext(
|
||||
AuthenticatedUserContext
|
||||
);
|
||||
const {
|
||||
receivedAssetPacks,
|
||||
receivedGameTemplates,
|
||||
badges,
|
||||
achievements,
|
||||
} = React.useContext(AuthenticatedUserContext);
|
||||
|
||||
const scrollView = React.useRef<?ScrollViewInterface>(null);
|
||||
React.useImperativeHandle(ref, () => ({
|
||||
@@ -394,6 +403,12 @@ export const AssetsHome = React.forwardRef<Props, AssetsHomeInterface>(
|
||||
</Text>
|
||||
|
||||
<PromotionsSlideshow />
|
||||
{onOpenProfile && shouldDisplayGithubStarCard({ badges }) && (
|
||||
<GithubStarCard
|
||||
achievements={achievements}
|
||||
onOpenProfile={onOpenProfile}
|
||||
/>
|
||||
)}
|
||||
</ColumnStackLayout>
|
||||
) : null}
|
||||
{allBundleTiles.length ? (
|
||||
|
@@ -966,6 +966,7 @@ const AssetsList = React.forwardRef<Props, AssetsListInterface>(
|
||||
!isAssetPackAudioOnly(openedAssetPack)) &&
|
||||
noResultComponent}
|
||||
{onPrivateAssetPackSelection &&
|
||||
onPrivateGameTemplateSelection &&
|
||||
openAuthorPublicProfileDialog &&
|
||||
authorPublicProfile && (
|
||||
<PublicProfileDialog
|
||||
@@ -976,6 +977,11 @@ const AssetsList = React.forwardRef<Props, AssetsListInterface>(
|
||||
setOpenAuthorPublicProfileDialog(false);
|
||||
setAuthorPublicProfile(null);
|
||||
}}
|
||||
onGameTemplateOpen={gameTemplateListingData => {
|
||||
onPrivateGameTemplateSelection(gameTemplateListingData);
|
||||
setOpenAuthorPublicProfileDialog(false);
|
||||
setAuthorPublicProfile(null);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
||||
|
@@ -5,6 +5,7 @@ import {
|
||||
buyProductWithCredits,
|
||||
redeemPrivateAssetPack,
|
||||
type PrivateAssetPackListingData,
|
||||
type PrivateGameTemplateListingData,
|
||||
getCalloutToGetSubscriptionOrClaimAssetPack,
|
||||
} from '../../Utils/GDevelopServices/Shop';
|
||||
import {
|
||||
@@ -60,6 +61,7 @@ import useAlertDialog from '../../UI/Alert/useAlertDialog';
|
||||
import PasswordPromptDialog from '../PasswordPromptDialog';
|
||||
import Window from '../../Utils/Window';
|
||||
import RaisedButton from '../../UI/RaisedButton';
|
||||
import PrivateAssetPackPurchaseDialog from './PrivateAssetPackPurchaseDialog';
|
||||
|
||||
const cellSpacing = 8;
|
||||
|
||||
@@ -136,21 +138,26 @@ const styles = {
|
||||
type Props = {|
|
||||
privateAssetPackListingData: PrivateAssetPackListingData,
|
||||
privateAssetPackListingDatasFromSameCreator?: ?Array<PrivateAssetPackListingData>,
|
||||
onOpenPurchaseDialog: () => void,
|
||||
onAssetPackOpen: (
|
||||
privateAssetPackListingData: PrivateAssetPackListingData,
|
||||
options?: {|
|
||||
forceProductPage?: boolean,
|
||||
|}
|
||||
) => void,
|
||||
onGameTemplateOpen: (
|
||||
privateGameTemplateListingData: PrivateGameTemplateListingData,
|
||||
options?: {|
|
||||
forceProductPage?: boolean,
|
||||
|}
|
||||
) => void,
|
||||
simulateAppStoreProduct?: boolean,
|
||||
|};
|
||||
|
||||
const PrivateAssetPackInformationPage = ({
|
||||
privateAssetPackListingData,
|
||||
privateAssetPackListingDatasFromSameCreator,
|
||||
onOpenPurchaseDialog,
|
||||
onAssetPackOpen,
|
||||
onGameTemplateOpen,
|
||||
simulateAppStoreProduct,
|
||||
}: Props) => {
|
||||
const { id, name, sellerId } = privateAssetPackListingData;
|
||||
@@ -173,6 +180,10 @@ const PrivateAssetPackInformationPage = ({
|
||||
const [selectedUsageType, setSelectedUsageType] = React.useState<string>(
|
||||
privateAssetPackListingData.prices[0].usageType
|
||||
);
|
||||
const [
|
||||
purchasingPrivateAssetPackListingData,
|
||||
setPurchasingPrivateAssetPackListingData,
|
||||
] = React.useState<?PrivateAssetPackListingData>(null);
|
||||
const { openSubscriptionDialog } = React.useContext(
|
||||
SubscriptionSuggestionContext
|
||||
);
|
||||
@@ -391,14 +402,13 @@ const PrivateAssetPackInformationPage = ({
|
||||
currency: price ? price.currency : undefined,
|
||||
});
|
||||
|
||||
onOpenPurchaseDialog();
|
||||
setPurchasingPrivateAssetPackListingData(privateAssetPackListingData);
|
||||
} catch (e) {
|
||||
console.warn('Unable to send event', e);
|
||||
}
|
||||
},
|
||||
[
|
||||
assetPack,
|
||||
onOpenPurchaseDialog,
|
||||
privateAssetPackListingData,
|
||||
isAlreadyReceived,
|
||||
onAssetPackOpen,
|
||||
@@ -736,6 +746,10 @@ const PrivateAssetPackInformationPage = ({
|
||||
onAssetPackOpen(assetPackListingData);
|
||||
setOpenSellerPublicProfileDialog(false);
|
||||
}}
|
||||
onGameTemplateOpen={gameTemplateListingData => {
|
||||
onGameTemplateOpen(gameTemplateListingData);
|
||||
setOpenSellerPublicProfileDialog(false);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
{displayPasswordPrompt && (
|
||||
@@ -746,6 +760,15 @@ const PrivateAssetPackInformationPage = ({
|
||||
setPasswordValue={setPassword}
|
||||
/>
|
||||
)}
|
||||
{!!purchasingPrivateAssetPackListingData && (
|
||||
<PrivateAssetPackPurchaseDialog
|
||||
privateAssetPackListingData={
|
||||
purchasingPrivateAssetPackListingData
|
||||
}
|
||||
usageType={selectedUsageType}
|
||||
onClose={() => setPurchasingPrivateAssetPackListingData(null)}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</I18n>
|
||||
|
@@ -1,10 +1,7 @@
|
||||
// @flow
|
||||
import * as React from 'react';
|
||||
import { t, Trans } from '@lingui/macro';
|
||||
import {
|
||||
listUserPurchases,
|
||||
type PrivateAssetPackListingData,
|
||||
} from '../../Utils/GDevelopServices/Shop';
|
||||
import { type PrivateAssetPackListingData } from '../../Utils/GDevelopServices/Shop';
|
||||
import Dialog, { DialogPrimaryButton } from '../../UI/Dialog';
|
||||
import AuthenticatedUserContext from '../../Profile/AuthenticatedUserContext';
|
||||
import CreateProfile from '../../Profile/CreateProfile';
|
||||
@@ -28,22 +25,25 @@ import PasswordPromptDialog from '../PasswordPromptDialog';
|
||||
|
||||
type Props = {|
|
||||
privateAssetPackListingData: PrivateAssetPackListingData,
|
||||
usageType: string,
|
||||
onClose: () => void,
|
||||
simulateAppStoreProduct?: boolean,
|
||||
|};
|
||||
|
||||
const PrivateAssetPackPurchaseDialog = ({
|
||||
privateAssetPackListingData,
|
||||
usageType,
|
||||
onClose,
|
||||
simulateAppStoreProduct,
|
||||
}: Props) => {
|
||||
const {
|
||||
profile,
|
||||
getAuthorizationHeader,
|
||||
onOpenLoginDialog,
|
||||
onOpenCreateAccountDialog,
|
||||
receivedAssetPacks,
|
||||
onPurchaseSuccessful,
|
||||
onRefreshAssetPackPurchases,
|
||||
assetPackPurchases,
|
||||
} = React.useContext(AuthenticatedUserContext);
|
||||
const [isPurchasing, setIsPurchasing] = React.useState(false);
|
||||
const [
|
||||
@@ -78,12 +78,24 @@ const PrivateAssetPackPurchaseDialog = ({
|
||||
return;
|
||||
}
|
||||
|
||||
const price = privateAssetPackListingData.prices.find(
|
||||
price => price.usageType === usageType
|
||||
);
|
||||
if (!price) {
|
||||
console.error('Unable to find the price for the usage type', usageType);
|
||||
await showAlert({
|
||||
title: t`An error happened`,
|
||||
message: t`Unable to find the price for this asset pack. Please try again later.`,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// Purchase with web.
|
||||
try {
|
||||
setIsPurchasing(true);
|
||||
const checkoutUrl = getPurchaseCheckoutUrl({
|
||||
productId: privateAssetPackListingData.id,
|
||||
priceName: privateAssetPackListingData.prices[0].name,
|
||||
priceName: price.name,
|
||||
userId: profile.id,
|
||||
userEmail: profile.email,
|
||||
...(password ? { password } : undefined),
|
||||
@@ -130,45 +142,36 @@ const PrivateAssetPackPurchaseDialog = ({
|
||||
[]
|
||||
);
|
||||
|
||||
const checkUserPurchases = React.useCallback(
|
||||
async () => {
|
||||
if (!profile) return;
|
||||
try {
|
||||
const userPurchases = await listUserPurchases(getAuthorizationHeader, {
|
||||
userId: profile.id,
|
||||
productType: 'asset-pack',
|
||||
role: 'receiver',
|
||||
});
|
||||
React.useEffect(
|
||||
() => {
|
||||
const checkIfPurchaseIsDone = async () => {
|
||||
if (
|
||||
userPurchases.find(
|
||||
isPurchasing &&
|
||||
assetPackPurchases &&
|
||||
assetPackPurchases.find(
|
||||
userPurchase =>
|
||||
userPurchase.productId === privateAssetPackListingData.id
|
||||
)
|
||||
) {
|
||||
// We found the purchase, the user has bought the asset pack.
|
||||
// We do not close the dialog yet, as we need to trigger a refresh of the asset store.
|
||||
// We do not close the dialog yet, as we need to trigger a refresh of the products received.
|
||||
await onPurchaseSuccessful();
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Unable to get the user purchases', error);
|
||||
await showAlert({
|
||||
title: t`An error happened`,
|
||||
message: t`An error happened while checking if your purchase was successful. If you have completed the payment, close and re-open the store to see your asset pack!`,
|
||||
});
|
||||
}
|
||||
};
|
||||
checkIfPurchaseIsDone();
|
||||
},
|
||||
[
|
||||
profile,
|
||||
getAuthorizationHeader,
|
||||
isPurchasing,
|
||||
assetPackPurchases,
|
||||
privateAssetPackListingData,
|
||||
onPurchaseSuccessful,
|
||||
showAlert,
|
||||
onRefreshAssetPackPurchases,
|
||||
]
|
||||
);
|
||||
|
||||
useInterval(
|
||||
() => {
|
||||
checkUserPurchases();
|
||||
onRefreshAssetPackPurchases();
|
||||
},
|
||||
isPurchasing ? 3900 : null
|
||||
);
|
||||
|
@@ -9,8 +9,6 @@ import PrivateGameTemplateInformationPage from './PrivateGameTemplateInformation
|
||||
type Props = {|
|
||||
privateGameTemplateListingData: PrivateGameTemplateListingData,
|
||||
privateGameTemplateListingDatasFromSameCreator: ?Array<PrivateGameTemplateListingData>,
|
||||
onOpenPurchaseDialog: () => void,
|
||||
isPurchaseDialogOpen: boolean,
|
||||
onGameTemplateOpen: PrivateGameTemplateListingData => void,
|
||||
onCreateWithGameTemplate: PrivateGameTemplateListingData => void,
|
||||
onClose: () => void,
|
||||
@@ -19,8 +17,6 @@ type Props = {|
|
||||
const PrivateGameTemplateInformationDialog = ({
|
||||
privateGameTemplateListingData,
|
||||
privateGameTemplateListingDatasFromSameCreator,
|
||||
onOpenPurchaseDialog,
|
||||
isPurchaseDialogOpen,
|
||||
onGameTemplateOpen,
|
||||
onCreateWithGameTemplate,
|
||||
onClose,
|
||||
@@ -34,7 +30,6 @@ const PrivateGameTemplateInformationDialog = ({
|
||||
label={<Trans>Back</Trans>}
|
||||
primary={false}
|
||||
onClick={onClose}
|
||||
disabled={isPurchaseDialogOpen}
|
||||
/>,
|
||||
]}
|
||||
open
|
||||
@@ -47,7 +42,6 @@ const PrivateGameTemplateInformationDialog = ({
|
||||
privateGameTemplateListingDatasFromSameCreator={
|
||||
privateGameTemplateListingDatasFromSameCreator
|
||||
}
|
||||
onOpenPurchaseDialog={onOpenPurchaseDialog}
|
||||
onGameTemplateOpen={onGameTemplateOpen}
|
||||
onCreateWithGameTemplate={onCreateWithGameTemplate}
|
||||
/>
|
||||
|
@@ -4,6 +4,7 @@ import { I18n } from '@lingui/react';
|
||||
import {
|
||||
buyProductWithCredits,
|
||||
type PrivateGameTemplateListingData,
|
||||
type PrivateAssetPackListingData,
|
||||
} from '../../Utils/GDevelopServices/Shop';
|
||||
import {
|
||||
getPrivateGameTemplate,
|
||||
@@ -56,6 +57,7 @@ import { CreditsPackageStoreContext } from '../CreditsPackages/CreditsPackageSto
|
||||
import GDevelopThemeContext from '../../UI/Theme/GDevelopThemeContext';
|
||||
import RaisedButton from '../../UI/RaisedButton';
|
||||
import Play from '../../UI/CustomSvgIcons/Play';
|
||||
import PrivateGameTemplatePurchaseDialog from './PrivateGameTemplatePurchaseDialog';
|
||||
|
||||
const cellSpacing = 8;
|
||||
|
||||
@@ -108,8 +110,8 @@ const styles = {
|
||||
type Props = {|
|
||||
privateGameTemplateListingData: PrivateGameTemplateListingData,
|
||||
privateGameTemplateListingDatasFromSameCreator?: ?Array<PrivateGameTemplateListingData>,
|
||||
onOpenPurchaseDialog: () => void,
|
||||
onGameTemplateOpen: PrivateGameTemplateListingData => void,
|
||||
onAssetPackOpen?: PrivateAssetPackListingData => void,
|
||||
onCreateWithGameTemplate: PrivateGameTemplateListingData => void,
|
||||
simulateAppStoreProduct?: boolean,
|
||||
|};
|
||||
@@ -117,8 +119,8 @@ type Props = {|
|
||||
const PrivateGameTemplateInformationPage = ({
|
||||
privateGameTemplateListingData,
|
||||
privateGameTemplateListingDatasFromSameCreator,
|
||||
onOpenPurchaseDialog,
|
||||
onGameTemplateOpen,
|
||||
onAssetPackOpen,
|
||||
onCreateWithGameTemplate,
|
||||
simulateAppStoreProduct,
|
||||
}: Props) => {
|
||||
@@ -143,6 +145,10 @@ const PrivateGameTemplateInformationPage = ({
|
||||
const [selectedUsageType, setSelectedUsageType] = React.useState<string>(
|
||||
privateGameTemplateListingData.prices[0].usageType
|
||||
);
|
||||
const [
|
||||
purchasingPrivateGameTemplateListingData,
|
||||
setPurchasingPrivateGameTemplateListingData,
|
||||
] = React.useState<?PrivateGameTemplateListingData>(null);
|
||||
const [isFetching, setIsFetching] = React.useState<boolean>(false);
|
||||
const [
|
||||
openSellerPublicProfileDialog,
|
||||
@@ -285,14 +291,15 @@ const PrivateGameTemplateInformationPage = ({
|
||||
usageType: selectedUsageType,
|
||||
});
|
||||
|
||||
onOpenPurchaseDialog();
|
||||
setPurchasingPrivateGameTemplateListingData(
|
||||
privateGameTemplateListingData
|
||||
);
|
||||
} catch (e) {
|
||||
console.warn('Unable to send event', e);
|
||||
}
|
||||
},
|
||||
[
|
||||
gameTemplate,
|
||||
onOpenPurchaseDialog,
|
||||
privateGameTemplateListingData,
|
||||
isAlreadyReceived,
|
||||
onCreateWithGameTemplate,
|
||||
@@ -590,6 +597,31 @@ const PrivateGameTemplateInformationPage = ({
|
||||
<PublicProfileDialog
|
||||
userId={sellerId}
|
||||
onClose={() => setOpenSellerPublicProfileDialog(false)}
|
||||
onGameTemplateOpen={
|
||||
onGameTemplateOpen
|
||||
? (gameTemplate: PrivateGameTemplateListingData) => {
|
||||
setOpenSellerPublicProfileDialog(false);
|
||||
onGameTemplateOpen(gameTemplate);
|
||||
}
|
||||
: undefined
|
||||
}
|
||||
onAssetPackOpen={
|
||||
onAssetPackOpen
|
||||
? (assetPack: PrivateAssetPackListingData) => {
|
||||
setOpenSellerPublicProfileDialog(false);
|
||||
onAssetPackOpen(assetPack);
|
||||
}
|
||||
: undefined
|
||||
}
|
||||
/>
|
||||
)}
|
||||
{!!purchasingPrivateGameTemplateListingData && (
|
||||
<PrivateGameTemplatePurchaseDialog
|
||||
privateGameTemplateListingData={
|
||||
purchasingPrivateGameTemplateListingData
|
||||
}
|
||||
usageType={selectedUsageType}
|
||||
onClose={() => setPurchasingPrivateGameTemplateListingData(null)}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
|
@@ -25,12 +25,14 @@ import PasswordPromptDialog from '../PasswordPromptDialog';
|
||||
|
||||
type Props = {|
|
||||
privateGameTemplateListingData: PrivateGameTemplateListingData,
|
||||
usageType: string,
|
||||
onClose: () => void,
|
||||
simulateAppStoreProduct?: boolean,
|
||||
|};
|
||||
|
||||
const PrivateGameTemplatePurchaseDialog = ({
|
||||
privateGameTemplateListingData,
|
||||
usageType,
|
||||
onClose,
|
||||
simulateAppStoreProduct,
|
||||
}: Props) => {
|
||||
@@ -76,12 +78,24 @@ const PrivateGameTemplatePurchaseDialog = ({
|
||||
return;
|
||||
}
|
||||
|
||||
const price = privateGameTemplateListingData.prices.find(
|
||||
price => price.usageType === usageType
|
||||
);
|
||||
if (!price) {
|
||||
console.error('Unable to find the price for the usage type', usageType);
|
||||
await showAlert({
|
||||
title: t`An error happened`,
|
||||
message: t`Unable to find the price for this game template. Please try again later.`,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// Purchase with web.
|
||||
try {
|
||||
setIsPurchasing(true);
|
||||
const checkoutUrl = getPurchaseCheckoutUrl({
|
||||
productId: privateGameTemplateListingData.id,
|
||||
priceName: privateGameTemplateListingData.prices[0].name,
|
||||
priceName: price.name,
|
||||
userId: profile.id,
|
||||
userEmail: profile.email,
|
||||
...(password ? { password } : undefined),
|
||||
@@ -153,6 +167,7 @@ const PrivateGameTemplatePurchaseDialog = ({
|
||||
gameTemplatePurchases,
|
||||
privateGameTemplateListingData,
|
||||
onPurchaseSuccessful,
|
||||
onRefreshGameTemplatePurchases,
|
||||
]
|
||||
);
|
||||
|
||||
|
@@ -0,0 +1,172 @@
|
||||
// @flow
|
||||
import * as React from 'react';
|
||||
import { I18n } from '@lingui/react';
|
||||
import { Trans } from '@lingui/macro';
|
||||
import {
|
||||
canAccessTutorial,
|
||||
type Tutorial,
|
||||
} from '../../Utils/GDevelopServices/Tutorial';
|
||||
import Dialog from '../../UI/Dialog';
|
||||
import FlatButton from '../../UI/FlatButton';
|
||||
import { selectMessageByLocale } from '../../Utils/i18n/MessageByLocale';
|
||||
import Text from '../../UI/Text';
|
||||
import AuthenticatedUserContext from '../../Profile/AuthenticatedUserContext';
|
||||
import { ColumnStackLayout } from '../../UI/Layout';
|
||||
import { Line } from '../../UI/Grid';
|
||||
import RaisedButton from '../../UI/RaisedButton';
|
||||
import Window from '../../Utils/Window';
|
||||
import AlertMessage from '../../UI/AlertMessage';
|
||||
import {
|
||||
type PrivatePdfTutorial,
|
||||
getPrivatePdfTutorial,
|
||||
} from '../../Utils/GDevelopServices/Asset';
|
||||
import PlaceholderError from '../../UI/PlaceholderError';
|
||||
import PlaceholderLoader from '../../UI/PlaceholderLoader';
|
||||
import { sendTutorialOpened } from '../../Utils/Analytics/EventSender';
|
||||
import Download from '../../UI/CustomSvgIcons/Download';
|
||||
import LockOpen from '../../UI/CustomSvgIcons/LockOpen';
|
||||
|
||||
type Props = {|
|
||||
tutorial: Tutorial,
|
||||
onClose: () => void,
|
||||
|};
|
||||
|
||||
export const PrivateTutorialViewDialog = ({ tutorial, onClose }: Props) => {
|
||||
const { limits, getAuthorizationHeader, profile } = React.useContext(
|
||||
AuthenticatedUserContext
|
||||
);
|
||||
const [error, setError] = React.useState<Error | null>(null);
|
||||
const [
|
||||
pdfTutorial,
|
||||
setPdfTutorial,
|
||||
] = React.useState<PrivatePdfTutorial | null>(null);
|
||||
const isLocked = !canAccessTutorial(
|
||||
tutorial,
|
||||
limits ? limits.capabilities : null
|
||||
);
|
||||
|
||||
const fetchTutorial = React.useCallback(
|
||||
async () => {
|
||||
if (!profile) return;
|
||||
if (!tutorial.isPrivateTutorial || tutorial.type !== 'pdf-tutorial') {
|
||||
console.error(
|
||||
'PrivateTutorialViewDialog is used for an unsupported tutorial type.'
|
||||
);
|
||||
}
|
||||
|
||||
setError(null);
|
||||
try {
|
||||
const pdfTutorial = await getPrivatePdfTutorial(
|
||||
getAuthorizationHeader,
|
||||
{
|
||||
userId: profile.id,
|
||||
tutorialId: tutorial.id,
|
||||
}
|
||||
);
|
||||
setPdfTutorial(pdfTutorial);
|
||||
} catch (error) {
|
||||
console.error(
|
||||
'An error occurred while fetching the PDF tutorial:',
|
||||
error
|
||||
);
|
||||
setError(error);
|
||||
}
|
||||
},
|
||||
[getAuthorizationHeader, profile, tutorial]
|
||||
);
|
||||
|
||||
React.useEffect(
|
||||
() => {
|
||||
fetchTutorial();
|
||||
},
|
||||
[fetchTutorial]
|
||||
);
|
||||
|
||||
return (
|
||||
<I18n>
|
||||
{({ i18n }) => (
|
||||
<Dialog
|
||||
title={selectMessageByLocale(i18n, tutorial.titleByLocale)}
|
||||
open
|
||||
maxWidth="md"
|
||||
actions={[
|
||||
<FlatButton
|
||||
key="close"
|
||||
label={<Trans>Close</Trans>}
|
||||
primary={false}
|
||||
onClick={onClose}
|
||||
/>,
|
||||
]}
|
||||
flexColumnBody
|
||||
>
|
||||
<ColumnStackLayout noMargin expand>
|
||||
<Text>
|
||||
{selectMessageByLocale(i18n, tutorial.descriptionByLocale)}
|
||||
</Text>
|
||||
{isLocked ? (
|
||||
<ColumnStackLayout noMargin>
|
||||
{tutorial.redeemLinkByLocale && (
|
||||
<Line noMargin justifyContent="center" alignItems="center">
|
||||
<RaisedButton
|
||||
icon={<LockOpen />}
|
||||
label={<Trans>Get access</Trans>}
|
||||
primary={true}
|
||||
onClick={() => {
|
||||
if (!tutorial.redeemLinkByLocale) return;
|
||||
|
||||
Window.openExternalURL(
|
||||
selectMessageByLocale(
|
||||
i18n,
|
||||
tutorial.redeemLinkByLocale
|
||||
)
|
||||
);
|
||||
}}
|
||||
/>
|
||||
</Line>
|
||||
)}
|
||||
<AlertMessage kind="info">
|
||||
{tutorial.redeemHintByLocale ? (
|
||||
selectMessageByLocale(i18n, tutorial.redeemHintByLocale)
|
||||
) : (
|
||||
<Trans>
|
||||
This tutorial must be unlocked to be accessed.
|
||||
</Trans>
|
||||
)}
|
||||
</AlertMessage>
|
||||
</ColumnStackLayout>
|
||||
) : pdfTutorial ? (
|
||||
<ColumnStackLayout noMargin>
|
||||
<Line noMargin justifyContent="center" alignItems="center">
|
||||
<RaisedButton
|
||||
icon={<Download />}
|
||||
label={<Trans>Download</Trans>}
|
||||
primary={true}
|
||||
onClick={() => {
|
||||
sendTutorialOpened(tutorial.id);
|
||||
Window.openExternalURL(pdfTutorial.downloadUrl);
|
||||
}}
|
||||
/>
|
||||
</Line>
|
||||
<AlertMessage kind="info">
|
||||
<Trans>
|
||||
Remember that your access to this resource is exclusive to
|
||||
your account.
|
||||
</Trans>
|
||||
</AlertMessage>
|
||||
</ColumnStackLayout>
|
||||
) : error ? (
|
||||
<PlaceholderError onRetry={fetchTutorial}>
|
||||
<Trans>
|
||||
Unable to load the tutorial. Please try again later or contact
|
||||
us if the problem persists.
|
||||
</Trans>
|
||||
</PlaceholderError>
|
||||
) : (
|
||||
<PlaceholderLoader />
|
||||
)}
|
||||
</ColumnStackLayout>
|
||||
</Dialog>
|
||||
)}
|
||||
</I18n>
|
||||
);
|
||||
};
|
@@ -41,7 +41,6 @@ import PrivateAssetPackInformationPage from './PrivateAssets/PrivateAssetPackInf
|
||||
import PlaceholderError from '../UI/PlaceholderError';
|
||||
import AlertMessage from '../UI/AlertMessage';
|
||||
import AuthenticatedUserContext from '../Profile/AuthenticatedUserContext';
|
||||
import PrivateAssetPackPurchaseDialog from './PrivateAssets/PrivateAssetPackPurchaseDialog';
|
||||
import { LineStackLayout } from '../UI/Layout';
|
||||
import {
|
||||
isHomePage,
|
||||
@@ -54,7 +53,6 @@ import PrivateAssetPackAudioFilesDownloadButton from './PrivateAssets/PrivateAss
|
||||
import Text from '../UI/Text';
|
||||
import { capitalize } from 'lodash';
|
||||
import PrivateGameTemplateInformationPage from './PrivateGameTemplates/PrivateGameTemplateInformationPage';
|
||||
import PrivateGameTemplatePurchaseDialog from './PrivateGameTemplates/PrivateGameTemplatePurchaseDialog';
|
||||
import { PrivateGameTemplateStoreContext } from './PrivateGameTemplates/PrivateGameTemplateStoreContext';
|
||||
|
||||
type Props = {|
|
||||
@@ -63,6 +61,7 @@ type Props = {|
|
||||
onOpenPrivateGameTemplateListingData?: (
|
||||
privateGameTemplateListingData: PrivateGameTemplateListingData
|
||||
) => void,
|
||||
onOpenProfile?: () => void,
|
||||
|};
|
||||
|
||||
export type AssetStoreInterface = {|
|
||||
@@ -100,6 +99,7 @@ export const AssetStore = React.forwardRef<Props, AssetStoreInterface>(
|
||||
hideGameTemplates,
|
||||
displayPromotions,
|
||||
onOpenPrivateGameTemplateListingData,
|
||||
onOpenProfile,
|
||||
}: Props,
|
||||
ref
|
||||
) => {
|
||||
@@ -155,14 +155,6 @@ export const AssetStore = React.forwardRef<Props, AssetStoreInterface>(
|
||||
[isMobile]
|
||||
);
|
||||
|
||||
const [
|
||||
purchasingPrivateAssetPackListingData,
|
||||
setPurchasingPrivateAssetPackListingData,
|
||||
] = React.useState<?PrivateAssetPackListingData>(null);
|
||||
const [
|
||||
purchasingPrivateGameTemplateListingData,
|
||||
setPurchasingPrivateGameTemplateListingData,
|
||||
] = React.useState<?PrivateGameTemplateListingData>(null);
|
||||
const { receivedAssetPacks, receivedGameTemplates } = React.useContext(
|
||||
AuthenticatedUserContext
|
||||
);
|
||||
@@ -452,44 +444,6 @@ export const AssetStore = React.forwardRef<Props, AssetStoreInterface>(
|
||||
[shopNavigationState, saveScrollPosition]
|
||||
);
|
||||
|
||||
// If the user has received the pack they are currently viewing,
|
||||
// we update the window to show it if they are not already on the pack page.
|
||||
React.useEffect(
|
||||
() => {
|
||||
if (!purchasingPrivateAssetPackListingData) return;
|
||||
// Ensure the user is not already on the pack page, to trigger the effect only once.
|
||||
const isOnPrivatePackPage =
|
||||
currentPage.openedAssetPack &&
|
||||
currentPage.openedAssetPack.id &&
|
||||
currentPage.openedAssetPack.id ===
|
||||
purchasingPrivateAssetPackListingData.id;
|
||||
if (receivedAssetPacks && !isOnPrivatePackPage) {
|
||||
const receivedAssetPack = receivedAssetPacks.find(
|
||||
pack => pack.id === purchasingPrivateAssetPackListingData.id
|
||||
);
|
||||
if (receivedAssetPack) {
|
||||
// The user has received the pack, close the pack information dialog, and open the pack in the search.
|
||||
setSearchText('');
|
||||
shopNavigationState.clearPreviousPageFromHistory(); // Clear the previous page from history to avoid going back to the pack information page.
|
||||
shopNavigationState.openPackPage({
|
||||
assetPack: receivedAssetPack,
|
||||
previousSearchText: '', // We were on a pack page.
|
||||
});
|
||||
openFiltersPanelIfAppropriate();
|
||||
}
|
||||
}
|
||||
},
|
||||
[
|
||||
receivedAssetPacks,
|
||||
purchasingPrivateAssetPackListingData,
|
||||
shopNavigationState,
|
||||
currentPage,
|
||||
saveScrollPosition,
|
||||
setSearchText,
|
||||
openFiltersPanelIfAppropriate,
|
||||
]
|
||||
);
|
||||
|
||||
// When a tag is selected from the asset details page,
|
||||
// first determine if it's a public or private pack,
|
||||
// then set it as the chosen category, clear old filters and open the filters panel.
|
||||
@@ -746,6 +700,7 @@ export const AssetStore = React.forwardRef<Props, AssetStoreInterface>(
|
||||
openedShopCategory={openedShopCategory}
|
||||
hideGameTemplates={hideGameTemplates}
|
||||
displayPromotions={displayPromotions}
|
||||
onOpenProfile={onOpenProfile}
|
||||
/>
|
||||
) : (
|
||||
<PlaceholderLoader />
|
||||
@@ -778,16 +733,13 @@ export const AssetStore = React.forwardRef<Props, AssetStoreInterface>(
|
||||
onOpenDetails={onOpenDetails}
|
||||
onAssetLoaded={() => applyBackScrollPosition(currentPage)}
|
||||
onPrivateAssetPackSelection={selectPrivateAssetPack}
|
||||
onPrivateGameTemplateSelection={selectPrivateGameTemplate}
|
||||
/>
|
||||
) : !!openedPrivateAssetPackListingData ? (
|
||||
<PrivateAssetPackInformationPage
|
||||
privateAssetPackListingData={openedPrivateAssetPackListingData}
|
||||
onOpenPurchaseDialog={() =>
|
||||
setPurchasingPrivateAssetPackListingData(
|
||||
openedPrivateAssetPackListingData
|
||||
)
|
||||
}
|
||||
onAssetPackOpen={selectPrivateAssetPack}
|
||||
onGameTemplateOpen={selectPrivateGameTemplate}
|
||||
privateAssetPackListingDatasFromSameCreator={
|
||||
privateAssetPackListingDatasFromSameCreator
|
||||
}
|
||||
@@ -797,39 +749,19 @@ export const AssetStore = React.forwardRef<Props, AssetStoreInterface>(
|
||||
privateGameTemplateListingData={
|
||||
openedPrivateGameTemplateListingData
|
||||
}
|
||||
onOpenPurchaseDialog={() =>
|
||||
setPurchasingPrivateGameTemplateListingData(
|
||||
openedPrivateGameTemplateListingData
|
||||
)
|
||||
}
|
||||
onCreateWithGameTemplate={() => {
|
||||
onOpenPrivateGameTemplateListingData &&
|
||||
onOpenPrivateGameTemplateListingData(
|
||||
openedPrivateGameTemplateListingData
|
||||
);
|
||||
}}
|
||||
onAssetPackOpen={selectPrivateAssetPack}
|
||||
onGameTemplateOpen={selectPrivateGameTemplate}
|
||||
privateGameTemplateListingDatasFromSameCreator={
|
||||
privateGameTemplateListingDatasFromSameCreator
|
||||
}
|
||||
/>
|
||||
) : null}
|
||||
{!!purchasingPrivateAssetPackListingData && (
|
||||
<PrivateAssetPackPurchaseDialog
|
||||
privateAssetPackListingData={
|
||||
purchasingPrivateAssetPackListingData
|
||||
}
|
||||
onClose={() => setPurchasingPrivateAssetPackListingData(null)}
|
||||
/>
|
||||
)}
|
||||
{!!purchasingPrivateGameTemplateListingData && (
|
||||
<PrivateGameTemplatePurchaseDialog
|
||||
privateGameTemplateListingData={
|
||||
purchasingPrivateGameTemplateListingData
|
||||
}
|
||||
onClose={() => setPurchasingPrivateGameTemplateListingData(null)}
|
||||
/>
|
||||
)}
|
||||
{canShowFiltersPanel && (
|
||||
<ResponsivePaperOrDrawer
|
||||
onClose={() => setIsFiltersPanelOpen(false)}
|
||||
|
@@ -0,0 +1,61 @@
|
||||
// @flow
|
||||
|
||||
import * as React from 'react';
|
||||
import Tooltip from '@material-ui/core/Tooltip';
|
||||
import { LineStackLayout } from '../UI/Layout';
|
||||
import Text from '../UI/Text';
|
||||
import { MarkdownText } from '../UI/MarkdownText';
|
||||
import { tooltipEnterDelay } from '../UI/Tooltip';
|
||||
|
||||
const styles = {
|
||||
leftColumn: { flex: 2, minWidth: 0, maxWidth: 150 },
|
||||
rightColumn: { flex: 3, minWidth: 75 },
|
||||
label: {
|
||||
overflow: 'hidden',
|
||||
textOverflow: 'ellipsis',
|
||||
lineHeight: '17px',
|
||||
maxHeight: 34, // 2 * lineHeight to limit to 2 lines.
|
||||
opacity: 0.7,
|
||||
},
|
||||
};
|
||||
type Props = {|
|
||||
label: string,
|
||||
markdownDescription?: ?string,
|
||||
field: React.Node,
|
||||
|};
|
||||
|
||||
const CompactPropertiesEditorRowField = (props: Props) => {
|
||||
const title = !props.markdownDescription
|
||||
? props.label
|
||||
: [props.label, ' - ', <MarkdownText source={props.markdownDescription} />];
|
||||
return (
|
||||
<LineStackLayout noMargin alignItems="center">
|
||||
<div style={styles.leftColumn}>
|
||||
<Tooltip
|
||||
title={title}
|
||||
enterDelay={tooltipEnterDelay}
|
||||
placement="bottom"
|
||||
PopperProps={{
|
||||
modifiers: {
|
||||
offset: {
|
||||
enabled: true,
|
||||
/**
|
||||
* It does not seem possible to get the tooltip closer to the anchor
|
||||
* when positioned on top. So it is positioned on bottom with a negative offset.
|
||||
*/
|
||||
offset: '0,-20',
|
||||
},
|
||||
},
|
||||
}}
|
||||
>
|
||||
<Text noMargin style={styles.label}>
|
||||
{props.label}
|
||||
</Text>
|
||||
</Tooltip>
|
||||
</div>
|
||||
<div style={styles.rightColumn}>{props.field}</div>
|
||||
</LineStackLayout>
|
||||
);
|
||||
};
|
||||
|
||||
export default CompactPropertiesEditorRowField;
|
@@ -0,0 +1,440 @@
|
||||
// @flow
|
||||
import * as React from 'react';
|
||||
import { mapFor } from '../Utils/MapFor';
|
||||
import { type Schema, type Instance } from '.';
|
||||
import { type ResourceKind } from '../ResourcesList/ResourceSource';
|
||||
import { type Field } from '.';
|
||||
import MeasurementUnitDocumentation from '../PropertiesEditor/MeasurementUnitDocumentation';
|
||||
|
||||
const createField = (
|
||||
name: string,
|
||||
property: gdPropertyDescriptor,
|
||||
getProperties: (instance: Instance) => any,
|
||||
onUpdateProperty: (
|
||||
instance: Instance,
|
||||
propertyName: string,
|
||||
newValue: string
|
||||
) => void,
|
||||
object: ?gdObject
|
||||
): ?Field => {
|
||||
const propertyDescription = property.getDescription();
|
||||
const getLabel = (instance: Instance) => {
|
||||
const propertyName = getProperties(instance)
|
||||
.get(name)
|
||||
.getLabel();
|
||||
if (propertyName) return propertyName;
|
||||
return (
|
||||
name.charAt(0).toUpperCase() +
|
||||
name
|
||||
.slice(1)
|
||||
.split(/(?=[A-Z])/)
|
||||
.join(' ')
|
||||
);
|
||||
};
|
||||
const getDescription = () => propertyDescription;
|
||||
const getEndAdornment = (instance: Instance) => {
|
||||
const property = getProperties(instance).get(name);
|
||||
const measurementUnit = property.getMeasurementUnit();
|
||||
return {
|
||||
label: getMeasurementUnitShortLabel(measurementUnit),
|
||||
tooltipContent: (
|
||||
<MeasurementUnitDocumentation
|
||||
label={measurementUnit.getLabel()}
|
||||
description={measurementUnit.getDescription()}
|
||||
elementsWithWords={measurementUnit.getElementsWithWords()}
|
||||
/>
|
||||
),
|
||||
};
|
||||
};
|
||||
|
||||
const valueType = property.getType().toLowerCase();
|
||||
if (valueType === 'number') {
|
||||
return {
|
||||
name,
|
||||
valueType,
|
||||
getValue: (instance: Instance): number => {
|
||||
return (
|
||||
parseFloat(
|
||||
getProperties(instance)
|
||||
.get(name)
|
||||
.getValue()
|
||||
) || 0
|
||||
); // Consider a missing value as 0 to avoid propagating NaN.
|
||||
},
|
||||
setValue: (instance: Instance, newValue: number) => {
|
||||
onUpdateProperty(instance, name, '' + newValue);
|
||||
},
|
||||
getLabel,
|
||||
getDescription,
|
||||
getEndAdornment,
|
||||
};
|
||||
} else if (valueType === 'string' || valueType === '') {
|
||||
return {
|
||||
name,
|
||||
valueType: 'string',
|
||||
getValue: (instance: Instance): string => {
|
||||
return getProperties(instance)
|
||||
.get(name)
|
||||
.getValue();
|
||||
},
|
||||
setValue: (instance: Instance, newValue: string) => {
|
||||
onUpdateProperty(instance, name, newValue);
|
||||
},
|
||||
getLabel,
|
||||
getDescription,
|
||||
};
|
||||
} else if (valueType === 'boolean') {
|
||||
return {
|
||||
name,
|
||||
valueType,
|
||||
getValue: (instance: Instance): boolean => {
|
||||
return (
|
||||
getProperties(instance)
|
||||
.get(name)
|
||||
.getValue() === 'true'
|
||||
);
|
||||
},
|
||||
setValue: (instance: Instance, newValue: boolean) => {
|
||||
onUpdateProperty(instance, name, newValue ? '1' : '0');
|
||||
},
|
||||
getLabel,
|
||||
getDescription,
|
||||
};
|
||||
} else if (valueType === 'choice') {
|
||||
// Choice is a "string" (with a selector for the user in the UI)
|
||||
const choices = property
|
||||
.getExtraInfo()
|
||||
.toJSArray()
|
||||
.map(value => ({ value, label: value }));
|
||||
return {
|
||||
name,
|
||||
valueType: 'string',
|
||||
getChoices: () => choices,
|
||||
getValue: (instance: Instance): string => {
|
||||
return getProperties(instance)
|
||||
.get(name)
|
||||
.getValue();
|
||||
},
|
||||
setValue: (instance: Instance, newValue: string) => {
|
||||
onUpdateProperty(instance, name, newValue);
|
||||
},
|
||||
getLabel,
|
||||
getDescription,
|
||||
};
|
||||
} else if (valueType === 'behavior') {
|
||||
const behaviorType =
|
||||
property.getExtraInfo().size() > 0 ? property.getExtraInfo().at(0) : '';
|
||||
return {
|
||||
name,
|
||||
valueType: 'string',
|
||||
getChoices: () => {
|
||||
return !object || behaviorType === ''
|
||||
? []
|
||||
: object
|
||||
.getAllBehaviorNames()
|
||||
.toJSArray()
|
||||
.map(name =>
|
||||
object.getBehavior(name).getTypeName() === behaviorType
|
||||
? name
|
||||
: null
|
||||
)
|
||||
.filter(Boolean)
|
||||
.map(value => ({ value, label: value }));
|
||||
},
|
||||
getValue: (instance: Instance): string => {
|
||||
return getProperties(instance)
|
||||
.get(name)
|
||||
.getValue();
|
||||
},
|
||||
setValue: (instance: Instance, newValue: string) => {
|
||||
onUpdateProperty(instance, name, newValue);
|
||||
},
|
||||
getLabel,
|
||||
getDescription,
|
||||
};
|
||||
} else if (valueType === 'resource') {
|
||||
// Resource is a "string" (with a selector in the UI)
|
||||
const extraInfos = property.getExtraInfo().toJSArray();
|
||||
// $FlowFixMe - assume the passed resource kind is always valid.
|
||||
const kind: ResourceKind = extraInfos[0] || '';
|
||||
// $FlowFixMe - assume the passed resource kind is always valid.
|
||||
const fallbackKind: ResourceKind = extraInfos[1] || '';
|
||||
return {
|
||||
name,
|
||||
valueType: 'resource',
|
||||
resourceKind: kind,
|
||||
fallbackResourceKind: fallbackKind,
|
||||
getValue: (instance: Instance): string => {
|
||||
return getProperties(instance)
|
||||
.get(name)
|
||||
.getValue();
|
||||
},
|
||||
setValue: (instance: Instance, newValue: string) => {
|
||||
onUpdateProperty(instance, name, newValue);
|
||||
},
|
||||
getLabel,
|
||||
getDescription,
|
||||
};
|
||||
} else if (valueType === 'color') {
|
||||
return {
|
||||
name,
|
||||
valueType: 'color',
|
||||
getValue: (instance: Instance): string => {
|
||||
return getProperties(instance)
|
||||
.get(name)
|
||||
.getValue();
|
||||
},
|
||||
setValue: (instance: Instance, newValue: string) => {
|
||||
onUpdateProperty(instance, name, newValue);
|
||||
},
|
||||
getLabel,
|
||||
getDescription,
|
||||
};
|
||||
} else if (valueType === 'textarea') {
|
||||
return {
|
||||
name,
|
||||
valueType: 'textarea',
|
||||
getValue: (instance: Instance): string => {
|
||||
return getProperties(instance)
|
||||
.get(name)
|
||||
.getValue();
|
||||
},
|
||||
setValue: (instance: Instance, newValue: string) => {
|
||||
onUpdateProperty(instance, name, newValue);
|
||||
},
|
||||
getLabel,
|
||||
getDescription,
|
||||
};
|
||||
} else {
|
||||
console.error(
|
||||
`A property with type=${valueType} could not be mapped to a field. Ensure that this type is correct and understood by the IDE.`
|
||||
);
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
const propertyKeywordCouples: Array<Array<string>> = [
|
||||
['X', 'Y', 'Z'],
|
||||
['Width', 'Height', 'Depth'],
|
||||
['Top', 'Bottom'],
|
||||
['Left', 'Right'],
|
||||
['Front', 'Back'],
|
||||
['Up', 'Down'],
|
||||
['Min', 'Max'],
|
||||
['Low', 'High'],
|
||||
['Color', 'Opacity'],
|
||||
['Horizontal', 'Vertical'],
|
||||
['Acceleration', 'Deceleration'],
|
||||
['Duration', 'Easing'],
|
||||
['EffectName', 'EffectProperty'],
|
||||
['Gravity', 'MaxFallingSpeed'],
|
||||
['JumpSpeed', 'JumpSustainTime'],
|
||||
['XGrabTolerance', 'YGrabOffset'],
|
||||
['MaxSpeed', 'SlopeMaxAngle'],
|
||||
];
|
||||
|
||||
const uncapitalize = str => {
|
||||
if (!str) return str;
|
||||
return str[0].toLowerCase() + str.substr(1);
|
||||
};
|
||||
|
||||
/**
|
||||
* Return true when the property exists and should be displayed.
|
||||
*
|
||||
* @param properties The properties
|
||||
* @param name The property name
|
||||
* @param visibility `true` when only deprecated properties must be displayed
|
||||
* and `false` when only not deprecated ones must be displayed
|
||||
*/
|
||||
const isPropertyVisible = (
|
||||
properties: gdMapStringPropertyDescriptor,
|
||||
name: string,
|
||||
visibility: 'All' | 'Basic' | 'Advanced' | 'Deprecated'
|
||||
): boolean => {
|
||||
if (!properties.has(name)) {
|
||||
return false;
|
||||
}
|
||||
const property = properties.get(name);
|
||||
if (property.isHidden()) {
|
||||
return false;
|
||||
}
|
||||
if (visibility === 'All') {
|
||||
return true;
|
||||
}
|
||||
if (visibility === 'Deprecated') {
|
||||
return property.isDeprecated();
|
||||
}
|
||||
if (visibility === 'Advanced') {
|
||||
return property.isAdvanced();
|
||||
}
|
||||
if (visibility === 'Basic') {
|
||||
return !property.isAdvanced() && !property.isDeprecated();
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
||||
/**
|
||||
* Transform a MapStringPropertyDescriptor to a schema that can be used
|
||||
* in CompactPropertiesEditor.
|
||||
* This method is similar to PropertiesMapToSchema.propertiesMapToSchema but returns
|
||||
* fields compatible with CompactPropertiesEditor.
|
||||
*
|
||||
* @param properties The properties to use
|
||||
* @param getProperties The function called to read again the properties
|
||||
* @param onUpdateProperty The function called to update a property of an object
|
||||
*/
|
||||
const propertiesMapToSchema = (
|
||||
properties: gdMapStringPropertyDescriptor,
|
||||
getProperties: (instance: Instance) => any,
|
||||
onUpdateProperty: (
|
||||
instance: Instance,
|
||||
propertyName: string,
|
||||
newValue: string
|
||||
) => void,
|
||||
object: ?gdObject,
|
||||
visibility: 'All' | 'Basic' | 'Advanced' | 'Deprecated' = 'All'
|
||||
): Schema => {
|
||||
const propertyNames = properties.keys();
|
||||
// Aggregate field by groups to be able to build field groups with a title.
|
||||
const fieldsByGroups = new Map<string, Array<Field>>();
|
||||
const alreadyHandledProperties = new Set<string>();
|
||||
mapFor(0, propertyNames.size(), i => {
|
||||
const name = propertyNames.at(i);
|
||||
const property = properties.get(name);
|
||||
if (!isPropertyVisible(properties, name, visibility)) {
|
||||
return null;
|
||||
}
|
||||
if (alreadyHandledProperties.has(name)) return null;
|
||||
|
||||
const groupName = property.getGroup() || '';
|
||||
let fields = fieldsByGroups.get(groupName);
|
||||
if (!fields) {
|
||||
fields = [];
|
||||
fieldsByGroups.set(groupName, fields);
|
||||
}
|
||||
|
||||
// Search a property couple that can be put in a row.
|
||||
let field: ?Field = null;
|
||||
for (const propertyKeywords of propertyKeywordCouples) {
|
||||
const rowPropertyNames: string[] = [];
|
||||
for (let index = 0; index < propertyKeywords.length; index++) {
|
||||
const keyword = propertyKeywords[index];
|
||||
|
||||
if (name.includes(keyword)) {
|
||||
const rowAllPropertyNames = propertyKeywords.map(otherKeyword =>
|
||||
name.replace(keyword, otherKeyword)
|
||||
);
|
||||
for (const rowPropertyName of rowAllPropertyNames) {
|
||||
if (isPropertyVisible(properties, rowPropertyName, visibility)) {
|
||||
rowPropertyNames.push(rowPropertyName);
|
||||
}
|
||||
}
|
||||
}
|
||||
const uncapitalizeKeyword = uncapitalize(keyword);
|
||||
if (name.startsWith(uncapitalizeKeyword)) {
|
||||
const rowAllPropertyNames = propertyKeywords.map(otherKeyword =>
|
||||
name.replace(uncapitalizeKeyword, uncapitalize(otherKeyword))
|
||||
);
|
||||
for (const rowPropertyName of rowAllPropertyNames) {
|
||||
if (isPropertyVisible(properties, rowPropertyName, visibility)) {
|
||||
rowPropertyNames.push(rowPropertyName);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (rowPropertyNames.length > 1) {
|
||||
const rowProperties = rowPropertyNames.map(name =>
|
||||
properties.get(name)
|
||||
);
|
||||
if (
|
||||
rowProperties.every(
|
||||
property => property.getGroup() === rowProperties[0].getGroup()
|
||||
)
|
||||
) {
|
||||
const rowFields: Field[] = [];
|
||||
for (
|
||||
let index = 0;
|
||||
index < rowProperties.length && index < rowPropertyNames.length;
|
||||
index++
|
||||
) {
|
||||
const rowProperty = rowProperties[index];
|
||||
const rowPropertyName = rowPropertyNames[index];
|
||||
|
||||
const field = createField(
|
||||
rowPropertyName,
|
||||
rowProperty,
|
||||
getProperties,
|
||||
onUpdateProperty,
|
||||
object
|
||||
);
|
||||
|
||||
if (field) {
|
||||
rowFields.push(field);
|
||||
}
|
||||
}
|
||||
if (rowFields.length === rowProperties.length) {
|
||||
field = {
|
||||
name: rowPropertyNames.join('-'),
|
||||
type: 'row',
|
||||
children: rowFields,
|
||||
};
|
||||
rowPropertyNames.forEach(propertyName => {
|
||||
alreadyHandledProperties.add(propertyName);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!field) {
|
||||
field = createField(
|
||||
name,
|
||||
property,
|
||||
getProperties,
|
||||
onUpdateProperty,
|
||||
object
|
||||
);
|
||||
}
|
||||
if (field) {
|
||||
fields.push(field);
|
||||
}
|
||||
});
|
||||
if (fieldsByGroups.size === 0) {
|
||||
return [];
|
||||
}
|
||||
const defaultGroupField = fieldsByGroups.get('');
|
||||
if (fieldsByGroups.size === 1 && defaultGroupField) {
|
||||
// Avoid to create a blank title
|
||||
return defaultGroupField;
|
||||
}
|
||||
// Create a group for the default one too because it would look weird with the indentation.
|
||||
const groupNames = [...fieldsByGroups.keys()].sort((a, b) =>
|
||||
a.localeCompare(b)
|
||||
);
|
||||
return groupNames.map(groupName => ({
|
||||
name: groupName,
|
||||
type: 'column',
|
||||
title: groupName,
|
||||
// The group actually always exists here.
|
||||
children: fieldsByGroups.get(groupName) || [],
|
||||
}));
|
||||
};
|
||||
|
||||
const exponents = ['⁰', '¹', '²', '³', '⁴', '⁵'];
|
||||
|
||||
export const getMeasurementUnitShortLabel = (
|
||||
measurementUnit: gdMeasurementUnit
|
||||
): string => {
|
||||
return mapFor(0, measurementUnit.getElementsCount(), i => {
|
||||
const baseUnit = measurementUnit.getElementBaseUnit(i);
|
||||
const power = measurementUnit.getElementPower(i);
|
||||
const absPower = Math.abs(power);
|
||||
const showPower = power < 0 || (absPower > 1 && absPower < 6);
|
||||
return (
|
||||
baseUnit.getSymbol() +
|
||||
(power < 0 ? '⁻' : '') +
|
||||
(showPower ? exponents[absPower] : '')
|
||||
);
|
||||
}).join(' · ');
|
||||
};
|
||||
|
||||
export default propertiesMapToSchema;
|
887
newIDE/app/src/CompactPropertiesEditor/index.js
Normal file
887
newIDE/app/src/CompactPropertiesEditor/index.js
Normal file
@@ -0,0 +1,887 @@
|
||||
// @flow
|
||||
import { type I18n as I18nType } from '@lingui/core';
|
||||
import * as React from 'react';
|
||||
import ResourceSelectorWithThumbnail from '../ResourcesList/ResourceSelectorWithThumbnail';
|
||||
import SelectOption from '../UI/SelectOption';
|
||||
import { type MenuItemTemplate } from '../UI/Menu/Menu.flow';
|
||||
import {
|
||||
type ResourceKind,
|
||||
type ResourceManagementProps,
|
||||
} from '../ResourcesList/ResourceSource';
|
||||
import {
|
||||
ResponsiveLineStackLayout,
|
||||
ColumnStackLayout,
|
||||
LineStackLayout,
|
||||
} from '../UI/Layout';
|
||||
import CompactSelectField from '../UI/CompactSelectField';
|
||||
import CompactSemiControlledTextField from '../UI/CompactSemiControlledTextField';
|
||||
import CompactSemiControlledNumberField from '../UI/CompactSemiControlledNumberField';
|
||||
import { type UnsavedChanges } from '../MainFrame/UnsavedChangesContext';
|
||||
import { Column, Line, Spacer, marginsSize } from '../UI/Grid';
|
||||
import Text from '../UI/Text';
|
||||
import useForceUpdate from '../Utils/UseForceUpdate';
|
||||
import Edit from '../UI/CustomSvgIcons/Edit';
|
||||
import IconButton from '../UI/IconButton';
|
||||
import FlatButton from '../UI/FlatButton';
|
||||
import VerticallyCenterWithBar from '../UI/VerticallyCenterWithBar';
|
||||
import GDevelopThemeContext from '../UI/Theme/GDevelopThemeContext';
|
||||
import { textEllipsisStyle } from '../UI/TextEllipsis';
|
||||
import CompactPropertiesEditorRowField from './CompactPropertiesEditorRowField';
|
||||
|
||||
// An "instance" here is the objects for which properties are shown
|
||||
export type Instance = Object; // This could be improved using generics.
|
||||
export type Instances = Array<Instance>;
|
||||
|
||||
// "Value" fields are fields displayed in the properties.
|
||||
export type ValueFieldCommonProperties = {|
|
||||
name: string,
|
||||
getLabel?: Instance => string,
|
||||
getDescription?: Instance => string,
|
||||
hideLabel?: boolean,
|
||||
getExtraDescription?: Instance => string,
|
||||
disabled?: (instances: Array<gdInitialInstance>) => boolean,
|
||||
onEditButtonBuildMenuTemplate?: (i18n: I18nType) => Array<MenuItemTemplate>,
|
||||
onEditButtonClick?: () => void,
|
||||
|};
|
||||
|
||||
// "Primitive" value fields are "simple" fields.
|
||||
export type PrimitiveValueField =
|
||||
| {|
|
||||
valueType: 'number',
|
||||
getValue: Instance => number,
|
||||
setValue: (instance: Instance, newValue: number) => void,
|
||||
// TODO: support this attribute.
|
||||
getEndAdornment?: Instance => {|
|
||||
label: string,
|
||||
tooltipContent: React.Node,
|
||||
|},
|
||||
getEndAdornmentIcon?: Instance => ?(className: string) => React.Node,
|
||||
onClickEndAdornment?: Instance => void,
|
||||
renderLeftIcon?: (className?: string) => React.Node,
|
||||
...ValueFieldCommonProperties,
|
||||
|}
|
||||
| {|
|
||||
valueType: 'string',
|
||||
getValue: Instance => string,
|
||||
setValue: (instance: Instance, newValue: string) => void,
|
||||
getChoices?: ?() => Array<{|
|
||||
value: string,
|
||||
label: string,
|
||||
labelIsUserDefined?: boolean,
|
||||
|}>,
|
||||
getEndAdornmentIcon?: Instance => ?(className: string) => React.Node,
|
||||
onClickEndAdornment?: Instance => void,
|
||||
renderLeftIcon?: (className?: string) => React.Node,
|
||||
...ValueFieldCommonProperties,
|
||||
|}
|
||||
| {|
|
||||
valueType: 'boolean',
|
||||
getValue: Instance => boolean,
|
||||
setValue: (instance: Instance, newValue: boolean) => void,
|
||||
...ValueFieldCommonProperties,
|
||||
|}
|
||||
| {|
|
||||
valueType: 'enumIcon',
|
||||
renderIcon: (value: any) => React.Node,
|
||||
getValue: Instance => any,
|
||||
isHighlighted: (value: any) => boolean,
|
||||
setValue: (instance: Instance, newValue: any) => void,
|
||||
getNextValue: (currentValue: any) => any,
|
||||
...ValueFieldCommonProperties,
|
||||
|}
|
||||
| {|
|
||||
valueType: 'color',
|
||||
getValue: Instance => string,
|
||||
setValue: (instance: Instance, newValue: string) => void,
|
||||
...ValueFieldCommonProperties,
|
||||
|}
|
||||
| {|
|
||||
valueType: 'textarea',
|
||||
getValue: Instance => string,
|
||||
setValue: (instance: Instance, newValue: string) => void,
|
||||
...ValueFieldCommonProperties,
|
||||
|};
|
||||
|
||||
// "Resource" fields are showing a resource selector.
|
||||
type ResourceField = {|
|
||||
valueType: 'resource',
|
||||
resourceKind: ResourceKind,
|
||||
fallbackResourceKind?: ResourceKind,
|
||||
getValue: Instance => string,
|
||||
setValue: (instance: Instance, newValue: string) => void,
|
||||
renderLeftIcon?: (className?: string) => React.Node,
|
||||
...ValueFieldCommonProperties,
|
||||
|};
|
||||
|
||||
type Title = {|
|
||||
name: string,
|
||||
title: string,
|
||||
renderLeftIcon: (className?: string) => React.Node,
|
||||
getValue?: Instance => string,
|
||||
nonFieldType: 'title',
|
||||
defaultValue?: string,
|
||||
|};
|
||||
|
||||
export type SectionTitle = {|
|
||||
name: string,
|
||||
title: string,
|
||||
nonFieldType: 'sectionTitle',
|
||||
getValue: typeof undefined,
|
||||
|};
|
||||
|
||||
type VerticalCenterWithBar = {|
|
||||
name: string,
|
||||
nonFieldType: 'verticalCenterWithBar',
|
||||
child: PrimitiveValueField,
|
||||
|};
|
||||
|
||||
type ActionButton = {|
|
||||
label: string,
|
||||
disabled: 'onValuesDifferent',
|
||||
getValue: Instance => string,
|
||||
nonFieldType: 'button',
|
||||
getIcon?: ({| fontSize: string |}) => React.Node,
|
||||
onClick: (instance: Instance) => void,
|
||||
|};
|
||||
|
||||
// A value field is a primitive or a resource.
|
||||
export type ValueField = PrimitiveValueField | ResourceField;
|
||||
|
||||
// A field can be a primitive, a resource or a list of fields
|
||||
export type Field =
|
||||
| PrimitiveValueField
|
||||
| ResourceField
|
||||
| SectionTitle
|
||||
| Title
|
||||
| ActionButton
|
||||
| VerticalCenterWithBar
|
||||
| {|
|
||||
name: string,
|
||||
type: 'row' | 'column',
|
||||
preventWrap?: boolean,
|
||||
removeSpacers?: boolean,
|
||||
title?: ?string,
|
||||
children: Array<Field>,
|
||||
|};
|
||||
|
||||
// The schema is the tree of all fields.
|
||||
export type Schema = Array<Field>;
|
||||
|
||||
type Props = {|
|
||||
onInstancesModified?: Instances => void,
|
||||
instances: Instances,
|
||||
schema: Schema,
|
||||
mode?: 'column' | 'row',
|
||||
preventWrap?: boolean,
|
||||
removeSpacers?: boolean,
|
||||
|
||||
// If set, render the "extra" description content from fields
|
||||
// (see getExtraDescription).
|
||||
renderExtraDescriptionText?: (extraDescription: string) => string,
|
||||
unsavedChanges?: ?UnsavedChanges,
|
||||
|
||||
// Optional context:
|
||||
project?: ?gdProject,
|
||||
resourceManagementProps?: ?ResourceManagementProps,
|
||||
|};
|
||||
|
||||
const styles = {
|
||||
columnContainer: {
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
},
|
||||
fieldContainer: {
|
||||
flex: 1,
|
||||
display: 'flex',
|
||||
flexDirection: 'row',
|
||||
alignItems: 'baseline',
|
||||
},
|
||||
field: {
|
||||
flex: 1,
|
||||
width: 'auto',
|
||||
},
|
||||
subHeader: {
|
||||
paddingLeft: 0,
|
||||
},
|
||||
container: { flex: 1, minWidth: 0 },
|
||||
separator: {
|
||||
marginRight: -marginsSize,
|
||||
marginLeft: -marginsSize,
|
||||
marginTop: marginsSize,
|
||||
borderTop: '1px solid black', // Border color is changed in the component.
|
||||
},
|
||||
};
|
||||
|
||||
export const Separator = () => {
|
||||
const gdevelopTheme = React.useContext(GDevelopThemeContext);
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
...styles.separator,
|
||||
borderColor: gdevelopTheme.listItem.separatorColor,
|
||||
}}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
const getDisabled = ({
|
||||
instances,
|
||||
field,
|
||||
}: {|
|
||||
instances: Instances,
|
||||
field: ValueField,
|
||||
|}): boolean => (field.disabled ? field.disabled(instances) : false);
|
||||
|
||||
/**
|
||||
* Get the value for the given field across all instances.
|
||||
* If one of the instances doesn't share the same value, returns the default value.
|
||||
* If there is no instances, returns the default value.
|
||||
* If the field does not have a `getValue` method, returns `null`.
|
||||
*/
|
||||
const getFieldValue = ({
|
||||
instances,
|
||||
field,
|
||||
defaultValue,
|
||||
}: {|
|
||||
instances: Instances,
|
||||
field: ValueField | ActionButton | SectionTitle | Title,
|
||||
defaultValue?: any,
|
||||
|}): any => {
|
||||
if (!instances[0]) {
|
||||
console.warn(
|
||||
'getFieldValue was called with an empty list of instances (or containing undefined). This is a bug that should be fixed.'
|
||||
);
|
||||
return defaultValue;
|
||||
}
|
||||
|
||||
const { getValue } = field;
|
||||
if (!getValue) return null;
|
||||
|
||||
let value = getValue(instances[0]);
|
||||
if (typeof defaultValue !== 'undefined') {
|
||||
for (var i = 1; i < instances.length; ++i) {
|
||||
if (value !== getValue(instances[i])) {
|
||||
value = defaultValue;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return value;
|
||||
};
|
||||
|
||||
const getFieldEndAdornmentIcon = ({
|
||||
instances,
|
||||
field,
|
||||
}: {|
|
||||
instances: Instances,
|
||||
field: ValueField,
|
||||
|}): ?(className: string) => React.Node => {
|
||||
if (!instances[0]) {
|
||||
console.warn(
|
||||
'getFieldEndAdornmentIcon was called with an empty list of instances (or containing undefined). This is a bug that should be fixed.'
|
||||
);
|
||||
return null;
|
||||
}
|
||||
if (!field.getEndAdornmentIcon) return null;
|
||||
|
||||
for (const instance of instances) {
|
||||
const getEndAdornmentIcon = field.getEndAdornmentIcon(instance);
|
||||
if (getEndAdornmentIcon) return getEndAdornmentIcon;
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
const getFieldLabel = ({
|
||||
instances,
|
||||
field,
|
||||
}: {|
|
||||
instances: Instances,
|
||||
field: ValueField,
|
||||
|}): any => {
|
||||
if (!instances[0]) {
|
||||
console.warn(
|
||||
'getFieldLabel was called with an empty list of instances (or containing undefined). This is a bug that should be fixed.'
|
||||
);
|
||||
return field.name;
|
||||
}
|
||||
|
||||
if (field.getLabel) return field.getLabel(instances[0]);
|
||||
|
||||
return field.name;
|
||||
};
|
||||
|
||||
const CompactPropertiesEditor = ({
|
||||
onInstancesModified,
|
||||
instances,
|
||||
schema,
|
||||
mode,
|
||||
renderExtraDescriptionText,
|
||||
unsavedChanges,
|
||||
project,
|
||||
resourceManagementProps,
|
||||
preventWrap,
|
||||
removeSpacers,
|
||||
}: Props) => {
|
||||
const forceUpdate = useForceUpdate();
|
||||
|
||||
const _onInstancesModified = React.useCallback(
|
||||
(instances: Instances) => {
|
||||
// This properties editor is dealing with fields that are
|
||||
// responsible to update their state (see field.setValue).
|
||||
|
||||
if (unsavedChanges) unsavedChanges.triggerUnsavedChanges();
|
||||
if (onInstancesModified) onInstancesModified(instances);
|
||||
forceUpdate();
|
||||
},
|
||||
[unsavedChanges, onInstancesModified, forceUpdate]
|
||||
);
|
||||
|
||||
const getFieldDescription = React.useCallback(
|
||||
(field: ValueField): ?string => {
|
||||
if (!instances[0]) {
|
||||
console.warn(
|
||||
'getFieldDescription was called with an empty list of instances (or containing undefined). This is a bug that should be fixed.'
|
||||
);
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const descriptions: Array<string> = [];
|
||||
if (field.getDescription)
|
||||
descriptions.push(field.getDescription(instances[0]));
|
||||
if (renderExtraDescriptionText && field.getExtraDescription)
|
||||
descriptions.push(
|
||||
renderExtraDescriptionText(field.getExtraDescription(instances[0]))
|
||||
);
|
||||
|
||||
return descriptions.join('\n') || undefined;
|
||||
},
|
||||
[instances, renderExtraDescriptionText]
|
||||
);
|
||||
|
||||
const renderInputField = React.useCallback(
|
||||
(field: ValueField) => {
|
||||
if (field.name === 'PLEASE_ALSO_SHOW_EDIT_BUTTON_THANKS') return null; // This special property was used in GDevelop 4 IDE to ask for a Edit button to be shown, ignore it.
|
||||
|
||||
if (field.valueType === 'boolean') {
|
||||
return 'TODO';
|
||||
// const { setValue } = field;
|
||||
// const description = getFieldDescription(field);
|
||||
// return (
|
||||
// <InlineCheckbox
|
||||
// label={
|
||||
// !description ? (
|
||||
// getFieldLabel({ instances, field })
|
||||
// ) : (
|
||||
// <React.Fragment>
|
||||
// <Line noMargin>{getFieldLabel({ instances, field })}</Line>
|
||||
// <FormHelperText style={{ display: 'inline' }}>
|
||||
// <MarkdownText source={description} />
|
||||
// </FormHelperText>
|
||||
// </React.Fragment>
|
||||
// )
|
||||
// }
|
||||
// key={field.name}
|
||||
// id={field.name}
|
||||
// checked={getFieldValue({ instances, field })}
|
||||
// onCheck={(event, newValue) => {
|
||||
// instances.forEach(i => setValue(i, !!newValue));
|
||||
// _onInstancesModified(instances);
|
||||
// }}
|
||||
// disabled={getDisabled({ instances, field })}
|
||||
// />
|
||||
// );
|
||||
} else if (field.valueType === 'number') {
|
||||
const { setValue, onClickEndAdornment } = field;
|
||||
// TODO: Support end adornment
|
||||
// const endAdornment = getEndAdornment && getEndAdornment(instances[0]);
|
||||
|
||||
const commonProps = {
|
||||
key: field.name,
|
||||
id: field.name,
|
||||
value: getFieldValue({
|
||||
instances,
|
||||
field,
|
||||
}),
|
||||
onChange: newValue => {
|
||||
// If the value is not a number, the user is probably still typing, adding a dot or a comma.
|
||||
// So don't update the value, it will be reverted if they leave the field.
|
||||
if (isNaN(newValue)) return;
|
||||
instances.forEach(i => setValue(i, newValue));
|
||||
_onInstancesModified(instances);
|
||||
},
|
||||
disabled: getDisabled({ instances, field }),
|
||||
renderEndAdornmentOnHover:
|
||||
getFieldEndAdornmentIcon({ instances, field }) || undefined,
|
||||
onClickEndAdornment: () => {
|
||||
if (!onClickEndAdornment) return;
|
||||
instances.forEach(i => onClickEndAdornment(i));
|
||||
_onInstancesModified(instances);
|
||||
},
|
||||
};
|
||||
if (field.renderLeftIcon || field.hideLabel) {
|
||||
return (
|
||||
<CompactSemiControlledNumberField
|
||||
{...commonProps}
|
||||
useLeftIconAsNumberControl
|
||||
renderLeftIcon={field.renderLeftIcon}
|
||||
leftIconTooltip={getFieldLabel({ instances, field })}
|
||||
// endAdornment={
|
||||
// endAdornment && (
|
||||
// <Tooltip title={endAdornment.tooltipContent}>
|
||||
// <InputAdornment position="end">
|
||||
// {endAdornment.label}
|
||||
// </InputAdornment>
|
||||
// </Tooltip>
|
||||
// )
|
||||
// }
|
||||
/>
|
||||
);
|
||||
} else {
|
||||
const { key, ...otherCommonProps } = commonProps;
|
||||
return (
|
||||
<CompactPropertiesEditorRowField
|
||||
key={key}
|
||||
label={getFieldLabel({ instances, field })}
|
||||
markdownDescription={getFieldDescription(field)}
|
||||
field={
|
||||
<CompactSemiControlledNumberField
|
||||
{...otherCommonProps}
|
||||
// endAdornment={
|
||||
// endAdornment && (
|
||||
// <Tooltip title={endAdornment.tooltipContent}>
|
||||
// <InputAdornment position="end">
|
||||
// {endAdornment.label}
|
||||
// </InputAdornment>
|
||||
// </Tooltip>
|
||||
// )
|
||||
// }
|
||||
/>
|
||||
}
|
||||
/>
|
||||
);
|
||||
}
|
||||
} else if (field.valueType === 'color') {
|
||||
return 'TODO';
|
||||
// const { setValue } = field;
|
||||
// return (
|
||||
// <Column key={field.name} expand noMargin>
|
||||
// <ColorField
|
||||
// id={field.name}
|
||||
// floatingLabelText={getFieldLabel({ instances, field })}
|
||||
// helperMarkdownText={getFieldDescription(field)}
|
||||
// disableAlpha
|
||||
// fullWidth
|
||||
// color={getFieldValue({ instances, field })}
|
||||
// onChange={color => {
|
||||
// const rgbString =
|
||||
// color.length === 0 ? '' : rgbOrHexToRGBString(color);
|
||||
// instances.forEach(i => setValue(i, rgbString));
|
||||
// _onInstancesModified(instances);
|
||||
// }}
|
||||
// />
|
||||
// </Column>
|
||||
// );
|
||||
} else if (field.valueType === 'enumIcon') {
|
||||
const value = getFieldValue({ instances, field });
|
||||
return (
|
||||
<IconButton
|
||||
key={field.name}
|
||||
id={field.name}
|
||||
size="small"
|
||||
tooltip={getFieldLabel({ instances, field })}
|
||||
selected={field.isHighlighted(value)}
|
||||
onClick={event => {
|
||||
instances.forEach(i =>
|
||||
field.setValue(i, field.getNextValue(value))
|
||||
);
|
||||
_onInstancesModified(instances);
|
||||
}}
|
||||
>
|
||||
{field.renderIcon(value)}
|
||||
</IconButton>
|
||||
);
|
||||
} else if (field.valueType === 'textarea') {
|
||||
return 'TODO';
|
||||
// const { setValue } = field;
|
||||
// return (
|
||||
// <SemiControlledTextField
|
||||
// key={field.name}
|
||||
// id={field.name}
|
||||
// onChange={text => {
|
||||
// instances.forEach(i => setValue(i, text || ''));
|
||||
// _onInstancesModified(instances);
|
||||
// }}
|
||||
// value={getFieldValue({ instances, field })}
|
||||
// floatingLabelText={getFieldLabel({ instances, field })}
|
||||
// floatingLabelFixed
|
||||
// helperMarkdownText={getFieldDescription(field)}
|
||||
// multiline
|
||||
// style={styles.field}
|
||||
// />
|
||||
// );
|
||||
} else if (field.valueType === 'resource') {
|
||||
return 'TODO';
|
||||
} else {
|
||||
const {
|
||||
// TODO: Still support onEditButtonClick & onEditButtonBuildMenuTemplate ?
|
||||
// onEditButtonBuildMenuTemplate,
|
||||
// onEditButtonClick,
|
||||
setValue,
|
||||
onClickEndAdornment,
|
||||
} = field;
|
||||
const commonProps = {
|
||||
key: field.name,
|
||||
id: field.name,
|
||||
value: getFieldValue({
|
||||
instances,
|
||||
field,
|
||||
defaultValue: '(Multiple values)',
|
||||
}),
|
||||
onChange: newValue => {
|
||||
instances.forEach(i => setValue(i, newValue || ''));
|
||||
_onInstancesModified(instances);
|
||||
},
|
||||
disabled: getDisabled({ instances, field }),
|
||||
renderEndAdornmentOnHover:
|
||||
getFieldEndAdornmentIcon({ instances, field }) || undefined,
|
||||
onClickEndAdornment: () => {
|
||||
if (!onClickEndAdornment) return;
|
||||
instances.forEach(i => onClickEndAdornment(i));
|
||||
_onInstancesModified(instances);
|
||||
},
|
||||
};
|
||||
if (field.renderLeftIcon || field.hideLabel) {
|
||||
return (
|
||||
<CompactSemiControlledTextField
|
||||
{...commonProps}
|
||||
renderLeftIcon={field.renderLeftIcon}
|
||||
leftIconTooltip={getFieldLabel({ instances, field })}
|
||||
/>
|
||||
);
|
||||
} else {
|
||||
const { key, ...otherCommonProps } = commonProps;
|
||||
|
||||
return (
|
||||
<CompactPropertiesEditorRowField
|
||||
key={key}
|
||||
label={getFieldLabel({ instances, field })}
|
||||
markdownDescription={getFieldDescription(field)}
|
||||
field={<CompactSemiControlledTextField {...otherCommonProps} />}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
},
|
||||
[instances, _onInstancesModified, getFieldDescription]
|
||||
);
|
||||
|
||||
const renderSelectField = React.useCallback(
|
||||
(field: ValueField) => {
|
||||
if (!field.getChoices || !field.getValue) return;
|
||||
|
||||
const children = field
|
||||
.getChoices()
|
||||
.map(({ value, label, labelIsUserDefined }) => (
|
||||
<SelectOption
|
||||
key={value}
|
||||
value={value}
|
||||
label={label}
|
||||
shouldNotTranslate={labelIsUserDefined}
|
||||
/>
|
||||
));
|
||||
|
||||
if (field.valueType === 'number') {
|
||||
const { setValue } = field;
|
||||
return (
|
||||
<CompactSelectField
|
||||
value={getFieldValue({ instances, field })}
|
||||
key={field.name}
|
||||
id={field.name}
|
||||
// floatingLabelText={getFieldLabel({ instances, field })}
|
||||
// helperMarkdownText={getFieldDescription(field)}
|
||||
onChange={(newValue: string) => {
|
||||
instances.forEach(i => setValue(i, parseFloat(newValue) || 0));
|
||||
_onInstancesModified(instances);
|
||||
}}
|
||||
disabled={field.disabled}
|
||||
>
|
||||
{children}
|
||||
</CompactSelectField>
|
||||
);
|
||||
} else if (field.valueType === 'string') {
|
||||
const { setValue } = field;
|
||||
return (
|
||||
<CompactSelectField
|
||||
value={getFieldValue({
|
||||
instances,
|
||||
field,
|
||||
defaultValue: '(Multiple values)',
|
||||
})}
|
||||
key={field.name}
|
||||
id={field.name}
|
||||
// floatingLabelText={getFieldLabel({ instances, field })}
|
||||
// helperMarkdownText={getFieldDescription(field)}
|
||||
onChange={(newValue: string) => {
|
||||
instances.forEach(i => setValue(i, newValue || ''));
|
||||
_onInstancesModified(instances);
|
||||
}}
|
||||
disabled={getDisabled({ instances, field })}
|
||||
renderLeftIcon={field.renderLeftIcon}
|
||||
leftIconTooltip={getFieldLabel({ instances, field })}
|
||||
>
|
||||
{children}
|
||||
</CompactSelectField>
|
||||
);
|
||||
}
|
||||
},
|
||||
[instances, _onInstancesModified]
|
||||
);
|
||||
|
||||
const renderButton = React.useCallback(
|
||||
(field: ActionButton) => {
|
||||
let disabled = false;
|
||||
if (field.disabled === 'onValuesDifferent') {
|
||||
const DIFFERENT_VALUES = 'DIFFERENT_VALUES';
|
||||
disabled =
|
||||
getFieldValue({
|
||||
instances,
|
||||
field,
|
||||
defaultValue: DIFFERENT_VALUES,
|
||||
}) === DIFFERENT_VALUES;
|
||||
}
|
||||
return (
|
||||
<React.Fragment key={`button-${field.label}`}>
|
||||
<FlatButton
|
||||
fullWidth
|
||||
primary
|
||||
leftIcon={
|
||||
field.getIcon ? (
|
||||
field.getIcon({ fontSize: 'small' })
|
||||
) : (
|
||||
<Edit fontSize="small" />
|
||||
)
|
||||
}
|
||||
disabled={disabled}
|
||||
label={field.label}
|
||||
onClick={() => {
|
||||
if (!instances[0]) return;
|
||||
field.onClick(instances[0]);
|
||||
}}
|
||||
/>
|
||||
<Spacer />
|
||||
</React.Fragment>
|
||||
);
|
||||
},
|
||||
[instances]
|
||||
);
|
||||
|
||||
const renderResourceField = (field: ResourceField) => {
|
||||
if (!project || !resourceManagementProps) {
|
||||
console.error(
|
||||
'You tried to display a resource field in a PropertiesEditor that does not support display resources. If you need to display resources, pass additional props (project, resourceManagementProps).'
|
||||
);
|
||||
return null;
|
||||
}
|
||||
|
||||
const { setValue } = field;
|
||||
return (
|
||||
<ResourceSelectorWithThumbnail
|
||||
key={field.name}
|
||||
project={project}
|
||||
resourceManagementProps={resourceManagementProps}
|
||||
resourceKind={field.resourceKind}
|
||||
fallbackResourceKind={field.fallbackResourceKind}
|
||||
resourceName={getFieldValue({
|
||||
instances,
|
||||
field,
|
||||
defaultValue: '(Multiple values)',
|
||||
})}
|
||||
onChange={newValue => {
|
||||
instances.forEach(i => setValue(i, newValue));
|
||||
_onInstancesModified(instances);
|
||||
}}
|
||||
floatingLabelText={getFieldLabel({ instances, field })}
|
||||
helperMarkdownText={getFieldDescription(field)}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
const renderVerticalCenterWithBar = (field: Field) =>
|
||||
field.child && field.child.getValue ? (
|
||||
<VerticallyCenterWithBar key={field.name}>
|
||||
{renderInputField(field.child)}
|
||||
</VerticallyCenterWithBar>
|
||||
) : (
|
||||
'TODO'
|
||||
);
|
||||
|
||||
const renderContainer =
|
||||
mode === 'row'
|
||||
? (fields: React.Node) =>
|
||||
preventWrap ? (
|
||||
removeSpacers ? (
|
||||
<Line noMargin alignItems="center" expand>
|
||||
{fields}
|
||||
</Line>
|
||||
) : (
|
||||
<LineStackLayout noMargin alignItems="center" expand>
|
||||
{fields}
|
||||
</LineStackLayout>
|
||||
)
|
||||
) : (
|
||||
<ResponsiveLineStackLayout noMargin alignItems="center" expand>
|
||||
{fields}
|
||||
</ResponsiveLineStackLayout>
|
||||
)
|
||||
: (fields: React.Node) =>
|
||||
removeSpacers ? (
|
||||
<Column noMargin expand>
|
||||
{fields}
|
||||
</Column>
|
||||
) : (
|
||||
<ColumnStackLayout noMargin expand>
|
||||
{fields}
|
||||
</ColumnStackLayout>
|
||||
);
|
||||
|
||||
const renderTitle = React.useCallback(
|
||||
(field: Title) => {
|
||||
const { getValue, renderLeftIcon } = field;
|
||||
|
||||
let additionalText = null;
|
||||
|
||||
if (getValue) {
|
||||
let selectedInstancesValue = getFieldValue({
|
||||
instances,
|
||||
field,
|
||||
defaultValue: field.defaultValue || 'Multiple Values',
|
||||
});
|
||||
if (!!selectedInstancesValue) additionalText = selectedInstancesValue;
|
||||
}
|
||||
|
||||
if (!!additionalText) {
|
||||
return (
|
||||
<LineStackLayout
|
||||
alignItems="center"
|
||||
key={`section-title-${field.name}`}
|
||||
expand
|
||||
noMargin
|
||||
>
|
||||
{renderLeftIcon()}
|
||||
<Text displayInlineAsSpan noMargin>
|
||||
{field.title}
|
||||
</Text>
|
||||
<Text displayInlineAsSpan noMargin>
|
||||
-
|
||||
</Text>
|
||||
<Text
|
||||
allowSelection
|
||||
displayInlineAsSpan
|
||||
noMargin
|
||||
style={textEllipsisStyle}
|
||||
>
|
||||
{additionalText}
|
||||
</Text>
|
||||
</LineStackLayout>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<LineStackLayout
|
||||
key={`title-${field.name}`}
|
||||
expand
|
||||
noMargin
|
||||
alignItems="center"
|
||||
>
|
||||
{renderLeftIcon()}
|
||||
<Text displayInlineAsSpan size="sub-title" noMargin>
|
||||
{field.title}
|
||||
</Text>
|
||||
</LineStackLayout>
|
||||
);
|
||||
},
|
||||
[instances]
|
||||
);
|
||||
const renderSectionTitle = React.useCallback((field: SectionTitle) => {
|
||||
return [
|
||||
<Separator key={field.name + '-separator'} />,
|
||||
<Line key={`section-title-${field.name}`} noMargin>
|
||||
<Text displayInlineAsSpan size="sub-title" noMargin>
|
||||
{field.title}
|
||||
</Text>
|
||||
</Line>,
|
||||
];
|
||||
}, []);
|
||||
|
||||
return renderContainer(
|
||||
schema.map(field => {
|
||||
if (!!field.nonFieldType) {
|
||||
if (field.nonFieldType === 'title') {
|
||||
return renderTitle(field);
|
||||
} else if (field.nonFieldType === 'sectionTitle') {
|
||||
return renderSectionTitle(field);
|
||||
} else if (field.nonFieldType === 'button') {
|
||||
return renderButton(field);
|
||||
} else if (field.nonFieldType === 'verticalCenterWithBar') {
|
||||
return renderVerticalCenterWithBar(field);
|
||||
}
|
||||
return null;
|
||||
} else if (field.children) {
|
||||
if (field.type === 'row') {
|
||||
const contentView = (
|
||||
<React.Fragment key={field.name}>
|
||||
<CompactPropertiesEditor
|
||||
project={project}
|
||||
resourceManagementProps={resourceManagementProps}
|
||||
schema={field.children}
|
||||
instances={instances}
|
||||
mode="row"
|
||||
unsavedChanges={unsavedChanges}
|
||||
onInstancesModified={onInstancesModified}
|
||||
preventWrap={field.preventWrap}
|
||||
removeSpacers={field.removeSpacers}
|
||||
/>
|
||||
</React.Fragment>
|
||||
);
|
||||
if (field.title) {
|
||||
return [
|
||||
<Separator key={field.name + '-separator'} />,
|
||||
<Text key={field.name + '-title'} size="sub-title" noMargin>
|
||||
{field.title}
|
||||
</Text>,
|
||||
contentView,
|
||||
];
|
||||
}
|
||||
return contentView;
|
||||
}
|
||||
|
||||
return (
|
||||
<div key={field.name} style={styles.container}>
|
||||
<React.Fragment key={field.name}>
|
||||
<CompactPropertiesEditor
|
||||
project={project}
|
||||
resourceManagementProps={resourceManagementProps}
|
||||
schema={field.children}
|
||||
instances={instances}
|
||||
mode="column"
|
||||
unsavedChanges={unsavedChanges}
|
||||
onInstancesModified={onInstancesModified}
|
||||
preventWrap={field.preventWrap}
|
||||
removeSpacers={field.removeSpacers}
|
||||
/>
|
||||
</React.Fragment>
|
||||
</div>
|
||||
);
|
||||
} else if (field.valueType === 'resource') {
|
||||
return renderResourceField(field);
|
||||
} else {
|
||||
if (field.getChoices && field.getValue) return renderSelectField(field);
|
||||
if (field.getValue) return renderInputField(field);
|
||||
}
|
||||
return null;
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
export default CompactPropertiesEditor;
|
@@ -30,5 +30,3 @@ export const handle = 'move-handle';
|
||||
export const linkContainer = 'link-container';
|
||||
|
||||
export const nameAndIconContainer = 'name-and-icon-container';
|
||||
|
||||
export const treeView = 'tree-view';
|
||||
|
@@ -50,6 +50,7 @@ import { isDescendant, type MoveFunctionArguments } from './helpers';
|
||||
import { dataObjectToProps } from '../../Utils/HTMLDataset';
|
||||
import useForceUpdate from '../../Utils/UseForceUpdate';
|
||||
import { useLongTouch } from '../../Utils/UseLongTouch';
|
||||
import GDevelopThemeContext from '../../UI/Theme/GDevelopThemeContext';
|
||||
const gd: libGDevelop = global.gd;
|
||||
|
||||
const eventsSheetEventsDnDType = 'events-sheet-events-dnd-type';
|
||||
@@ -230,9 +231,12 @@ const EventContainer = (props: EventsContainerProps) => {
|
||||
};
|
||||
|
||||
const SortableTree = ({ className, ...otherProps }) => {
|
||||
const gdevelopTheme = React.useContext(GDevelopThemeContext);
|
||||
return (
|
||||
<SortableTreeWithoutDndContext
|
||||
className={`${eventsTree} ${className}`}
|
||||
className={`${eventsTree} ${
|
||||
gdevelopTheme.palette.type === 'light' ? 'light-theme' : 'dark-theme'
|
||||
} ${className}`}
|
||||
{...otherProps}
|
||||
/>
|
||||
);
|
||||
|
@@ -137,7 +137,7 @@
|
||||
background-image: url('./unfold.png');
|
||||
background-position: 11px 3px !important;
|
||||
}
|
||||
.gd-events-sheet .DefaultLightTheme .rst__expandButton {
|
||||
.gd-events-sheet .light-theme .rst__expandButton {
|
||||
background-image: url('./unfoldDark.png');
|
||||
}
|
||||
|
||||
@@ -145,7 +145,7 @@
|
||||
background-image: url('./fold.png');
|
||||
background-position: center 7px !important;
|
||||
}
|
||||
.gd-events-sheet .DefaultLightTheme .rst__collapseButton {
|
||||
.gd-events-sheet .light-theme .rst__collapseButton {
|
||||
background-image: url('./foldDark.png');
|
||||
}
|
||||
|
||||
|
@@ -13,6 +13,7 @@ import {
|
||||
type FieldFocusFunction,
|
||||
} from './ParameterFieldCommons';
|
||||
import EventsRootVariablesFinder from '../../Utils/EventsRootVariablesFinder';
|
||||
import GlobalIcon from '../../UI/CustomSvgIcons/Publish';
|
||||
|
||||
export default React.forwardRef<ParameterFieldProps, ParameterFieldInterface>(
|
||||
function GlobalVariableField(props: ParameterFieldProps, ref) {
|
||||
@@ -89,5 +90,4 @@ export default React.forwardRef<ParameterFieldProps, ParameterFieldInterface>(
|
||||
|
||||
export const renderInlineGlobalVariable = (
|
||||
props: ParameterInlineRendererProps
|
||||
) =>
|
||||
renderVariableWithIcon(props, 'res/types/globalvar.png', 'global variable');
|
||||
) => renderVariableWithIcon(props, GlobalIcon, 'global variable');
|
||||
|
@@ -16,6 +16,7 @@ import { getLastObjectParameterValue } from './ParameterMetadataTools';
|
||||
import EventsRootVariablesFinder from '../../Utils/EventsRootVariablesFinder';
|
||||
import getObjectByName from '../../Utils/GetObjectByName';
|
||||
import getObjectGroupByName from '../../Utils/GetObjectGroupByName';
|
||||
import ObjectIcon from '../../UI/CustomSvgIcons/Object';
|
||||
|
||||
// TODO Move this function to the ObjectsContainersList class.
|
||||
const getObjectOrGroupVariablesContainers = (
|
||||
@@ -165,5 +166,4 @@ export default React.forwardRef<ParameterFieldProps, ParameterFieldInterface>(
|
||||
|
||||
export const renderInlineObjectVariable = (
|
||||
props: ParameterInlineRendererProps
|
||||
) =>
|
||||
renderVariableWithIcon(props, 'res/types/objectvar.png', 'object variable');
|
||||
) => renderVariableWithIcon(props, ObjectIcon, 'object variable');
|
||||
|
@@ -13,6 +13,7 @@ import {
|
||||
type FieldFocusFunction,
|
||||
} from './ParameterFieldCommons';
|
||||
import EventsRootVariablesFinder from '../../Utils/EventsRootVariablesFinder';
|
||||
import SceneIcon from '../../UI/CustomSvgIcons/Scene';
|
||||
|
||||
export default React.forwardRef<ParameterFieldProps, ParameterFieldInterface>(
|
||||
function SceneVariableField(props: ParameterFieldProps, ref) {
|
||||
@@ -97,4 +98,4 @@ export default React.forwardRef<ParameterFieldProps, ParameterFieldInterface>(
|
||||
|
||||
export const renderInlineSceneVariable = (
|
||||
props: ParameterInlineRendererProps
|
||||
) => renderVariableWithIcon(props, 'res/types/scenevar.png', 'scene variable');
|
||||
) => renderVariableWithIcon(props, SceneIcon, 'scene variable');
|
||||
|
@@ -24,6 +24,7 @@ import { TextFieldWithButtonLayout } from '../../UI/Layout';
|
||||
import { type ParameterInlineRendererProps } from './ParameterInlineRenderer.flow';
|
||||
import ShareExternal from '../../UI/CustomSvgIcons/ShareExternal';
|
||||
import intersection from 'lodash/intersection';
|
||||
import SvgIcon, { type SvgIconProps } from '@material-ui/core/SvgIcon';
|
||||
|
||||
type Props = {
|
||||
...ParameterFieldProps,
|
||||
@@ -265,13 +266,17 @@ export const renderVariableWithIcon = (
|
||||
InvalidParameterValue,
|
||||
MissingParameterValue,
|
||||
}: ParameterInlineRendererProps,
|
||||
iconPath: string,
|
||||
VariableIcon: SvgIconProps => React.Element<typeof SvgIcon>,
|
||||
tooltip: string
|
||||
) => {
|
||||
if (!value && !parameterMetadata.isOptional()) {
|
||||
return <MissingParameterValue />;
|
||||
}
|
||||
|
||||
const IconAndNameContainer = expressionIsValid
|
||||
? React.Fragment
|
||||
: InvalidParameterValue;
|
||||
|
||||
return (
|
||||
<span
|
||||
title={tooltip}
|
||||
@@ -282,18 +287,14 @@ export const renderVariableWithIcon = (
|
||||
VariableNameQuickAnalyzeResults.OK,
|
||||
})}
|
||||
>
|
||||
<img
|
||||
className={classNames({
|
||||
[icon]: true,
|
||||
})}
|
||||
src={iconPath}
|
||||
alt=""
|
||||
/>
|
||||
{expressionIsValid ? (
|
||||
value
|
||||
) : (
|
||||
<InvalidParameterValue>{value}</InvalidParameterValue>
|
||||
)}
|
||||
<IconAndNameContainer>
|
||||
<VariableIcon
|
||||
className={classNames({
|
||||
[icon]: true,
|
||||
})}
|
||||
/>
|
||||
{value}
|
||||
</IconAndNameContainer>
|
||||
</span>
|
||||
);
|
||||
};
|
||||
|
@@ -105,7 +105,7 @@ export const DoneFooter = ({
|
||||
</LineStackLayout>
|
||||
<Text>
|
||||
<Trans>
|
||||
You can now upload the game to a web hosting to play to the game.
|
||||
You can now upload the game to a web hosting service to play it.
|
||||
</Trans>
|
||||
</Text>
|
||||
<Spacer />
|
||||
|
@@ -21,6 +21,7 @@ import {
|
||||
updateComment,
|
||||
type Comment,
|
||||
type GameRatings,
|
||||
canCommentBeRatedByOwner,
|
||||
} from '../../Utils/GDevelopServices/Play';
|
||||
import { type AuthenticatedUser } from '../../Profile/AuthenticatedUserContext';
|
||||
import { useOptimisticState } from '../../Utils/UseOptimisticState';
|
||||
@@ -119,6 +120,7 @@ const FeedbackCard = ({
|
||||
!!comment.processedAt,
|
||||
processComment
|
||||
);
|
||||
const canRateComment = canCommentBeRatedByOwner(comment);
|
||||
|
||||
const [ownerQualityRating, setOwnerQualityRating] = useOptimisticState(
|
||||
(comment.qualityRatingPerRole && comment.qualityRatingPerRole.owner) ||
|
||||
@@ -158,58 +160,62 @@ const FeedbackCard = ({
|
||||
disabled={processed}
|
||||
cardCornerAction={
|
||||
<LineStackLayout noMargin>
|
||||
<IconButton
|
||||
size="small"
|
||||
tooltip={t`Rank this comment as great`}
|
||||
onClick={() => setOwnerQualityRating('great', i18n)}
|
||||
>
|
||||
<Heart
|
||||
htmlColor={
|
||||
ownerQualityRating === 'great'
|
||||
? theme.message.valid
|
||||
: undefined
|
||||
}
|
||||
/>
|
||||
</IconButton>
|
||||
<IconButton
|
||||
size="small"
|
||||
tooltip={t`Rank this comment as good`}
|
||||
onClick={() => setOwnerQualityRating('good', i18n)}
|
||||
>
|
||||
<Like
|
||||
htmlColor={
|
||||
ownerQualityRating === 'good'
|
||||
? theme.message.valid
|
||||
: undefined
|
||||
}
|
||||
/>
|
||||
</IconButton>
|
||||
<IconButton
|
||||
size="small"
|
||||
tooltip={t`Rank this comment as bad`}
|
||||
onClick={() => setOwnerQualityRating('bad', i18n)}
|
||||
>
|
||||
<Dislike
|
||||
htmlColor={
|
||||
ownerQualityRating === 'bad'
|
||||
? theme.message.warning
|
||||
: undefined
|
||||
}
|
||||
/>
|
||||
</IconButton>
|
||||
<IconButton
|
||||
size="small"
|
||||
tooltip={t`Report this comment as abusive, harmful or spam`}
|
||||
onClick={() => setOwnerQualityRating('harmful', i18n)}
|
||||
>
|
||||
<Danger
|
||||
htmlColor={
|
||||
ownerQualityRating === 'harmful'
|
||||
? theme.message.error
|
||||
: undefined
|
||||
}
|
||||
/>
|
||||
</IconButton>
|
||||
{canRateComment && (
|
||||
<LineStackLayout noMargin>
|
||||
<IconButton
|
||||
size="small"
|
||||
tooltip={t`Rank this comment as great`}
|
||||
onClick={() => setOwnerQualityRating('great', i18n)}
|
||||
>
|
||||
<Heart
|
||||
htmlColor={
|
||||
ownerQualityRating === 'great'
|
||||
? theme.message.valid
|
||||
: undefined
|
||||
}
|
||||
/>
|
||||
</IconButton>
|
||||
<IconButton
|
||||
size="small"
|
||||
tooltip={t`Rank this comment as good`}
|
||||
onClick={() => setOwnerQualityRating('good', i18n)}
|
||||
>
|
||||
<Like
|
||||
htmlColor={
|
||||
ownerQualityRating === 'good'
|
||||
? theme.message.valid
|
||||
: undefined
|
||||
}
|
||||
/>
|
||||
</IconButton>
|
||||
<IconButton
|
||||
size="small"
|
||||
tooltip={t`Rank this comment as bad`}
|
||||
onClick={() => setOwnerQualityRating('bad', i18n)}
|
||||
>
|
||||
<Dislike
|
||||
htmlColor={
|
||||
ownerQualityRating === 'bad'
|
||||
? theme.message.warning
|
||||
: undefined
|
||||
}
|
||||
/>
|
||||
</IconButton>
|
||||
<IconButton
|
||||
size="small"
|
||||
tooltip={t`Report this comment as abusive, harmful or spam`}
|
||||
onClick={() => setOwnerQualityRating('harmful', i18n)}
|
||||
>
|
||||
<Danger
|
||||
htmlColor={
|
||||
ownerQualityRating === 'harmful'
|
||||
? theme.message.error
|
||||
: undefined
|
||||
}
|
||||
/>
|
||||
</IconButton>
|
||||
</LineStackLayout>
|
||||
)}
|
||||
<IconButton
|
||||
size="small"
|
||||
tooltip={processed ? t`Mark as unread` : t`Mark as read`}
|
||||
|
@@ -403,8 +403,7 @@ function LeaderboardAppearanceDialog({
|
||||
<Column noMargin>
|
||||
<Text noMargin>
|
||||
<Trans>
|
||||
Get a business subscription to unlock custom CSS or
|
||||
contact us.
|
||||
Get a pro subscription to unlock custom CSS.
|
||||
</Trans>
|
||||
</Text>
|
||||
<Link
|
||||
|
@@ -12,37 +12,50 @@ import SelectOption from '../../UI/SelectOption';
|
||||
import Text from '../../UI/Text';
|
||||
import TextField from '../../UI/TextField';
|
||||
|
||||
import { type LeaderboardSortOption } from '../../Utils/GDevelopServices/Play';
|
||||
import {
|
||||
type LeaderboardSortOption,
|
||||
type Leaderboard,
|
||||
} from '../../Utils/GDevelopServices/Play';
|
||||
import { Column, LargeSpacer, Line } from '../../UI/Grid';
|
||||
import HelpButton from '../../UI/HelpButton';
|
||||
import Checkbox from '../../UI/Checkbox';
|
||||
import { FormHelperText } from '@material-ui/core';
|
||||
import { MarkdownText } from '../../UI/MarkdownText';
|
||||
import SemiControlledTextField from '../../UI/SemiControlledTextField';
|
||||
import AuthenticatedUserContext from '../../Profile/AuthenticatedUserContext';
|
||||
import GetSubscriptionCard from '../../Profile/Subscription/GetSubscriptionCard';
|
||||
|
||||
type SortOptions = {|
|
||||
export type LeaderboardOptions = {|
|
||||
sort: LeaderboardSortOption,
|
||||
extremeAllowedScore: ?number,
|
||||
autoPlayerNamePrefix: string,
|
||||
ignoreCustomPlayerNames: boolean,
|
||||
disableLoginInLeaderboard: boolean,
|
||||
|};
|
||||
|
||||
type Props = {
|
||||
open: boolean,
|
||||
sort: LeaderboardSortOption,
|
||||
extremeAllowedScore?: number,
|
||||
onSave: SortOptions => Promise<void>,
|
||||
leaderboard: Leaderboard,
|
||||
onSave: LeaderboardOptions => Promise<void>,
|
||||
onClose: () => void,
|
||||
};
|
||||
|
||||
const extremeAllowedScoreMax = Number.MAX_SAFE_INTEGER;
|
||||
const extremeAllowedScoreMin = Number.MIN_SAFE_INTEGER;
|
||||
|
||||
function LeaderboardSortOptionsDialog({
|
||||
function LeaderboardOptionsDialog({
|
||||
open,
|
||||
onClose,
|
||||
onSave,
|
||||
sort,
|
||||
extremeAllowedScore,
|
||||
leaderboard,
|
||||
}: Props) {
|
||||
const [isLoading, setIsLoading] = React.useState<boolean>(false);
|
||||
const authenticatedUser = React.useContext(AuthenticatedUserContext);
|
||||
const canDisableLoginInLeaderboard =
|
||||
(authenticatedUser.limits &&
|
||||
authenticatedUser.limits.capabilities.leaderboards
|
||||
.canDisableLoginInLeaderboard) ||
|
||||
false;
|
||||
|
||||
const [
|
||||
extremeAllowedScoreError,
|
||||
@@ -51,16 +64,29 @@ function LeaderboardSortOptionsDialog({
|
||||
const [
|
||||
displayExtremeAllowedScoreInput,
|
||||
setDisplayExtremeAllowedScoreInput,
|
||||
] = React.useState<boolean>(extremeAllowedScore !== undefined);
|
||||
] = React.useState<boolean>(leaderboard.extremeAllowedScore !== undefined);
|
||||
const [
|
||||
extremeAllowedScoreValue,
|
||||
setExtremeAllowedScoreValue,
|
||||
] = React.useState<number>(extremeAllowedScore || 0);
|
||||
] = React.useState<number>(leaderboard.extremeAllowedScore || 0);
|
||||
|
||||
const [sortOrder, setSortOrder] = React.useState<LeaderboardSortOption>(
|
||||
sort || 'ASC'
|
||||
leaderboard.sort || 'ASC'
|
||||
);
|
||||
|
||||
const [
|
||||
autoPlayerNamePrefix,
|
||||
setAutoPlayerNamePrefix,
|
||||
] = React.useState<string>(leaderboard.autoPlayerNamePrefix || '');
|
||||
const [
|
||||
ignoreCustomPlayerNames,
|
||||
setIgnoreCustomPlayerNames,
|
||||
] = React.useState<boolean>(!!leaderboard.ignoreCustomPlayerNames);
|
||||
const [
|
||||
disableLoginInLeaderboard,
|
||||
setDisableLoginInLeaderboard,
|
||||
] = React.useState<boolean>(!!leaderboard.disableLoginInLeaderboard);
|
||||
|
||||
const onSaveSettings = async (i18n: I18nType) => {
|
||||
if (displayExtremeAllowedScoreInput) {
|
||||
if (extremeAllowedScoreValue > extremeAllowedScoreMax) {
|
||||
@@ -92,6 +118,9 @@ function LeaderboardSortOptionsDialog({
|
||||
extremeAllowedScore: displayExtremeAllowedScoreInput
|
||||
? extremeAllowedScoreValue
|
||||
: null,
|
||||
autoPlayerNamePrefix,
|
||||
ignoreCustomPlayerNames,
|
||||
disableLoginInLeaderboard,
|
||||
};
|
||||
await onSave(sortOrderSettings);
|
||||
};
|
||||
@@ -179,40 +208,95 @@ function LeaderboardSortOptionsDialog({
|
||||
)
|
||||
}
|
||||
/>
|
||||
</ColumnStackLayout>
|
||||
{displayExtremeAllowedScoreInput && (
|
||||
<Column noMargin>
|
||||
<Line>
|
||||
<LargeSpacer />
|
||||
<TextField
|
||||
fullWidth
|
||||
type="number"
|
||||
floatingLabelText={
|
||||
sortOrder === 'ASC' ? (
|
||||
<Trans>Minimum score</Trans>
|
||||
) : (
|
||||
<Trans>Maximum score</Trans>
|
||||
)
|
||||
}
|
||||
value={extremeAllowedScoreValue}
|
||||
errorText={extremeAllowedScoreError}
|
||||
min={extremeAllowedScoreMin}
|
||||
max={extremeAllowedScoreMax}
|
||||
onChange={(e, newValue: string) => {
|
||||
if (!!extremeAllowedScoreError) {
|
||||
setExtremeAllowedScoreError(null);
|
||||
{displayExtremeAllowedScoreInput && (
|
||||
<Column noMargin>
|
||||
<Line>
|
||||
<LargeSpacer />
|
||||
<TextField
|
||||
fullWidth
|
||||
type="number"
|
||||
floatingLabelText={
|
||||
sortOrder === 'ASC' ? (
|
||||
<Trans>Minimum score</Trans>
|
||||
) : (
|
||||
<Trans>Maximum score</Trans>
|
||||
)
|
||||
}
|
||||
value={extremeAllowedScoreValue}
|
||||
errorText={extremeAllowedScoreError}
|
||||
min={extremeAllowedScoreMin}
|
||||
max={extremeAllowedScoreMax}
|
||||
onChange={(e, newValue: string) => {
|
||||
if (!!extremeAllowedScoreError) {
|
||||
setExtremeAllowedScoreError(null);
|
||||
}
|
||||
|
||||
setExtremeAllowedScoreValue(parseFloat(newValue));
|
||||
}}
|
||||
/>
|
||||
</Line>
|
||||
</Column>
|
||||
)}
|
||||
setExtremeAllowedScoreValue(parseFloat(newValue));
|
||||
}}
|
||||
/>
|
||||
</Line>
|
||||
</Column>
|
||||
)}
|
||||
<Text size="block-title">
|
||||
<Trans>Connected players</Trans>
|
||||
</Text>
|
||||
<Checkbox
|
||||
label={<Trans>Disable login buttons in leaderboard</Trans>}
|
||||
checked={disableLoginInLeaderboard}
|
||||
disabled={!canDisableLoginInLeaderboard}
|
||||
onCheck={(e, checked) => setDisableLoginInLeaderboard(checked)}
|
||||
tooltipOrHelperText={
|
||||
<Trans>
|
||||
If activated, players won't be able to log in and claim a
|
||||
score just sent without being already logged in to the game.
|
||||
</Trans>
|
||||
}
|
||||
/>
|
||||
{!canDisableLoginInLeaderboard && (
|
||||
<GetSubscriptionCard subscriptionDialogOpeningReason="Leaderboard customization">
|
||||
<Line>
|
||||
<Column noMargin>
|
||||
<Text noMargin>
|
||||
<Trans>
|
||||
Get a pro subscription to get full leaderboard
|
||||
customization.
|
||||
</Trans>
|
||||
</Text>
|
||||
</Column>
|
||||
</Line>
|
||||
</GetSubscriptionCard>
|
||||
)}
|
||||
<Text size="block-title">
|
||||
<Trans>Anonymous players</Trans>
|
||||
</Text>
|
||||
<SemiControlledTextField
|
||||
floatingLabelText={
|
||||
<Trans>
|
||||
Player name prefix (for auto-generated player names)
|
||||
</Trans>
|
||||
}
|
||||
fullWidth
|
||||
maxLength={40}
|
||||
value={autoPlayerNamePrefix}
|
||||
onChange={text => setAutoPlayerNamePrefix(text)}
|
||||
/>
|
||||
<Checkbox
|
||||
label={<Trans>Enforce only auto-generated player names</Trans>}
|
||||
checked={ignoreCustomPlayerNames}
|
||||
onCheck={(e, checked) => setIgnoreCustomPlayerNames(checked)}
|
||||
tooltipOrHelperText={
|
||||
<Trans>
|
||||
If checked, player names will always be auto-generated, even
|
||||
if the game sent a custom name. Helpful if you're having a
|
||||
leaderboard where you want full anonymity.
|
||||
</Trans>
|
||||
}
|
||||
/>
|
||||
</ColumnStackLayout>
|
||||
</Dialog>
|
||||
)}
|
||||
</I18n>
|
||||
);
|
||||
}
|
||||
|
||||
export default LeaderboardSortOptionsDialog;
|
||||
export default LeaderboardOptionsDialog;
|
@@ -28,8 +28,6 @@ import Refresh from '../../UI/CustomSvgIcons/Refresh';
|
||||
import Trash from '../../UI/CustomSvgIcons/Trash';
|
||||
import Visibility from '../../UI/CustomSvgIcons/Visibility';
|
||||
import VisibilityOff from '../../UI/CustomSvgIcons/VisibilityOff';
|
||||
import Lock from '../../UI/CustomSvgIcons/Lock';
|
||||
import LockOpen from '../../UI/CustomSvgIcons/LockOpen';
|
||||
import Copy from '../../UI/CustomSvgIcons/Copy';
|
||||
|
||||
import PlaceholderLoader from '../../UI/PlaceholderLoader';
|
||||
@@ -62,8 +60,9 @@ import Text from '../../UI/Text';
|
||||
import { GameRegistration } from '../GameRegistration';
|
||||
import LeaderboardAppearanceDialog from './LeaderboardAppearanceDialog';
|
||||
import FlatButton from '../../UI/FlatButton';
|
||||
import LeaderboardSortOptionsDialog from './LeaderboardSortOptionsDialog';
|
||||
import { type LeaderboardSortOption } from '../../Utils/GDevelopServices/Play';
|
||||
import LeaderboardOptionsDialog, {
|
||||
type LeaderboardOptions,
|
||||
} from './LeaderboardOptionsDialog';
|
||||
import { formatScore } from '../../Leaderboard/LeaderboardScoreFormatter';
|
||||
import Toggle from '../../UI/Toggle';
|
||||
import AuthenticatedUserContext from '../../Profile/AuthenticatedUserContext';
|
||||
@@ -90,8 +89,6 @@ type ApiError = {|
|
||||
| 'leaderboardNameUpdate'
|
||||
| 'leaderboardSortUpdate'
|
||||
| 'leaderboardVisibilityUpdate'
|
||||
| 'leaderboardAutoPlayerNamePrefixUpdate'
|
||||
| 'leaderboardIgnoreCustomPlayerNamesUpdate'
|
||||
| 'leaderboardPrimaryUpdate'
|
||||
| 'leaderboardAppearanceUpdate'
|
||||
| 'leaderboardPlayerUnicityDisplayChoiceUpdate'
|
||||
@@ -132,10 +129,6 @@ const getApiError = (payload: LeaderboardUpdatePayload): ApiError => ({
|
||||
? 'leaderboardSortUpdate'
|
||||
: payload.visibility
|
||||
? 'leaderboardVisibilityUpdate'
|
||||
: payload.ignoreCustomPlayerNames !== undefined
|
||||
? 'leaderboardIgnoreCustomPlayerNamesUpdate'
|
||||
: payload.autoPlayerNamePrefix !== undefined
|
||||
? 'leaderboardAutoPlayerNamePrefixUpdate'
|
||||
: payload.primary
|
||||
? 'leaderboardPrimaryUpdate'
|
||||
: payload.customizationSettings
|
||||
@@ -156,16 +149,6 @@ const getApiError = (payload: LeaderboardUpdatePayload): ApiError => ({
|
||||
An error occurred when updating the visibility of the leaderboard, please
|
||||
close the dialog, come back and try again.
|
||||
</Trans>
|
||||
) : payload.ignoreCustomPlayerNames !== undefined ? (
|
||||
<Trans>
|
||||
An error occurred when updating the handling of player names of the
|
||||
leaderboard, please close the dialog, come back and try again.
|
||||
</Trans>
|
||||
) : payload.autoPlayerNamePrefix !== undefined ? (
|
||||
<Trans>
|
||||
An error occurred when updating the handling of player names of the
|
||||
leaderboard, please close the dialog, come back and try again.
|
||||
</Trans>
|
||||
) : payload.primary ? (
|
||||
<Trans>
|
||||
An error occurred when setting the leaderboard as default, please close
|
||||
@@ -222,28 +205,16 @@ export const LeaderboardAdmin = ({
|
||||
const authenticatedUser = React.useContext(AuthenticatedUserContext);
|
||||
const { limits } = authenticatedUser;
|
||||
|
||||
const [
|
||||
isEditingSortOptions,
|
||||
setIsEditingSortOptions,
|
||||
] = React.useState<boolean>(false);
|
||||
const [isEditingOptions, setIsEditingOptions] = React.useState<boolean>(
|
||||
false
|
||||
);
|
||||
const [isEditingName, setIsEditingName] = React.useState<boolean>(false);
|
||||
const [
|
||||
isEditingAutoPlayerNamePrefix,
|
||||
setIsEditingAutoPlayerNamePrefix,
|
||||
] = React.useState<boolean>(false);
|
||||
const [isRequestPending, setIsRequestPending] = React.useState<boolean>(
|
||||
false
|
||||
);
|
||||
const [newName, setNewName] = React.useState<string>('');
|
||||
const [newNameError, setNewNameError] = React.useState<?string>(null);
|
||||
const [
|
||||
newAutoPlayerNamePrefix,
|
||||
setNewAutoPlayerNamePrefix,
|
||||
] = React.useState<string>('');
|
||||
const newNameTextFieldRef = React.useRef<?TextFieldInterface>(null);
|
||||
const newAutoPlayerNamePrefixTextFieldRef = React.useRef<?TextFieldInterface>(
|
||||
null
|
||||
);
|
||||
const [apiError, setApiError] = React.useState<?ApiError>(null);
|
||||
const [
|
||||
displayGameRegistration,
|
||||
@@ -295,8 +266,6 @@ export const LeaderboardAdmin = ({
|
||||
try {
|
||||
await updateLeaderboard(payload);
|
||||
if (payload.name) setIsEditingName(false);
|
||||
if (payload.autoPlayerNamePrefix !== undefined)
|
||||
setIsEditingAutoPlayerNamePrefix(false);
|
||||
} catch (err) {
|
||||
console.error('An error occurred when updating leaderboard', err);
|
||||
setApiError(getApiError(payload));
|
||||
@@ -772,7 +741,7 @@ export const LeaderboardAdmin = ({
|
||||
) : null,
|
||||
secondaryAction: (
|
||||
<IconButton
|
||||
onClick={() => setIsEditingSortOptions(true)}
|
||||
onClick={() => setIsEditingOptions(true)}
|
||||
tooltip={t`Edit`}
|
||||
edge="end"
|
||||
disabled={isRequestPending || isEditingName}
|
||||
@@ -835,148 +804,22 @@ export const LeaderboardAdmin = ({
|
||||
),
|
||||
},
|
||||
{
|
||||
key: 'ignoreCustomPlayerNames',
|
||||
avatar: currentLeaderboard.ignoreCustomPlayerNames ? (
|
||||
<Lock />
|
||||
) : (
|
||||
<LockOpen />
|
||||
),
|
||||
key: 'options',
|
||||
avatar: <Tag />,
|
||||
text: (
|
||||
<Tooltip
|
||||
title={i18n._(
|
||||
currentLeaderboard.ignoreCustomPlayerNames
|
||||
? t`Even if the action is used to send a score with a custom player username, this name will be ignored by the leaderboard.`
|
||||
: t`The player name sent in the action to send a score will be used.`
|
||||
)}
|
||||
>
|
||||
<Text size="body2">
|
||||
{currentLeaderboard.ignoreCustomPlayerNames ? (
|
||||
<Trans>Ignore unauthenticated player usernames</Trans>
|
||||
) : (
|
||||
<Trans>Allow unauthenticated player usernames</Trans>
|
||||
)}
|
||||
</Text>
|
||||
</Tooltip>
|
||||
<Text size="body2">
|
||||
<Trans>Configuration</Trans>
|
||||
</Text>
|
||||
),
|
||||
secondaryText:
|
||||
apiError &&
|
||||
apiError.action === 'leaderboardIgnoreCustomPlayerNamesUpdate' ? (
|
||||
<Text color="error" size="body2">
|
||||
{apiError.message}
|
||||
</Text>
|
||||
) : null,
|
||||
secondaryText: null,
|
||||
secondaryAction: (
|
||||
<IconButton
|
||||
onClick={async () => {
|
||||
await onUpdateLeaderboard(i18n, {
|
||||
ignoreCustomPlayerNames: !currentLeaderboard.ignoreCustomPlayerNames,
|
||||
});
|
||||
}}
|
||||
tooltip={
|
||||
currentLeaderboard.ignoreCustomPlayerNames
|
||||
? t`Change to allow custom player usernames`
|
||||
: t`Change to ignore custom player usernames`
|
||||
}
|
||||
onClick={() => setIsEditingOptions(true)}
|
||||
tooltip={t`Edit`}
|
||||
edge="end"
|
||||
disabled={isRequestPending || isEditingName}
|
||||
>
|
||||
<SwitchHorizontal />
|
||||
</IconButton>
|
||||
),
|
||||
},
|
||||
{
|
||||
key: 'autoPlayerNamePrefix',
|
||||
avatar: <Tag />,
|
||||
text: isEditingAutoPlayerNamePrefix ? (
|
||||
<Line alignItems="center" expand noMargin>
|
||||
<TextField
|
||||
id="edit-autoPlayerNamePrefix-field"
|
||||
ref={newAutoPlayerNamePrefixTextFieldRef}
|
||||
margin="none"
|
||||
style={styles.leaderboardNameTextField}
|
||||
maxLength={40}
|
||||
value={newAutoPlayerNamePrefix}
|
||||
onChange={(e, text) => setNewAutoPlayerNamePrefix(text)}
|
||||
onKeyPress={event => {
|
||||
if (shouldValidate(event) && !isRequestPending) {
|
||||
onUpdateLeaderboard(i18n, {
|
||||
autoPlayerNamePrefix: newAutoPlayerNamePrefix,
|
||||
});
|
||||
}
|
||||
}}
|
||||
disabled={isRequestPending}
|
||||
/>
|
||||
{!isRequestPending && (
|
||||
<>
|
||||
<Spacer />
|
||||
<IconButton
|
||||
tooltip={t`Cancel`}
|
||||
style={{ padding: 0 }}
|
||||
onClick={() => {
|
||||
setIsEditingAutoPlayerNamePrefix(false);
|
||||
}}
|
||||
>
|
||||
<Cross />
|
||||
</IconButton>
|
||||
</>
|
||||
)}
|
||||
</Line>
|
||||
) : (
|
||||
<Tooltip
|
||||
title={
|
||||
currentLeaderboard.autoPlayerNamePrefix ||
|
||||
i18n._('No custom prefix for auto-generated player names')
|
||||
}
|
||||
>
|
||||
<Text size="body2" style={styles.leaderboardNameText}>
|
||||
{currentLeaderboard.autoPlayerNamePrefix ||
|
||||
i18n._('No custom prefix for auto-generated player names')}
|
||||
</Text>
|
||||
</Tooltip>
|
||||
),
|
||||
secondaryText:
|
||||
apiError &&
|
||||
apiError.action === 'leaderboardAutoPlayerNamePrefixUpdate' ? (
|
||||
<Text color="error" size="body2">
|
||||
{apiError.message}
|
||||
</Text>
|
||||
) : null,
|
||||
secondaryAction: (
|
||||
<IconButton
|
||||
onClick={() => {
|
||||
if (isEditingAutoPlayerNamePrefix) {
|
||||
onUpdateLeaderboard(i18n, {
|
||||
autoPlayerNamePrefix: newAutoPlayerNamePrefix,
|
||||
});
|
||||
} else {
|
||||
setNewAutoPlayerNamePrefix(
|
||||
currentLeaderboard.autoPlayerNamePrefix || ''
|
||||
);
|
||||
setIsEditingAutoPlayerNamePrefix(true);
|
||||
}
|
||||
}}
|
||||
tooltip={
|
||||
isEditingAutoPlayerNamePrefix
|
||||
? t`Save`
|
||||
: t`Change the default prefix for player names`
|
||||
}
|
||||
disabled={isRequestPending}
|
||||
edge="end"
|
||||
id={
|
||||
isEditingAutoPlayerNamePrefix
|
||||
? 'save-autoPlayerNamePrefix-button'
|
||||
: 'edit-autoPlayerNamePrefix-button'
|
||||
}
|
||||
>
|
||||
{isEditingAutoPlayerNamePrefix ? (
|
||||
isRequestPending ? (
|
||||
<CircularProgress size={20} />
|
||||
) : (
|
||||
<Save />
|
||||
)
|
||||
) : (
|
||||
<Edit />
|
||||
)}
|
||||
<EditFile />
|
||||
</IconButton>
|
||||
),
|
||||
},
|
||||
@@ -985,7 +828,7 @@ export const LeaderboardAdmin = ({
|
||||
avatar: <TextFormat />,
|
||||
text: (
|
||||
<Text size="body2">
|
||||
<Trans>Leaderboard appearance</Trans>
|
||||
<Trans>Appearance</Trans>
|
||||
</Text>
|
||||
),
|
||||
secondaryText:
|
||||
@@ -1290,24 +1133,20 @@ export const LeaderboardAdmin = ({
|
||||
}}
|
||||
/>
|
||||
) : null}
|
||||
{isEditingSortOptions && currentLeaderboard ? (
|
||||
<LeaderboardSortOptionsDialog
|
||||
{isEditingOptions && currentLeaderboard ? (
|
||||
<LeaderboardOptionsDialog
|
||||
open
|
||||
onClose={() => setIsEditingSortOptions(false)}
|
||||
onSave={async (sortOptions: {|
|
||||
sort: LeaderboardSortOption,
|
||||
extremeAllowedScore: ?number,
|
||||
|}) => {
|
||||
onClose={() => setIsEditingOptions(false)}
|
||||
onSave={async (options: LeaderboardOptions) => {
|
||||
try {
|
||||
await onUpdateLeaderboard(i18n, {
|
||||
...sortOptions,
|
||||
...options,
|
||||
});
|
||||
} finally {
|
||||
setIsEditingSortOptions(false);
|
||||
setIsEditingOptions(false);
|
||||
}
|
||||
}}
|
||||
sort={currentLeaderboard.sort}
|
||||
extremeAllowedScore={currentLeaderboard.extremeAllowedScore}
|
||||
leaderboard={currentLeaderboard}
|
||||
/>
|
||||
) : null}
|
||||
</>
|
||||
|
@@ -49,9 +49,9 @@ export const findGDJS = (
|
||||
let gdjsRoot = `https://resources.gdevelop-app.com/GDJS-${getIDEVersion()}`;
|
||||
|
||||
// If you want to test your local changes to the game engine on the local web-app,
|
||||
// run `npx serve --cors` (or another CORS enabled http server on port 5000)
|
||||
// run `npx serve --cors -p 5001` (or another CORS enabled http server on port 5001)
|
||||
// in `newIDE/app/resources/GDJS` and uncomment this line:
|
||||
// gdjsRoot = `http://localhost:5000`;
|
||||
// gdjsRoot = `http://localhost:5001`;
|
||||
|
||||
return Promise.all(
|
||||
filesToDownload[fileSet].map(relativeFilePath => {
|
||||
|
@@ -138,10 +138,9 @@ export const getExtraInstructionInformation = (type: string): ?Hint => {
|
||||
if (type.indexOf('P2P::') === 0) {
|
||||
return {
|
||||
kind: 'warning',
|
||||
message: t`P2P is a peer-to-peer networking solution. It leaks IP addresses to other players - it is made to play with friends, and is not suitable for playing with unknown players.
|
||||
P2P also only allows to exchange messages and leaves implementation of higher-level tasks, such as synchronizing the game state, to you.
|
||||
message: t`P2P is merely a peer-to-peer networking solution. It only handles the connection to another player, and the exchange of messages. Higher-level tasks, such as synchronizing the game state, are left to by implemented by you.
|
||||
|
||||
Use the THNK Framework if you seek an easier, more performant and more flexible solution suitable for playing with strangers.`,
|
||||
Use the THNK Framework if you seek an easy, performant and flexible higher-level solution.`,
|
||||
identifier: 'p2p-is-networking',
|
||||
};
|
||||
}
|
||||
|
@@ -0,0 +1,614 @@
|
||||
// @flow
|
||||
|
||||
import * as React from 'react';
|
||||
import { type I18n as I18nType } from '@lingui/core';
|
||||
import { t } from '@lingui/macro';
|
||||
|
||||
import { type Schema, type SectionTitle } from '../../CompactPropertiesEditor';
|
||||
import enumerateLayers from '../../LayersList/EnumerateLayers';
|
||||
import { styles } from '.';
|
||||
|
||||
import Layers from '../../UI/CustomSvgIcons/Layers';
|
||||
import LetterX from '../../UI/CustomSvgIcons/LetterX';
|
||||
import LetterY from '../../UI/CustomSvgIcons/LetterY';
|
||||
import LetterH from '../../UI/CustomSvgIcons/LetterH';
|
||||
import LetterW from '../../UI/CustomSvgIcons/LetterW';
|
||||
import LetterD from '../../UI/CustomSvgIcons/LetterD';
|
||||
import LetterZ from '../../UI/CustomSvgIcons/LetterZ';
|
||||
import Instance from '../../UI/CustomSvgIcons/Instance';
|
||||
import Link from '../../UI/CustomSvgIcons/Link';
|
||||
import Unlink from '../../UI/CustomSvgIcons/Unlink';
|
||||
import RemoveCircle from '../../UI/CustomSvgIcons/RemoveCircle';
|
||||
import Lock from '../../UI/CustomSvgIcons/Lock';
|
||||
import LockOpen from '../../UI/CustomSvgIcons/LockOpen';
|
||||
import Restore from '../../UI/CustomSvgIcons/Restore';
|
||||
import Object3d from '../../UI/CustomSvgIcons/Object3d';
|
||||
import Object2d from '../../UI/CustomSvgIcons/Object2d';
|
||||
import RotateX from '../../UI/CustomSvgIcons/RotateX';
|
||||
import RotateY from '../../UI/CustomSvgIcons/RotateY';
|
||||
import RotateZ from '../../UI/CustomSvgIcons/RotateZ';
|
||||
|
||||
/**
|
||||
* Applies ratio to value without intermediary value to avoid precision issues.
|
||||
*/
|
||||
const applyRatio = ({
|
||||
oldReferenceValue,
|
||||
newReferenceValue,
|
||||
valueToApplyTo,
|
||||
}: {|
|
||||
oldReferenceValue: number,
|
||||
newReferenceValue: number,
|
||||
valueToApplyTo: number,
|
||||
|}) => {
|
||||
return (newReferenceValue / oldReferenceValue) * valueToApplyTo;
|
||||
};
|
||||
|
||||
const getEditObjectButton = ({
|
||||
i18n,
|
||||
onEditObjectByName,
|
||||
is3DInstance,
|
||||
}: {|
|
||||
i18n: I18nType,
|
||||
onEditObjectByName: (name: string) => void,
|
||||
is3DInstance: boolean,
|
||||
|}) => ({
|
||||
label: i18n._(t`Edit object`),
|
||||
disabled: 'onValuesDifferent',
|
||||
nonFieldType: 'button',
|
||||
getIcon: is3DInstance
|
||||
? props => <Object3d {...props} />
|
||||
: props => <Object2d {...props} />,
|
||||
getValue: (instance: gdInitialInstance) => instance.getObjectName(),
|
||||
onClick: (instance: gdInitialInstance) =>
|
||||
onEditObjectByName(instance.getObjectName()),
|
||||
});
|
||||
|
||||
const getRotationXAndRotationYFields = ({ i18n }: {| i18n: I18nType |}) => [
|
||||
{
|
||||
name: 'Rotation X',
|
||||
getLabel: () => i18n._(t`Rotation (X)`),
|
||||
valueType: 'number',
|
||||
getValue: (instance: gdInitialInstance) => instance.getRotationX(),
|
||||
setValue: (instance: gdInitialInstance, newValue: number) =>
|
||||
instance.setRotationX(newValue),
|
||||
renderLeftIcon: className => <RotateX className={className} />,
|
||||
},
|
||||
{
|
||||
name: 'Rotation Y',
|
||||
getLabel: () => i18n._(t`Rotation (Y)`),
|
||||
valueType: 'number',
|
||||
getValue: (instance: gdInitialInstance) => instance.getRotationY(),
|
||||
setValue: (instance: gdInitialInstance, newValue: number) =>
|
||||
instance.setRotationY(newValue),
|
||||
renderLeftIcon: className => <RotateY className={className} />,
|
||||
},
|
||||
];
|
||||
const getRotationZField = ({ i18n }: {| i18n: I18nType |}) => ({
|
||||
name: 'Angle',
|
||||
getLabel: () => i18n._(t`Rotation (Z)`),
|
||||
valueType: 'number',
|
||||
getValue: (instance: gdInitialInstance) => instance.getAngle(),
|
||||
setValue: (instance: gdInitialInstance, newValue: number) =>
|
||||
instance.setAngle(newValue),
|
||||
renderLeftIcon: className => <RotateZ className={className} />,
|
||||
});
|
||||
const getXAndYFields = ({ i18n }: {| i18n: I18nType |}): Schema => [
|
||||
{
|
||||
name: 'X',
|
||||
getLabel: () => i18n._(t`X`),
|
||||
valueType: 'number',
|
||||
getValue: (instance: gdInitialInstance) => instance.getX(),
|
||||
setValue: (instance: gdInitialInstance, newValue: number) =>
|
||||
instance.setX(newValue),
|
||||
renderLeftIcon: className => <LetterX className={className} />,
|
||||
},
|
||||
{
|
||||
name: 'Y',
|
||||
getLabel: () => i18n._(t`Y`),
|
||||
valueType: 'number',
|
||||
getValue: (instance: gdInitialInstance) => instance.getY(),
|
||||
setValue: (instance: gdInitialInstance, newValue: number) =>
|
||||
instance.setY(newValue),
|
||||
renderLeftIcon: className => <LetterY className={className} />,
|
||||
},
|
||||
];
|
||||
const getZField = ({ i18n }: {| i18n: I18nType |}) => ({
|
||||
name: 'Z',
|
||||
getLabel: () => i18n._(t`Z`),
|
||||
valueType: 'number',
|
||||
getValue: (instance: gdInitialInstance) => instance.getZ(),
|
||||
setValue: (instance: gdInitialInstance, newValue: number) =>
|
||||
instance.setZ(newValue),
|
||||
renderLeftIcon: className => <LetterZ className={className} />,
|
||||
});
|
||||
const getLayerField = ({
|
||||
i18n,
|
||||
layout,
|
||||
}: {|
|
||||
i18n: I18nType,
|
||||
layout: gdLayout,
|
||||
|}) => ({
|
||||
name: 'Layer',
|
||||
getLabel: () => i18n._(t`Layer`),
|
||||
valueType: 'string',
|
||||
getChoices: () => enumerateLayers(layout),
|
||||
getValue: (instance: gdInitialInstance) => instance.getLayer(),
|
||||
setValue: (instance: gdInitialInstance, newValue: string) =>
|
||||
instance.setLayer(newValue),
|
||||
renderLeftIcon: className => <Layers className={className} />,
|
||||
});
|
||||
const getZOrderField = ({ i18n }: {| i18n: I18nType |}) => ({
|
||||
name: 'Z Order',
|
||||
getLabel: () => i18n._(t`Z Order`),
|
||||
valueType: 'number',
|
||||
getValue: (instance: gdInitialInstance) => instance.getZOrder(),
|
||||
setValue: (instance: gdInitialInstance, newValue: number) =>
|
||||
instance.setZOrder(newValue),
|
||||
renderLeftIcon: className => <LetterZ className={className} />,
|
||||
});
|
||||
|
||||
const getTitleRow = ({ i18n }: {| i18n: I18nType |}) => ({
|
||||
name: 'Title',
|
||||
type: 'row',
|
||||
preventWrap: true,
|
||||
children: [
|
||||
{
|
||||
name: 'Instance',
|
||||
title: i18n._(t`Instance`),
|
||||
renderLeftIcon: className => (
|
||||
<Instance className={className} style={styles.icon} />
|
||||
),
|
||||
getValue: (instance: gdInitialInstance) => instance.getObjectName(),
|
||||
nonFieldType: 'title',
|
||||
defaultValue: i18n._(t`Different objects`),
|
||||
},
|
||||
{
|
||||
name: 'Lock instance',
|
||||
getLabel: (instance: gdInitialInstance) =>
|
||||
instance.isSealed()
|
||||
? i18n._(t`Free instance`)
|
||||
: instance.isLocked()
|
||||
? i18n._(t`Prevent selection in the editor`)
|
||||
: i18n._(t`Lock position/angle in the editor`),
|
||||
valueType: 'enumIcon',
|
||||
renderIcon: value =>
|
||||
value === 'sealed' ? (
|
||||
<RemoveCircle style={styles.icon} />
|
||||
) : value === 'locked' ? (
|
||||
<Lock style={styles.icon} />
|
||||
) : (
|
||||
<LockOpen style={styles.icon} />
|
||||
),
|
||||
isHighlighted: value => value === 'locked' || value === 'sealed',
|
||||
getValue: (instance: gdInitialInstance) =>
|
||||
instance.isSealed()
|
||||
? 'sealed'
|
||||
: instance.isLocked()
|
||||
? 'locked'
|
||||
: 'free',
|
||||
setValue: (
|
||||
instance: gdInitialInstance,
|
||||
newValue: 'sealed' | 'locked' | 'free'
|
||||
) => {
|
||||
instance.setSealed(newValue === 'sealed' ? true : false);
|
||||
instance.setLocked(
|
||||
newValue === 'sealed' || newValue === 'locked' ? true : false
|
||||
);
|
||||
},
|
||||
getNextValue: (
|
||||
currentValue: 'sealed' | 'locked' | 'free'
|
||||
): 'sealed' | 'locked' | 'free' => {
|
||||
if (currentValue === 'free') return 'locked';
|
||||
if (currentValue === 'locked') return 'sealed';
|
||||
return 'free';
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
const getWidthField = ({
|
||||
i18n,
|
||||
getInstanceWidth,
|
||||
getInstanceHeight,
|
||||
getInstanceDepth,
|
||||
forceUpdate,
|
||||
}: {|
|
||||
i18n: I18nType,
|
||||
getInstanceWidth: gdInitialInstance => number,
|
||||
getInstanceHeight: gdInitialInstance => number,
|
||||
getInstanceDepth: gdInitialInstance => number,
|
||||
forceUpdate: () => void,
|
||||
|}) => ({
|
||||
name: 'Width',
|
||||
getLabel: () => i18n._(t`Width`),
|
||||
valueType: 'number',
|
||||
getValue: getInstanceWidth,
|
||||
setValue: (instance: gdInitialInstance, newValue: number) => {
|
||||
const shouldKeepRatio = instance.shouldKeepRatio();
|
||||
const newWidth = Math.max(newValue, 0);
|
||||
if (shouldKeepRatio) {
|
||||
const initialWidth = getInstanceWidth(instance) || 1;
|
||||
instance.setCustomWidth(newWidth);
|
||||
instance.setCustomHeight(
|
||||
applyRatio({
|
||||
oldReferenceValue: initialWidth,
|
||||
newReferenceValue: newWidth,
|
||||
valueToApplyTo: getInstanceHeight(instance),
|
||||
})
|
||||
);
|
||||
instance.setCustomDepth(
|
||||
applyRatio({
|
||||
oldReferenceValue: initialWidth,
|
||||
newReferenceValue: newWidth,
|
||||
valueToApplyTo: getInstanceDepth(instance),
|
||||
})
|
||||
);
|
||||
} else {
|
||||
instance.setCustomWidth(newWidth);
|
||||
instance.setCustomHeight(getInstanceHeight(instance));
|
||||
instance.setCustomDepth(getInstanceDepth(instance));
|
||||
}
|
||||
|
||||
// This must be done after reading the size.
|
||||
instance.setHasCustomSize(true);
|
||||
instance.setHasCustomDepth(true);
|
||||
forceUpdate();
|
||||
},
|
||||
renderLeftIcon: className => <LetterW className={className} />,
|
||||
getEndAdornmentIcon: (instance: gdInitialInstance) => {
|
||||
if (instance.hasCustomDepth() || instance.hasCustomSize()) {
|
||||
return className => <Restore className={className} />;
|
||||
}
|
||||
return null;
|
||||
},
|
||||
onClickEndAdornment: (instance: gdInitialInstance) => {
|
||||
instance.setHasCustomSize(false);
|
||||
instance.setHasCustomDepth(false);
|
||||
forceUpdate();
|
||||
},
|
||||
});
|
||||
const getHeightField = ({
|
||||
i18n,
|
||||
getInstanceWidth,
|
||||
getInstanceHeight,
|
||||
getInstanceDepth,
|
||||
forceUpdate,
|
||||
}: {|
|
||||
i18n: I18nType,
|
||||
getInstanceWidth: gdInitialInstance => number,
|
||||
getInstanceHeight: gdInitialInstance => number,
|
||||
getInstanceDepth: gdInitialInstance => number,
|
||||
forceUpdate: () => void,
|
||||
|}) => ({
|
||||
name: 'Height',
|
||||
getLabel: () => i18n._(t`Height`),
|
||||
valueType: 'number',
|
||||
getValue: getInstanceHeight,
|
||||
setValue: (instance: gdInitialInstance, newValue: number) => {
|
||||
const shouldKeepRatio = instance.shouldKeepRatio();
|
||||
const newHeight = Math.max(newValue, 0);
|
||||
if (shouldKeepRatio) {
|
||||
const initialHeight = getInstanceHeight(instance) || 1;
|
||||
instance.setCustomWidth(
|
||||
applyRatio({
|
||||
oldReferenceValue: initialHeight,
|
||||
newReferenceValue: newHeight,
|
||||
valueToApplyTo: getInstanceWidth(instance),
|
||||
})
|
||||
);
|
||||
instance.setCustomHeight(newHeight);
|
||||
instance.setCustomDepth(
|
||||
applyRatio({
|
||||
oldReferenceValue: initialHeight,
|
||||
newReferenceValue: newHeight,
|
||||
valueToApplyTo: getInstanceDepth(instance),
|
||||
})
|
||||
);
|
||||
} else {
|
||||
instance.setCustomWidth(getInstanceWidth(instance));
|
||||
instance.setCustomHeight(newHeight);
|
||||
instance.setCustomDepth(getInstanceDepth(instance));
|
||||
}
|
||||
|
||||
// This must be done after reading the size.
|
||||
instance.setHasCustomSize(true);
|
||||
instance.setHasCustomDepth(true);
|
||||
forceUpdate();
|
||||
},
|
||||
renderLeftIcon: className => <LetterH className={className} />,
|
||||
getEndAdornmentIcon: (instance: gdInitialInstance) => {
|
||||
if (instance.hasCustomDepth() || instance.hasCustomSize()) {
|
||||
return className => <Restore className={className} />;
|
||||
}
|
||||
return null;
|
||||
},
|
||||
onClickEndAdornment: (instance: gdInitialInstance) => {
|
||||
instance.setHasCustomSize(false);
|
||||
instance.setHasCustomDepth(false);
|
||||
forceUpdate();
|
||||
},
|
||||
});
|
||||
const getDepthField = ({
|
||||
i18n,
|
||||
getInstanceWidth,
|
||||
getInstanceHeight,
|
||||
getInstanceDepth,
|
||||
forceUpdate,
|
||||
}: {|
|
||||
i18n: I18nType,
|
||||
getInstanceWidth: gdInitialInstance => number,
|
||||
getInstanceHeight: gdInitialInstance => number,
|
||||
getInstanceDepth: gdInitialInstance => number,
|
||||
forceUpdate: () => void,
|
||||
|}) => ({
|
||||
name: 'Depth',
|
||||
getLabel: () => i18n._(t`Depth`),
|
||||
valueType: 'number',
|
||||
getValue: getInstanceDepth,
|
||||
setValue: (instance: gdInitialInstance, newValue: number) => {
|
||||
const shouldKeepRatio = instance.shouldKeepRatio();
|
||||
const newDepth = Math.max(newValue, 0);
|
||||
if (shouldKeepRatio) {
|
||||
const initialDepth = getInstanceDepth(instance) || 1;
|
||||
instance.setCustomWidth(
|
||||
applyRatio({
|
||||
oldReferenceValue: initialDepth,
|
||||
newReferenceValue: newDepth,
|
||||
valueToApplyTo: getInstanceWidth(instance),
|
||||
})
|
||||
);
|
||||
instance.setCustomHeight(
|
||||
applyRatio({
|
||||
oldReferenceValue: initialDepth,
|
||||
newReferenceValue: newDepth,
|
||||
valueToApplyTo: getInstanceHeight(instance),
|
||||
})
|
||||
);
|
||||
instance.setCustomDepth(newDepth);
|
||||
} else {
|
||||
instance.setCustomWidth(getInstanceWidth(instance));
|
||||
instance.setCustomHeight(getInstanceHeight(instance));
|
||||
instance.setCustomDepth(newDepth);
|
||||
}
|
||||
|
||||
// This must be done after reading the size.
|
||||
instance.setHasCustomSize(true);
|
||||
instance.setHasCustomDepth(true);
|
||||
forceUpdate();
|
||||
},
|
||||
renderLeftIcon: className => <LetterD className={className} />,
|
||||
getEndAdornmentIcon: (instance: gdInitialInstance) => {
|
||||
if (instance.hasCustomDepth() || instance.hasCustomSize()) {
|
||||
return className => <Restore className={className} />;
|
||||
}
|
||||
return null;
|
||||
},
|
||||
onClickEndAdornment: (instance: gdInitialInstance) => {
|
||||
instance.setHasCustomSize(false);
|
||||
instance.setHasCustomDepth(false);
|
||||
forceUpdate();
|
||||
},
|
||||
});
|
||||
const getKeepRatioField = ({
|
||||
i18n,
|
||||
getInstanceWidth,
|
||||
getInstanceHeight,
|
||||
getInstanceDepth,
|
||||
forceUpdate,
|
||||
}: {|
|
||||
i18n: I18nType,
|
||||
getInstanceWidth: gdInitialInstance => number,
|
||||
getInstanceHeight: gdInitialInstance => number,
|
||||
getInstanceDepth: gdInitialInstance => number,
|
||||
forceUpdate: () => void,
|
||||
|}) => ({
|
||||
name: 'Keep ratio',
|
||||
getLabel: () => i18n._(t`Keep ratio`),
|
||||
valueType: 'enumIcon',
|
||||
isHighlighted: value => value,
|
||||
renderIcon: value =>
|
||||
value ? <Link style={styles.icon} /> : <Unlink style={styles.icon} />,
|
||||
getValue: (instance: gdInitialInstance) => instance.shouldKeepRatio(),
|
||||
setValue: (instance: gdInitialInstance, newValue: boolean) =>
|
||||
instance.setShouldKeepRatio(newValue),
|
||||
getNextValue: (currentValue: boolean) => !currentValue,
|
||||
});
|
||||
|
||||
export const makeSchema = ({
|
||||
is3DInstance,
|
||||
i18n,
|
||||
forceUpdate,
|
||||
onEditObjectByName,
|
||||
onGetInstanceSize,
|
||||
layout,
|
||||
}: {|
|
||||
is3DInstance: boolean,
|
||||
i18n: I18nType,
|
||||
forceUpdate: () => void,
|
||||
onEditObjectByName: (name: string) => void,
|
||||
onGetInstanceSize: gdInitialInstance => [number, number, number],
|
||||
layout: gdLayout,
|
||||
|}): Schema => {
|
||||
const getInstanceWidth = (instance: gdInitialInstance) =>
|
||||
instance.hasCustomSize()
|
||||
? instance.getCustomWidth()
|
||||
: onGetInstanceSize(instance)[0];
|
||||
|
||||
const getInstanceHeight = (instance: gdInitialInstance) =>
|
||||
instance.hasCustomSize()
|
||||
? instance.getCustomHeight()
|
||||
: onGetInstanceSize(instance)[1];
|
||||
|
||||
const getInstanceDepth = (instance: gdInitialInstance) =>
|
||||
instance.hasCustomDepth()
|
||||
? instance.getCustomDepth()
|
||||
: onGetInstanceSize(instance)[2];
|
||||
|
||||
if (is3DInstance) {
|
||||
return [
|
||||
getTitleRow({ i18n }),
|
||||
getEditObjectButton({ i18n, onEditObjectByName, is3DInstance }),
|
||||
{
|
||||
name: 'Position',
|
||||
type: 'row',
|
||||
preventWrap: true,
|
||||
removeSpacers: true,
|
||||
children: [...getXAndYFields({ i18n }), getZField({ i18n })],
|
||||
},
|
||||
{
|
||||
name: 'Size',
|
||||
type: 'row',
|
||||
preventWrap: true,
|
||||
children: [
|
||||
{
|
||||
name: 'Custom size',
|
||||
type: 'column',
|
||||
children: [
|
||||
getWidthField({
|
||||
i18n,
|
||||
getInstanceWidth,
|
||||
getInstanceHeight,
|
||||
getInstanceDepth,
|
||||
forceUpdate,
|
||||
}),
|
||||
getHeightField({
|
||||
i18n,
|
||||
getInstanceWidth,
|
||||
getInstanceHeight,
|
||||
getInstanceDepth,
|
||||
forceUpdate,
|
||||
}),
|
||||
getDepthField({
|
||||
i18n,
|
||||
getInstanceWidth,
|
||||
getInstanceHeight,
|
||||
getInstanceDepth,
|
||||
forceUpdate,
|
||||
}),
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'Keep ratio column',
|
||||
nonFieldType: 'verticalCenterWithBar',
|
||||
child: getKeepRatioField({
|
||||
i18n,
|
||||
getInstanceWidth,
|
||||
getInstanceHeight,
|
||||
getInstanceDepth,
|
||||
forceUpdate,
|
||||
}),
|
||||
},
|
||||
],
|
||||
},
|
||||
getLayerField({ i18n, layout }),
|
||||
{
|
||||
name: 'Rotation',
|
||||
type: 'row',
|
||||
title: i18n._(t`Rotation`),
|
||||
preventWrap: true,
|
||||
removeSpacers: true,
|
||||
children: getRotationXAndRotationYFields({ i18n }),
|
||||
},
|
||||
getRotationZField({ i18n }),
|
||||
];
|
||||
}
|
||||
|
||||
return [
|
||||
getTitleRow({ i18n }),
|
||||
getEditObjectButton({ i18n, onEditObjectByName, is3DInstance }),
|
||||
{
|
||||
name: 'Position',
|
||||
type: 'row',
|
||||
preventWrap: true,
|
||||
removeSpacers: true,
|
||||
children: getXAndYFields({ i18n }),
|
||||
},
|
||||
getZOrderField({ i18n }),
|
||||
{
|
||||
name: 'Size',
|
||||
type: 'row',
|
||||
preventWrap: true,
|
||||
children: [
|
||||
{
|
||||
name: 'Custom size',
|
||||
type: 'column',
|
||||
children: [
|
||||
getWidthField({
|
||||
i18n,
|
||||
getInstanceWidth,
|
||||
getInstanceHeight,
|
||||
getInstanceDepth,
|
||||
forceUpdate,
|
||||
}),
|
||||
getHeightField({
|
||||
i18n,
|
||||
getInstanceWidth,
|
||||
getInstanceHeight,
|
||||
getInstanceDepth,
|
||||
forceUpdate,
|
||||
}),
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'Keep ratio column',
|
||||
nonFieldType: 'verticalCenterWithBar',
|
||||
child: getKeepRatioField({
|
||||
i18n,
|
||||
getInstanceWidth,
|
||||
getInstanceHeight,
|
||||
getInstanceDepth,
|
||||
forceUpdate,
|
||||
}),
|
||||
},
|
||||
],
|
||||
},
|
||||
getLayerField({ i18n, layout }),
|
||||
{
|
||||
name: 'Rotation',
|
||||
type: 'row',
|
||||
title: i18n._(t`Rotation`),
|
||||
preventWrap: true,
|
||||
removeSpacers: true,
|
||||
children: [getRotationZField({ i18n })],
|
||||
},
|
||||
];
|
||||
};
|
||||
|
||||
export const reorderInstanceSchemaForCustomProperties = (
|
||||
schema: Schema,
|
||||
i18n: I18nType
|
||||
): Schema => {
|
||||
const newSchema = [...schema];
|
||||
const animationFieldIndex = newSchema.findIndex(
|
||||
field => field.name && field.name === 'animation'
|
||||
);
|
||||
const contentSectionTitle: SectionTitle = {
|
||||
nonFieldType: 'sectionTitle',
|
||||
name: 'Content',
|
||||
title: 'Content',
|
||||
getValue: undefined,
|
||||
};
|
||||
if (animationFieldIndex === -1) {
|
||||
if (newSchema.length > 0) {
|
||||
newSchema.unshift(contentSectionTitle);
|
||||
}
|
||||
return newSchema;
|
||||
}
|
||||
|
||||
const [animationField] = newSchema.splice(animationFieldIndex, 1);
|
||||
|
||||
const firstFields: Schema = [
|
||||
{
|
||||
name: 'Animation',
|
||||
type: 'row',
|
||||
title: i18n._(t`Animation`),
|
||||
// $FlowIgnore - We are confident the animation field is not a row or a column.
|
||||
children: [{ ...animationField, hideLabel: true }],
|
||||
},
|
||||
];
|
||||
if (newSchema.length > 0) {
|
||||
firstFields.push(contentSectionTitle);
|
||||
}
|
||||
newSchema.unshift(...firstFields);
|
||||
return newSchema;
|
||||
};
|
@@ -0,0 +1,232 @@
|
||||
// @flow
|
||||
import * as React from 'react';
|
||||
import { Trans } from '@lingui/macro';
|
||||
import { type I18n as I18nType } from '@lingui/core';
|
||||
|
||||
import Paper from '../../UI/Paper';
|
||||
import EmptyMessage from '../../UI/EmptyMessage';
|
||||
import CompactPropertiesEditor, {
|
||||
Separator,
|
||||
} from '../../CompactPropertiesEditor';
|
||||
import propertiesMapToSchema from '../../CompactPropertiesEditor/PropertiesMapToCompactSchema';
|
||||
import { type Schema } from '../../CompactPropertiesEditor';
|
||||
import getObjectByName from '../../Utils/GetObjectByName';
|
||||
import IconButton from '../../UI/IconButton';
|
||||
import { Line, Column, Spacer, marginsSize } from '../../UI/Grid';
|
||||
import Text from '../../UI/Text';
|
||||
import { type UnsavedChanges } from '../../MainFrame/UnsavedChangesContext';
|
||||
import ScrollView from '../../UI/ScrollView';
|
||||
import EventsRootVariablesFinder from '../../Utils/EventsRootVariablesFinder';
|
||||
import VariablesList, {
|
||||
type HistoryHandler,
|
||||
} from '../../VariablesList/VariablesList';
|
||||
import ShareExternal from '../../UI/CustomSvgIcons/ShareExternal';
|
||||
import useForceUpdate from '../../Utils/UseForceUpdate';
|
||||
import ErrorBoundary from '../../UI/ErrorBoundary';
|
||||
import {
|
||||
makeSchema,
|
||||
reorderInstanceSchemaForCustomProperties,
|
||||
} from './CompactPropertiesSchema';
|
||||
|
||||
export const styles = {
|
||||
paper: {
|
||||
display: 'flex',
|
||||
flex: 1,
|
||||
minWidth: 0,
|
||||
flexDirection: 'column',
|
||||
},
|
||||
icon: {
|
||||
fontSize: 18,
|
||||
},
|
||||
scrollView: { paddingTop: marginsSize },
|
||||
};
|
||||
|
||||
const gd: libGDevelop = global.gd;
|
||||
|
||||
type Props = {|
|
||||
project: gdProject,
|
||||
layout: gdLayout,
|
||||
instances: Array<gdInitialInstance>,
|
||||
onEditObjectByName: string => void,
|
||||
onInstancesModified?: (Array<gdInitialInstance>) => void,
|
||||
onGetInstanceSize: gdInitialInstance => [number, number, number],
|
||||
editInstanceVariables: gdInitialInstance => void,
|
||||
unsavedChanges?: ?UnsavedChanges,
|
||||
i18n: I18nType,
|
||||
historyHandler?: HistoryHandler,
|
||||
|};
|
||||
|
||||
export type CompactInstancePropertiesEditorInterface = {|
|
||||
forceUpdate: () => void,
|
||||
|};
|
||||
|
||||
const CompactInstancePropertiesEditor = ({
|
||||
instances,
|
||||
i18n,
|
||||
project,
|
||||
layout,
|
||||
unsavedChanges,
|
||||
historyHandler,
|
||||
onEditObjectByName,
|
||||
onGetInstanceSize,
|
||||
editInstanceVariables,
|
||||
onInstancesModified,
|
||||
}: Props) => {
|
||||
const forceUpdate = useForceUpdate();
|
||||
|
||||
const schemaFor2D: Schema = React.useMemo(
|
||||
() =>
|
||||
makeSchema({
|
||||
i18n,
|
||||
is3DInstance: false,
|
||||
onGetInstanceSize,
|
||||
onEditObjectByName,
|
||||
layout,
|
||||
forceUpdate,
|
||||
}),
|
||||
[i18n, onGetInstanceSize, onEditObjectByName, layout, forceUpdate]
|
||||
);
|
||||
|
||||
const schemaFor3D: Schema = React.useMemo(
|
||||
() =>
|
||||
makeSchema({
|
||||
i18n,
|
||||
is3DInstance: true,
|
||||
onGetInstanceSize,
|
||||
onEditObjectByName,
|
||||
layout,
|
||||
forceUpdate,
|
||||
}),
|
||||
[i18n, onGetInstanceSize, onEditObjectByName, layout, forceUpdate]
|
||||
);
|
||||
|
||||
// TODO: multiple instances support.
|
||||
const instance = instances[0];
|
||||
|
||||
const { object, instanceSchema } = React.useMemo<{|
|
||||
object?: gdObject,
|
||||
instanceSchema?: Schema,
|
||||
|}>(
|
||||
() => {
|
||||
if (!instance) return { object: undefined, instanceSchema: undefined };
|
||||
|
||||
const associatedObjectName = instance.getObjectName();
|
||||
const object = getObjectByName(project, layout, associatedObjectName);
|
||||
const properties = instance.getCustomProperties(project, layout);
|
||||
if (!object) return { object: undefined, instanceSchema: undefined };
|
||||
|
||||
const is3DInstance = gd.MetadataProvider.getObjectMetadata(
|
||||
project.getCurrentPlatform(),
|
||||
object.getType()
|
||||
).isRenderedIn3D();
|
||||
const instanceSchemaForCustomProperties = propertiesMapToSchema(
|
||||
properties,
|
||||
(instance: gdInitialInstance) =>
|
||||
instance.getCustomProperties(project, layout),
|
||||
(instance: gdInitialInstance, name, value) =>
|
||||
instance.updateCustomProperty(name, value, project, layout)
|
||||
);
|
||||
|
||||
const reorderedInstanceSchemaForCustomProperties = reorderInstanceSchemaForCustomProperties(
|
||||
instanceSchemaForCustomProperties,
|
||||
i18n
|
||||
);
|
||||
return {
|
||||
object,
|
||||
instanceSchema: is3DInstance
|
||||
? schemaFor3D.concat(reorderedInstanceSchemaForCustomProperties)
|
||||
: schemaFor2D.concat(reorderedInstanceSchemaForCustomProperties),
|
||||
};
|
||||
},
|
||||
[project, layout, instance, schemaFor2D, schemaFor3D, i18n]
|
||||
);
|
||||
|
||||
if (!object || !instance || !instanceSchema) return null;
|
||||
|
||||
return (
|
||||
<ErrorBoundary
|
||||
componentTitle={<Trans>Instance properties</Trans>}
|
||||
scope="scene-editor-instance-properties"
|
||||
>
|
||||
<ScrollView
|
||||
autoHideScrollbar
|
||||
style={styles.scrollView}
|
||||
key={instances
|
||||
.map((instance: gdInitialInstance) => '' + instance.ptr)
|
||||
.join(';')}
|
||||
>
|
||||
<Column expand noMargin id="instance-properties-editor">
|
||||
<Column>
|
||||
<CompactPropertiesEditor
|
||||
unsavedChanges={unsavedChanges}
|
||||
schema={instanceSchema}
|
||||
instances={instances}
|
||||
onInstancesModified={onInstancesModified}
|
||||
/>
|
||||
<Spacer />
|
||||
<Separator />
|
||||
<Line alignItems="center" justifyContent="space-between">
|
||||
<Text size="sub-title" noMargin>
|
||||
<Trans>Instance Variables</Trans>
|
||||
</Text>
|
||||
<IconButton
|
||||
size="small"
|
||||
onClick={() => {
|
||||
editInstanceVariables(instance);
|
||||
}}
|
||||
>
|
||||
<ShareExternal style={styles.icon} />
|
||||
</IconButton>
|
||||
</Line>
|
||||
</Column>
|
||||
{object ? (
|
||||
<VariablesList
|
||||
directlyStoreValueChangesWhileEditing
|
||||
inheritedVariablesContainer={object.getVariables()}
|
||||
variablesContainer={instance.getVariables()}
|
||||
size="small"
|
||||
onComputeAllVariableNames={() =>
|
||||
object
|
||||
? EventsRootVariablesFinder.findAllObjectVariables(
|
||||
project.getCurrentPlatform(),
|
||||
project,
|
||||
layout,
|
||||
object
|
||||
)
|
||||
: []
|
||||
}
|
||||
historyHandler={historyHandler}
|
||||
toolbarIconStyle={styles.icon}
|
||||
/>
|
||||
) : null}
|
||||
</Column>
|
||||
</ScrollView>
|
||||
</ErrorBoundary>
|
||||
);
|
||||
};
|
||||
|
||||
const CompactInstancePropertiesEditorContainer = React.forwardRef<
|
||||
Props,
|
||||
CompactInstancePropertiesEditorInterface
|
||||
>((props, ref) => {
|
||||
const forceUpdate = useForceUpdate();
|
||||
React.useImperativeHandle(ref, () => ({
|
||||
forceUpdate,
|
||||
}));
|
||||
|
||||
return (
|
||||
<Paper background="dark" square style={styles.paper}>
|
||||
{!props.instances || !props.instances.length ? (
|
||||
<EmptyMessage>
|
||||
<Trans>
|
||||
Click on an instance in the scene to display its properties
|
||||
</Trans>
|
||||
</EmptyMessage>
|
||||
) : (
|
||||
<CompactInstancePropertiesEditor {...props} />
|
||||
)}
|
||||
</Paper>
|
||||
);
|
||||
});
|
||||
|
||||
export default CompactInstancePropertiesEditorContainer;
|
@@ -37,9 +37,9 @@ export default class LayerRenderer {
|
||||
onMoveInstanceEnd: void => void;
|
||||
onDownInstance: (gdInitialInstance, number, number) => void;
|
||||
onUpInstance: (gdInitialInstance, number, number) => void;
|
||||
/**Used for instances culling on rendering */
|
||||
/** Used for instances culling on rendering. */
|
||||
viewTopLeft: [number, number];
|
||||
/** Used for instances culling on rendering */
|
||||
/** Used for instances culling on rendering. */
|
||||
viewBottomRight: [number, number];
|
||||
|
||||
renderedInstances: { [number]: RenderedInstance | Rendered3DInstance } = {};
|
||||
@@ -134,8 +134,8 @@ export default class LayerRenderer {
|
||||
this.onDownInstance = onDownInstance;
|
||||
this.onUpInstance = onUpInstance;
|
||||
|
||||
this.viewTopLeft = [0, 0]; // Used for instances culling on rendering
|
||||
this.viewBottomRight = [0, 0]; // Used for instances culling on rendering
|
||||
this.viewTopLeft = [0, 0];
|
||||
this.viewBottomRight = [0, 0];
|
||||
|
||||
this.pixiContainer = new PIXI.Container();
|
||||
|
||||
@@ -182,6 +182,9 @@ export default class LayerRenderer {
|
||||
|
||||
if (renderedInstance instanceof Rendered3DInstance) {
|
||||
const threeObject = renderedInstance.getThreeObject();
|
||||
if (threeObject) {
|
||||
threeObject.visible = isVisible;
|
||||
}
|
||||
if (this._threeGroup && threeObject) {
|
||||
this._threeGroup.add(threeObject);
|
||||
}
|
||||
@@ -256,7 +259,7 @@ export default class LayerRenderer {
|
||||
};
|
||||
|
||||
getUnrotatedInstanceSize = (instance: gdInitialInstance) => {
|
||||
const renderedInstance = this.renderedInstances[instance.ptr];
|
||||
const renderedInstance = this.getRendererOfInstance(instance);
|
||||
const hasCustomSize = instance.hasCustomSize();
|
||||
const hasCustomDepth = instance.hasCustomDepth();
|
||||
const width = hasCustomSize
|
||||
@@ -516,11 +519,17 @@ export default class LayerRenderer {
|
||||
}
|
||||
|
||||
_computeViewBounds() {
|
||||
// Add a margin of 100 pixels around the view. Culling will hide PIXI objects,
|
||||
// and hidden objects won't respond to events. Hence, a margin allow the cursor to go
|
||||
// slightly out of the canvas when moving an instance, and still have the instance
|
||||
// to follow the cursor.
|
||||
const margin = 100;
|
||||
/**
|
||||
* Add a margin around the view. Culling will hide PIXI and THREE objects,
|
||||
* and hidden objects won't respond to events.
|
||||
* Hence, this margin allows for two things:
|
||||
* - it allows the cursor to go slightly out of the canvas when moving an
|
||||
* instance, and still have the instance to follow the cursor.
|
||||
* - THREE objects, depending on their shape and orientation, should appear
|
||||
* on the screen even though their coordinates are off the view. This margin
|
||||
* should cover most of the cases.
|
||||
*/
|
||||
const margin = 1000;
|
||||
this.viewTopLeft = this.viewPosition.toSceneCoordinates(-margin, -margin);
|
||||
this.viewBottomRight = this.viewPosition.toSceneCoordinates(
|
||||
this.viewPosition.getWidth() + margin,
|
||||
|
@@ -13,6 +13,7 @@ import {
|
||||
} from './InstancesResizer';
|
||||
import { type InstanceMeasurer } from './InstancesRenderer';
|
||||
import Rectangle from '../Utils/Rectangle';
|
||||
import KeyboardShortcuts from '../UI/KeyboardShortcuts';
|
||||
|
||||
type Props = {|
|
||||
instancesSelection: InstancesSelection,
|
||||
@@ -27,6 +28,9 @@ type Props = {|
|
||||
onRotateEnd: () => void,
|
||||
toCanvasCoordinates: (x: number, y: number) => [number, number],
|
||||
screenType: ScreenType,
|
||||
keyboardShortcuts: KeyboardShortcuts,
|
||||
onPanMove: (deltaX: number, deltaY: number, x: number, y: number) => void,
|
||||
onPanEnd: () => void,
|
||||
|};
|
||||
|
||||
const getButtonSizes = (screenType: ScreenType) => {
|
||||
@@ -77,12 +81,24 @@ export default class SelectedInstances {
|
||||
onRotateEnd: () => void;
|
||||
toCanvasCoordinates: (x: number, y: number) => [number, number];
|
||||
_screenType: ScreenType;
|
||||
keyboardShortcuts: KeyboardShortcuts;
|
||||
onPanMove: (deltaX: number, deltaY: number, x: number, y: number) => void;
|
||||
onPanEnd: () => void;
|
||||
|
||||
pixiContainer = new PIXI.Container();
|
||||
rectanglesContainer = new PIXI.Container();
|
||||
selectedRectangles = [];
|
||||
resizeButtons: { [ResizeGrabbingLocation]: PIXI.Graphics } = {};
|
||||
rotateButton = new PIXI.Graphics();
|
||||
/**
|
||||
* When moving the view, a click on the buttons generated by this class
|
||||
* won't trigger a resize and interrupt the move of the view.
|
||||
* Similarly, when resizing an instance, if we start to move the view
|
||||
* (space key pressed), the resizing is interrupted to move the view.
|
||||
* This state is used to detect the change and call the "end" function
|
||||
* associated of the proper interaction (buttonInteraction or viewMoving).
|
||||
*/
|
||||
_currentPanMovingGoal: null | 'buttonInteraction' | 'viewMoving' = null;
|
||||
|
||||
constructor({
|
||||
instancesSelection,
|
||||
@@ -93,6 +109,9 @@ export default class SelectedInstances {
|
||||
onRotateEnd,
|
||||
toCanvasCoordinates,
|
||||
screenType,
|
||||
keyboardShortcuts,
|
||||
onPanMove,
|
||||
onPanEnd,
|
||||
}: Props) {
|
||||
this.instanceMeasurer = instanceMeasurer;
|
||||
this.onResize = onResize;
|
||||
@@ -102,51 +121,114 @@ export default class SelectedInstances {
|
||||
this.toCanvasCoordinates = toCanvasCoordinates;
|
||||
this.instancesSelection = instancesSelection;
|
||||
this._screenType = screenType;
|
||||
this.keyboardShortcuts = keyboardShortcuts;
|
||||
this.onPanMove = onPanMove;
|
||||
this.onPanEnd = onPanEnd;
|
||||
|
||||
this.pixiContainer.addChild(this.rectanglesContainer);
|
||||
|
||||
for (const resizeGrabbingLocation of resizeGrabbingLocationValues) {
|
||||
const resizeButton = new PIXI.Graphics();
|
||||
this.resizeButtons[resizeGrabbingLocation] = resizeButton;
|
||||
this._makeButton(
|
||||
resizeButton,
|
||||
event => {
|
||||
this._makeButton({
|
||||
objectButton: resizeButton,
|
||||
onMove: event => {
|
||||
this.onResize(event.deltaX, event.deltaY, resizeGrabbingLocation);
|
||||
},
|
||||
() => {
|
||||
onEnd: () => {
|
||||
this.onResizeEnd();
|
||||
},
|
||||
resizeGrabbingIconNames[resizeGrabbingLocation]
|
||||
);
|
||||
onPanMove: event => {
|
||||
this.onPanMove(
|
||||
event.deltaX,
|
||||
event.deltaY,
|
||||
event.data.global.x,
|
||||
event.data.global.y
|
||||
);
|
||||
},
|
||||
onPanEnd: () => {
|
||||
this.onPanEnd();
|
||||
},
|
||||
cursor: resizeGrabbingIconNames[resizeGrabbingLocation],
|
||||
});
|
||||
}
|
||||
this._makeButton(
|
||||
this.rotateButton,
|
||||
event => {
|
||||
this._makeButton({
|
||||
objectButton: this.rotateButton,
|
||||
onMove: event => {
|
||||
this.onRotate(event.deltaX, event.deltaY);
|
||||
},
|
||||
() => {
|
||||
onEnd: () => {
|
||||
this.onRotateEnd();
|
||||
},
|
||||
'url("res/actions/rotate24_black.png"),auto'
|
||||
);
|
||||
onPanMove: event => {
|
||||
this.onPanMove(
|
||||
event.deltaX,
|
||||
event.deltaY,
|
||||
event.data.global.x,
|
||||
event.data.global.y
|
||||
);
|
||||
},
|
||||
onPanEnd: () => {
|
||||
this.onPanEnd();
|
||||
},
|
||||
cursor: 'url("res/actions/rotate24_black.png"),auto',
|
||||
});
|
||||
}
|
||||
|
||||
setScreenType(screenType: ScreenType) {
|
||||
this._screenType = screenType;
|
||||
}
|
||||
|
||||
_makeButton(
|
||||
_makeButton({
|
||||
objectButton,
|
||||
onMove,
|
||||
onEnd,
|
||||
onPanMove,
|
||||
onPanEnd,
|
||||
cursor,
|
||||
}: {
|
||||
objectButton: PIXI.Graphics,
|
||||
onMove: (event: PanMoveEvent) => void,
|
||||
onEnd: () => void,
|
||||
cursor: string
|
||||
) {
|
||||
onPanMove: (event: PanMoveEvent) => void,
|
||||
onPanEnd: () => void,
|
||||
cursor: string,
|
||||
}) {
|
||||
objectButton.eventMode = 'static';
|
||||
objectButton.buttonMode = true;
|
||||
objectButton.cursor = cursor;
|
||||
panable(objectButton);
|
||||
objectButton.addEventListener('panmove', onMove);
|
||||
objectButton.addEventListener('panend', onEnd);
|
||||
|
||||
// change cursor style if space is pressed
|
||||
objectButton.addEventListener('mousemove', event => {
|
||||
if (this.keyboardShortcuts.shouldMoveView()) {
|
||||
objectButton.cursor = 'grab';
|
||||
} else {
|
||||
objectButton.cursor = cursor;
|
||||
}
|
||||
});
|
||||
|
||||
objectButton.addEventListener('panmove', event => {
|
||||
if (this.keyboardShortcuts.shouldMoveView()) {
|
||||
if (this._currentPanMovingGoal === 'buttonInteraction') {
|
||||
onEnd();
|
||||
}
|
||||
onPanMove(event);
|
||||
this._currentPanMovingGoal = 'viewMoving';
|
||||
} else {
|
||||
onMove(event);
|
||||
this._currentPanMovingGoal = 'buttonInteraction';
|
||||
}
|
||||
});
|
||||
|
||||
objectButton.addEventListener('panend', event => {
|
||||
if (this.keyboardShortcuts.shouldMoveView()) {
|
||||
onPanEnd();
|
||||
} else {
|
||||
onEnd();
|
||||
}
|
||||
this._currentPanMovingGoal = null;
|
||||
});
|
||||
this.pixiContainer.addChild(objectButton);
|
||||
}
|
||||
|
||||
|
@@ -449,6 +449,9 @@ export default class InstancesEditor extends Component<Props> {
|
||||
instanceMeasurer: this.instancesRenderer.getInstanceMeasurer(),
|
||||
toCanvasCoordinates: this.viewPosition.toCanvasCoordinates,
|
||||
screenType: this.props.screenType,
|
||||
keyboardShortcuts: this.keyboardShortcuts,
|
||||
onPanMove: this._onPanMove,
|
||||
onPanEnd: this._onPanEnd,
|
||||
});
|
||||
this.highlightedInstance = new HighlightedInstance({
|
||||
instanceMeasurer: this.instancesRenderer.getInstanceMeasurer(),
|
||||
|
@@ -33,6 +33,11 @@ import PlanRecommendationRow from './PlanRecommendationRow';
|
||||
import { SurveyCard } from './SurveyCard';
|
||||
import PlaceholderLoader from '../../../../UI/PlaceholderLoader';
|
||||
import PromotionsSlideshow from '../../../../Promotions/PromotionsSlideshow';
|
||||
import { PrivateTutorialViewDialog } from '../../../../AssetStore/PrivateTutorials/PrivateTutorialViewDialog';
|
||||
import {
|
||||
GithubStarCard,
|
||||
shouldDisplayGithubStarCard,
|
||||
} from '../../../../Profile/GithubStarCard';
|
||||
|
||||
const styles = {
|
||||
textTutorialContent: {
|
||||
@@ -177,6 +182,7 @@ type Props = {|
|
||||
subscriptionPlansWithPricingSystems: ?(SubscriptionPlanWithPricingSystems[]),
|
||||
onStartSurvey: null | (() => void),
|
||||
hasFilledSurveyAlready: boolean,
|
||||
onOpenProfile: () => void,
|
||||
|};
|
||||
|
||||
const RecommendationList = ({
|
||||
@@ -185,11 +191,23 @@ const RecommendationList = ({
|
||||
subscriptionPlansWithPricingSystems,
|
||||
onStartSurvey,
|
||||
hasFilledSurveyAlready,
|
||||
onOpenProfile,
|
||||
}: Props) => {
|
||||
const { recommendations, subscription, profile } = authenticatedUser;
|
||||
const {
|
||||
recommendations,
|
||||
subscription,
|
||||
limits,
|
||||
badges,
|
||||
achievements,
|
||||
} = authenticatedUser;
|
||||
const { tutorials } = React.useContext(TutorialContext);
|
||||
const { getTutorialProgress } = React.useContext(PreferencesContext);
|
||||
|
||||
const [
|
||||
selectedTutorial,
|
||||
setSelectedTutorial,
|
||||
] = React.useState<Tutorial | null>(null);
|
||||
|
||||
if (!recommendations) return <PlaceholderLoader />;
|
||||
|
||||
const recommendedTutorials = tutorials
|
||||
@@ -222,7 +240,11 @@ const RecommendationList = ({
|
||||
recommendation => recommendation.type === 'plan'
|
||||
);
|
||||
|
||||
const getTutorialPartProgress = ({ tutorialId }: { tutorialId: string }) => {
|
||||
const getInAppTutorialPartProgress = ({
|
||||
tutorialId,
|
||||
}: {
|
||||
tutorialId: string,
|
||||
}) => {
|
||||
const tutorialProgress = getTutorialProgress({
|
||||
tutorialId,
|
||||
userId: authenticatedUser.profile
|
||||
@@ -251,7 +273,7 @@ const RecommendationList = ({
|
||||
if (guidedLessonsRecommendation) {
|
||||
const displayTextAfterGuidedLessons = guidedLessonsIds
|
||||
? guidedLessonsIds
|
||||
.map(tutorialId => getTutorialPartProgress({ tutorialId }))
|
||||
.map(tutorialId => getInAppTutorialPartProgress({ tutorialId }))
|
||||
.every(progress => progress === 100)
|
||||
: false;
|
||||
|
||||
@@ -282,7 +304,12 @@ const RecommendationList = ({
|
||||
title={<Trans>Get started with game creation</Trans>}
|
||||
margin="dense"
|
||||
items={recommendedVideoTutorials.map(tutorial =>
|
||||
formatTutorialToImageTileComponent(i18n, tutorial)
|
||||
formatTutorialToImageTileComponent({
|
||||
i18n,
|
||||
limits,
|
||||
tutorial,
|
||||
onSelectTutorial: setSelectedTutorial,
|
||||
})
|
||||
)}
|
||||
getColumnsFromWindowSize={getVideoTutorialsColumnsFromWidth}
|
||||
getLimitFromWindowSize={getTutorialsLimitsFromWidth}
|
||||
@@ -308,6 +335,13 @@ const RecommendationList = ({
|
||||
</Text>
|
||||
<Spacer />
|
||||
<PromotionsSlideshow />
|
||||
<Spacer />
|
||||
{shouldDisplayGithubStarCard({ badges }) && (
|
||||
<GithubStarCard
|
||||
achievements={achievements}
|
||||
onOpenProfile={onOpenProfile}
|
||||
/>
|
||||
)}
|
||||
</SectionRow>
|
||||
);
|
||||
|
||||
@@ -320,8 +354,11 @@ const RecommendationList = ({
|
||||
}
|
||||
if (planRecommendation) {
|
||||
const shouldDisplayPlanRecommendation =
|
||||
profile &&
|
||||
!profile.isStudent &&
|
||||
limits &&
|
||||
!(
|
||||
limits.capabilities.classrooms &&
|
||||
limits.capabilities.classrooms.hideUpgradeNotice
|
||||
) &&
|
||||
(!subscription ||
|
||||
isPlanRecommendationRelevant(subscription, planRecommendation));
|
||||
if (
|
||||
@@ -341,7 +378,18 @@ const RecommendationList = ({
|
||||
);
|
||||
}
|
||||
}
|
||||
return items;
|
||||
|
||||
return (
|
||||
<>
|
||||
{items}
|
||||
{selectedTutorial && (
|
||||
<PrivateTutorialViewDialog
|
||||
tutorial={selectedTutorial}
|
||||
onClose={() => setSelectedTutorial(null)}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}}
|
||||
</I18n>
|
||||
);
|
||||
|
@@ -1,27 +1,11 @@
|
||||
// @flow
|
||||
import * as React from 'react';
|
||||
import GDevelopThemeContext from '../../../../UI/Theme/GDevelopThemeContext';
|
||||
import RaisedButton from '../../../../UI/RaisedButton';
|
||||
import { Trans } from '@lingui/macro';
|
||||
import { Line, Spacer } from '../../../../UI/Grid';
|
||||
import {
|
||||
ColumnStackLayout,
|
||||
ResponsiveLineStackLayout,
|
||||
} from '../../../../UI/Layout';
|
||||
import { ColumnStackLayout } from '../../../../UI/Layout';
|
||||
import Text from '../../../../UI/Text';
|
||||
|
||||
const styles = {
|
||||
subscriptionContainer: {
|
||||
display: 'flex',
|
||||
borderRadius: 10,
|
||||
alignItems: 'center',
|
||||
padding: 16,
|
||||
},
|
||||
surveyIcon: {
|
||||
width: 200,
|
||||
height: 105,
|
||||
},
|
||||
};
|
||||
import { CalloutCard } from '../../../../UI/CalloutCard';
|
||||
|
||||
type Props = {|
|
||||
onStartSurvey: () => void,
|
||||
@@ -32,52 +16,42 @@ export const SurveyCard = ({
|
||||
onStartSurvey,
|
||||
hasFilledSurveyAlready,
|
||||
}: Props) => {
|
||||
const gdevelopTheme = React.useContext(GDevelopThemeContext);
|
||||
|
||||
const subscriptionContainerStyle = {
|
||||
...styles.subscriptionContainer,
|
||||
border: `1px solid ${gdevelopTheme.palette.secondary}`,
|
||||
};
|
||||
|
||||
return (
|
||||
<div style={subscriptionContainerStyle}>
|
||||
<ResponsiveLineStackLayout noMargin>
|
||||
<img
|
||||
src="res/start-survey.svg"
|
||||
style={styles.surveyIcon}
|
||||
alt="Survey"
|
||||
/>
|
||||
<Line noMargin expand>
|
||||
<ColumnStackLayout alignItems="flex-start" expand>
|
||||
<Text noMargin size="block-title">
|
||||
{hasFilledSurveyAlready ? (
|
||||
<Trans>Have you changed your usage of GDevelop?</Trans>
|
||||
<CalloutCard
|
||||
renderImage={style => (
|
||||
<img src="res/start-survey.svg" style={style} alt="Survey" />
|
||||
)}
|
||||
>
|
||||
<Line noMargin expand>
|
||||
<ColumnStackLayout alignItems="flex-start" expand>
|
||||
<Text noMargin size="block-title">
|
||||
{hasFilledSurveyAlready ? (
|
||||
<Trans>Have you changed your usage of GDevelop?</Trans>
|
||||
) : (
|
||||
<Trans>Personalize your suggested content</Trans>
|
||||
)}
|
||||
</Text>
|
||||
<Text noMargin size="body">
|
||||
<Trans>
|
||||
Answer a 1-minute survey to personalize your “Get started”
|
||||
content.
|
||||
</Trans>
|
||||
</Text>
|
||||
<Spacer />
|
||||
<RaisedButton
|
||||
label={
|
||||
hasFilledSurveyAlready ? (
|
||||
<Trans>Redo the survey</Trans>
|
||||
) : (
|
||||
<Trans>Personalize your suggested content</Trans>
|
||||
)}
|
||||
</Text>
|
||||
<Text noMargin size="body">
|
||||
<Trans>
|
||||
Answer a 1-minute survey to personalize your “Get started”
|
||||
content.
|
||||
</Trans>
|
||||
</Text>
|
||||
<Spacer />
|
||||
<RaisedButton
|
||||
label={
|
||||
hasFilledSurveyAlready ? (
|
||||
<Trans>Redo the survey</Trans>
|
||||
) : (
|
||||
<Trans>Start the survey!</Trans>
|
||||
)
|
||||
}
|
||||
primary
|
||||
onClick={onStartSurvey}
|
||||
/>
|
||||
<Spacer />
|
||||
</ColumnStackLayout>
|
||||
</Line>
|
||||
</ResponsiveLineStackLayout>
|
||||
</div>
|
||||
<Trans>Start the survey!</Trans>
|
||||
)
|
||||
}
|
||||
primary
|
||||
onClick={onStartSurvey}
|
||||
/>
|
||||
<Spacer />
|
||||
</ColumnStackLayout>
|
||||
</Line>
|
||||
</CalloutCard>
|
||||
);
|
||||
};
|
||||
|
@@ -90,6 +90,7 @@ type Props = {|
|
||||
onUserSurveyHidden: () => void,
|
||||
selectInAppTutorial: (tutorialId: string) => void,
|
||||
subscriptionPlansWithPricingSystems: ?(SubscriptionPlanWithPricingSystems[]),
|
||||
onOpenProfile: () => void,
|
||||
|};
|
||||
|
||||
const GetStartedSection = ({
|
||||
@@ -97,6 +98,7 @@ const GetStartedSection = ({
|
||||
onUserSurveyStarted,
|
||||
onUserSurveyHidden,
|
||||
subscriptionPlansWithPricingSystems,
|
||||
onOpenProfile,
|
||||
}: Props) => {
|
||||
const isFillingOutSurvey = hasStartedUserSurvey();
|
||||
const isOnline = useOnlineStatus();
|
||||
@@ -182,7 +184,7 @@ const GetStartedSection = ({
|
||||
// Artificial delay to build up expectations.
|
||||
recommendationsGettingDelayPromise.current = delay(2500);
|
||||
await Promise.all([
|
||||
onEditProfile({ survey }, preferences, { throwError: true }),
|
||||
onEditProfile({ survey }, preferences),
|
||||
recommendationsGettingDelayPromise.current,
|
||||
]);
|
||||
sendUserSurveyCompleted();
|
||||
@@ -639,6 +641,7 @@ const GetStartedSection = ({
|
||||
subscriptionPlansWithPricingSystems={
|
||||
subscriptionPlansWithPricingSystems
|
||||
}
|
||||
onOpenProfile={onOpenProfile}
|
||||
onStartSurvey={
|
||||
profile
|
||||
? () => {
|
||||
|
@@ -17,7 +17,7 @@ import Preferences from '../../../UI/CustomSvgIcons/Preferences';
|
||||
import GDevelopGLogo from '../../../UI/CustomSvgIcons/GDevelopGLogo';
|
||||
import GDevelopThemeContext from '../../../UI/Theme/GDevelopThemeContext';
|
||||
import HomePageMenuBar from './HomePageMenuBar';
|
||||
import type { Profile } from '../../../Utils/GDevelopServices/Authentication';
|
||||
import type { Limits } from '../../../Utils/GDevelopServices/Usage';
|
||||
import AuthenticatedUserContext from '../../../Profile/AuthenticatedUserContext';
|
||||
import GraphsIcon from '../../../UI/CustomSvgIcons/Graphs';
|
||||
|
||||
@@ -129,12 +129,20 @@ const homePageMenuTabs: { [tab: string]: HomePageMenuTab } = {
|
||||
};
|
||||
|
||||
export const getTabsToDisplay = ({
|
||||
profile,
|
||||
limits,
|
||||
}: {|
|
||||
profile: ?Profile,
|
||||
limits: ?Limits,
|
||||
|}): HomePageMenuTab[] => {
|
||||
const displayTeamViewTab = profile && profile.isTeacher;
|
||||
const displayPlayTab = !profile || !profile.isStudent;
|
||||
const displayTeamViewTab =
|
||||
limits &&
|
||||
limits.capabilities.classrooms &&
|
||||
limits.capabilities.classrooms.showClassroomTab;
|
||||
const displayPlayTab =
|
||||
!limits ||
|
||||
!(
|
||||
limits.capabilities.classrooms &&
|
||||
limits.capabilities.classrooms.hidePlayTab
|
||||
);
|
||||
const tabs = [
|
||||
'get-started',
|
||||
'build',
|
||||
@@ -162,13 +170,13 @@ export const HomePageMenu = ({
|
||||
onOpenAbout,
|
||||
}: Props) => {
|
||||
const gdevelopTheme = React.useContext(GDevelopThemeContext);
|
||||
const { profile } = React.useContext(AuthenticatedUserContext);
|
||||
const { limits } = React.useContext(AuthenticatedUserContext);
|
||||
const [
|
||||
isHomePageMenuDrawerOpen,
|
||||
setIsHomePageMenuDrawerOpen,
|
||||
] = React.useState(false);
|
||||
|
||||
const tabsToDisplay = getTabsToDisplay({ profile });
|
||||
const tabsToDisplay = getTabsToDisplay({ limits });
|
||||
|
||||
const buttons: {
|
||||
label: React.Node,
|
||||
|
@@ -21,11 +21,19 @@ import { Toolbar, ToolbarGroup } from '../../../UI/Toolbar';
|
||||
import AuthenticatedUserContext from '../../../Profile/AuthenticatedUserContext';
|
||||
|
||||
const iconSize = 20;
|
||||
const iconButtonPaddingVertical = 8;
|
||||
const iconButtonPaddingHorizontal = 5;
|
||||
const iconButtonLabelPadding = 6;
|
||||
const iconButtonPaddingTop = 8;
|
||||
/**
|
||||
* Padding bottom is bigger than padding top to leave space for the Android/iOS
|
||||
* bottom navigation bar.
|
||||
*/
|
||||
const iconButtonPaddingBottom = 20;
|
||||
const iconButtonPaddingHorizontal = 8;
|
||||
const iconButtonLabelPadding = 4;
|
||||
const toolbarHeight =
|
||||
iconSize + 2 * iconButtonLabelPadding + 2 * iconButtonPaddingVertical;
|
||||
iconSize +
|
||||
2 * iconButtonLabelPadding +
|
||||
iconButtonPaddingTop +
|
||||
iconButtonPaddingBottom;
|
||||
export const homepageDesktopMenuBarWidth = 230;
|
||||
export const homepageMediumMenuBarWidth =
|
||||
verticalTabButtonSize + 2 * marginsSize;
|
||||
@@ -53,7 +61,7 @@ export const styles = {
|
||||
marginBottom: 'env(safe-area-inset-bottom)',
|
||||
},
|
||||
mobileButton: {
|
||||
padding: `${iconButtonPaddingVertical}px ${iconButtonPaddingHorizontal}px`,
|
||||
padding: `${iconButtonPaddingTop}px ${iconButtonPaddingHorizontal}px ${iconButtonPaddingBottom}px ${iconButtonPaddingHorizontal}px`,
|
||||
fontSize: 'inherit',
|
||||
},
|
||||
buttonLabel: { padding: iconButtonLabelPadding, display: 'flex' },
|
||||
@@ -77,8 +85,8 @@ const HomePageMenuBar = ({
|
||||
const { isMobile, isMediumScreen } = useResponsiveWindowSize();
|
||||
const isMobileOrSmallScreen = isMobile || isMediumScreen;
|
||||
const theme = React.useContext(GDevelopThemeContext);
|
||||
const { profile } = React.useContext(AuthenticatedUserContext);
|
||||
const tabsToDisplay = getTabsToDisplay({ profile });
|
||||
const { limits } = React.useContext(AuthenticatedUserContext);
|
||||
const tabsToDisplay = getTabsToDisplay({ limits });
|
||||
const largeScreenOnlyButtons: {
|
||||
label: React.Node,
|
||||
getIcon: GetIconFunction,
|
||||
|
@@ -0,0 +1,62 @@
|
||||
// @flow
|
||||
import * as React from 'react';
|
||||
import RaisedButton from '../../../../UI/RaisedButton';
|
||||
import { Trans } from '@lingui/macro';
|
||||
import { Spacer } from '../../../../UI/Grid';
|
||||
import {
|
||||
ColumnStackLayout,
|
||||
ResponsiveLineStackLayout,
|
||||
} from '../../../../UI/Layout';
|
||||
import Text from '../../../../UI/Text';
|
||||
import TextButton from '../../../../UI/TextButton';
|
||||
import Window from '../../../../Utils/Window';
|
||||
import { CalloutCard } from '../../../../UI/CalloutCard';
|
||||
|
||||
type Props = {|
|
||||
onSeeResources: () => void,
|
||||
|};
|
||||
|
||||
export const EducationCard = ({ onSeeResources }: Props) => {
|
||||
return (
|
||||
<CalloutCard
|
||||
renderImage={style => (
|
||||
<img
|
||||
src="res/education-resources.svg"
|
||||
style={style}
|
||||
alt="Content for teachers"
|
||||
/>
|
||||
)}
|
||||
>
|
||||
<ResponsiveLineStackLayout noMargin expand alignItems="stretch">
|
||||
<ColumnStackLayout alignItems="flex-start" expand noMargin>
|
||||
<Text noMargin size="block-title">
|
||||
<Trans>Content for Teachers</Trans>
|
||||
</Text>
|
||||
<Text noMargin size="body">
|
||||
<Trans>
|
||||
Access GDevelop’s resources for teaching game development and
|
||||
promote careers in technology.
|
||||
</Trans>
|
||||
</Text>
|
||||
<Spacer />
|
||||
</ColumnStackLayout>
|
||||
<Spacer />
|
||||
<ColumnStackLayout justifyContent="center" alignItems="center" noMargin>
|
||||
<RaisedButton
|
||||
label={<Trans>See resources</Trans>}
|
||||
primary
|
||||
fullWidth
|
||||
onClick={onSeeResources}
|
||||
/>
|
||||
<TextButton
|
||||
label={<Trans>About education plan</Trans>}
|
||||
fullWidth
|
||||
onClick={() => {
|
||||
Window.openExternalURL('https://gdevelop.io/pricing/education');
|
||||
}}
|
||||
/>
|
||||
</ColumnStackLayout>
|
||||
</ResponsiveLineStackLayout>
|
||||
</CalloutCard>
|
||||
);
|
||||
};
|
@@ -1,7 +1,7 @@
|
||||
// @flow
|
||||
import * as React from 'react';
|
||||
import { I18n } from '@lingui/react';
|
||||
import { Line, Column } from '../../../../UI/Grid';
|
||||
import { Line, Column, Spacer } from '../../../../UI/Grid';
|
||||
import Text from '../../../../UI/Text';
|
||||
import Window from '../../../../Utils/Window';
|
||||
import { Trans } from '@lingui/macro';
|
||||
@@ -29,6 +29,10 @@ import ChevronArrowRight from '../../../../UI/CustomSvgIcons/ChevronArrowRight';
|
||||
import Upload from '../../../../UI/CustomSvgIcons/Upload';
|
||||
import WikiSearchBar from '../../../../UI/WikiSearchBar';
|
||||
import FlingGame from '../InAppTutorials/FlingGame';
|
||||
import AuthenticatedUserContext from '../../../../Profile/AuthenticatedUserContext';
|
||||
import { type Limits } from '../../../../Utils/GDevelopServices/Usage';
|
||||
import { PrivateTutorialViewDialog } from '../../../../AssetStore/PrivateTutorials/PrivateTutorialViewDialog';
|
||||
import { EducationCard } from './EducationCard';
|
||||
|
||||
const useStyles = makeStyles({
|
||||
tile: {
|
||||
@@ -86,15 +90,19 @@ const styles = {
|
||||
};
|
||||
|
||||
type TutorialsRowProps = {|
|
||||
limits: ?Limits,
|
||||
tutorials: Tutorial[],
|
||||
category: TutorialCategory,
|
||||
onSelectCategory: TutorialCategory => void,
|
||||
onSelectTutorial: (tutorial: Tutorial) => void,
|
||||
|};
|
||||
|
||||
export const TutorialsRow = ({
|
||||
limits,
|
||||
tutorials,
|
||||
category,
|
||||
onSelectCategory,
|
||||
onSelectTutorial,
|
||||
}: TutorialsRowProps) => (
|
||||
<I18n>
|
||||
{({ i18n }) => (
|
||||
@@ -103,7 +111,14 @@ export const TutorialsRow = ({
|
||||
description={TUTORIAL_CATEGORY_TEXTS[category].description}
|
||||
items={tutorials
|
||||
.filter(tutorial => tutorial.category === category)
|
||||
.map(tutorial => formatTutorialToImageTileComponent(i18n, tutorial))}
|
||||
.map(tutorial =>
|
||||
formatTutorialToImageTileComponent({
|
||||
i18n,
|
||||
limits,
|
||||
tutorial,
|
||||
onSelectTutorial,
|
||||
})
|
||||
)}
|
||||
onShowAll={() => onSelectCategory(category)}
|
||||
showAllIcon={<ChevronArrowRight fontSize="small" />}
|
||||
getColumnsFromWindowSize={getTutorialsColumnsFromWidth}
|
||||
@@ -128,6 +143,7 @@ const MainPage = ({
|
||||
tutorials,
|
||||
selectInAppTutorial,
|
||||
}: Props) => {
|
||||
const { limits } = React.useContext(AuthenticatedUserContext);
|
||||
const classes = useStyles();
|
||||
const {
|
||||
windowSize,
|
||||
@@ -159,6 +175,11 @@ const MainPage = ({
|
||||
},
|
||||
].filter(Boolean);
|
||||
|
||||
const [
|
||||
selectedTutorial,
|
||||
setSelectedTutorial,
|
||||
] = React.useState<Tutorial | null>(null);
|
||||
|
||||
return (
|
||||
<SectionContainer title={<Trans>Help and guides</Trans>}>
|
||||
<SectionRow>
|
||||
@@ -219,25 +240,41 @@ const MainPage = ({
|
||||
<Trans>Learn everything about GDevelop from the ground up</Trans>
|
||||
</Text>
|
||||
</Line>
|
||||
{limits &&
|
||||
limits.capabilities.classrooms &&
|
||||
limits.capabilities.classrooms.hideUpgradeNotice ? null : (
|
||||
<>
|
||||
<Spacer />
|
||||
<EducationCard
|
||||
onSeeResources={() => onSelectCategory('education-curriculum')}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</SectionRow>
|
||||
<SectionRow>
|
||||
<TutorialsRow
|
||||
limits={limits}
|
||||
category="official-beginner"
|
||||
onSelectCategory={onSelectCategory}
|
||||
onSelectTutorial={setSelectedTutorial}
|
||||
tutorials={tutorials}
|
||||
/>
|
||||
</SectionRow>
|
||||
<SectionRow>
|
||||
<TutorialsRow
|
||||
limits={limits}
|
||||
category="official-intermediate"
|
||||
onSelectCategory={onSelectCategory}
|
||||
onSelectTutorial={setSelectedTutorial}
|
||||
tutorials={tutorials}
|
||||
/>
|
||||
</SectionRow>
|
||||
<SectionRow>
|
||||
<TutorialsRow
|
||||
limits={limits}
|
||||
category="official-advanced"
|
||||
onSelectCategory={onSelectCategory}
|
||||
onSelectTutorial={setSelectedTutorial}
|
||||
tutorials={tutorials}
|
||||
/>
|
||||
</SectionRow>
|
||||
@@ -313,18 +350,37 @@ const MainPage = ({
|
||||
</SectionRow>
|
||||
<SectionRow>
|
||||
<TutorialsRow
|
||||
category="full-game"
|
||||
limits={limits}
|
||||
category="education-curriculum"
|
||||
onSelectCategory={onSelectCategory}
|
||||
onSelectTutorial={setSelectedTutorial}
|
||||
tutorials={tutorials}
|
||||
/>
|
||||
</SectionRow>
|
||||
<SectionRow>
|
||||
<TutorialsRow
|
||||
category="game-mechanic"
|
||||
limits={limits}
|
||||
category="full-game"
|
||||
onSelectCategory={onSelectCategory}
|
||||
onSelectTutorial={setSelectedTutorial}
|
||||
tutorials={tutorials}
|
||||
/>
|
||||
</SectionRow>
|
||||
<SectionRow>
|
||||
<TutorialsRow
|
||||
limits={limits}
|
||||
category="game-mechanic"
|
||||
onSelectCategory={onSelectCategory}
|
||||
onSelectTutorial={setSelectedTutorial}
|
||||
tutorials={tutorials}
|
||||
/>
|
||||
</SectionRow>
|
||||
{selectedTutorial && (
|
||||
<PrivateTutorialViewDialog
|
||||
tutorial={selectedTutorial}
|
||||
onClose={() => setSelectedTutorial(null)}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
</SectionContainer>
|
||||
);
|
||||
|
@@ -9,6 +9,8 @@ import {
|
||||
import { formatTutorialToImageTileComponent, TUTORIAL_CATEGORY_TEXTS } from '.';
|
||||
import ImageTileGrid from '../../../../UI/ImageTileGrid';
|
||||
import { type WindowSizeType } from '../../../../UI/Responsive/ResponsiveWindowMeasurer';
|
||||
import AuthenticatedUserContext from '../../../../Profile/AuthenticatedUserContext';
|
||||
import { PrivateTutorialViewDialog } from '../../../../AssetStore/PrivateTutorials/PrivateTutorialViewDialog';
|
||||
|
||||
const getColumnsFromWindowSize = (windowSize: WindowSizeType) => {
|
||||
switch (windowSize) {
|
||||
@@ -32,10 +34,17 @@ type Props = {|
|
||||
|};
|
||||
|
||||
const TutorialsCategoryPage = ({ category, tutorials, onBack }: Props) => {
|
||||
const { limits } = React.useContext(AuthenticatedUserContext);
|
||||
const texts = TUTORIAL_CATEGORY_TEXTS[category];
|
||||
const filteredTutorials = tutorials.filter(
|
||||
tutorial => tutorial.category === category
|
||||
);
|
||||
|
||||
const [
|
||||
selectedTutorial,
|
||||
setSelectedTutorial,
|
||||
] = React.useState<Tutorial | null>(null);
|
||||
|
||||
return (
|
||||
<I18n>
|
||||
{({ i18n }) => (
|
||||
@@ -47,11 +56,22 @@ const TutorialsCategoryPage = ({ category, tutorials, onBack }: Props) => {
|
||||
<SectionRow>
|
||||
<ImageTileGrid
|
||||
items={filteredTutorials.map(tutorial =>
|
||||
formatTutorialToImageTileComponent(i18n, tutorial)
|
||||
formatTutorialToImageTileComponent({
|
||||
i18n,
|
||||
limits,
|
||||
tutorial,
|
||||
onSelectTutorial: setSelectedTutorial,
|
||||
})
|
||||
)}
|
||||
getColumnsFromWindowSize={getColumnsFromWindowSize}
|
||||
/>
|
||||
</SectionRow>
|
||||
{selectedTutorial && (
|
||||
<PrivateTutorialViewDialog
|
||||
tutorial={selectedTutorial}
|
||||
onClose={() => setSelectedTutorial(null)}
|
||||
/>
|
||||
)}
|
||||
</SectionContainer>
|
||||
)}
|
||||
</I18n>
|
||||
|
@@ -5,6 +5,7 @@ import { type HomeTab } from '../HomePageMenu';
|
||||
import {
|
||||
type TutorialCategory,
|
||||
type Tutorial,
|
||||
canAccessTutorial,
|
||||
} from '../../../../Utils/GDevelopServices/Tutorial';
|
||||
import MainPage from './MainPage';
|
||||
import TutorialsCategoryPage from './TutorialsCategoryPage';
|
||||
@@ -19,6 +20,7 @@ import { type ImageTileComponent } from '../../../../UI/ImageTileGrid';
|
||||
import Paper from '../../../../UI/Paper';
|
||||
import { selectMessageByLocale } from '../../../../Utils/i18n/MessageByLocale';
|
||||
import ErrorBoundary from '../../../../UI/ErrorBoundary';
|
||||
import { type Limits } from '../../../../Utils/GDevelopServices/Usage';
|
||||
|
||||
export const TUTORIAL_CATEGORY_TEXTS = {
|
||||
'full-game': {
|
||||
@@ -47,30 +49,65 @@ export const TUTORIAL_CATEGORY_TEXTS = {
|
||||
title: <Trans>Advanced course</Trans>,
|
||||
description: <Trans>The icing on the cake</Trans>,
|
||||
},
|
||||
'education-curriculum': {
|
||||
title: <Trans>Education curriculum and resources</Trans>,
|
||||
description: (
|
||||
<Trans>
|
||||
For teachers and educators having the GDevelop Education subscription.
|
||||
Ready to use resources for teaching.
|
||||
</Trans>
|
||||
),
|
||||
},
|
||||
recommendations: {
|
||||
title: <Trans>Recommendations</Trans>,
|
||||
description: null,
|
||||
},
|
||||
};
|
||||
|
||||
export const formatTutorialToImageTileComponent = (
|
||||
type FormatTutorialToImageTileComponentProps = {|
|
||||
i18n: I18nType,
|
||||
tutorial: Tutorial
|
||||
): ImageTileComponent => ({
|
||||
title: selectMessageByLocale(i18n, tutorial.titleByLocale) || tutorial.title,
|
||||
description:
|
||||
selectMessageByLocale(i18n, tutorial.descriptionByLocale) ||
|
||||
tutorial.description,
|
||||
onClick: () => {
|
||||
sendTutorialOpened(tutorial.id);
|
||||
Window.openExternalURL(
|
||||
selectMessageByLocale(i18n, tutorial.linkByLocale) || tutorial.link
|
||||
);
|
||||
},
|
||||
imageUrl:
|
||||
selectMessageByLocale(i18n, tutorial.thumbnailUrlByLocale) ||
|
||||
tutorial.thumbnailUrl,
|
||||
overlayText: tutorial.duration
|
||||
? secondsToMinutesAndSeconds(tutorial.duration)
|
||||
: '\u{1F4D8}',
|
||||
overlayTextPosition: 'bottomRight',
|
||||
});
|
||||
limits: ?Limits,
|
||||
tutorial: Tutorial,
|
||||
onSelectTutorial: (tutorial: Tutorial) => void,
|
||||
|};
|
||||
|
||||
export const formatTutorialToImageTileComponent = ({
|
||||
i18n,
|
||||
tutorial,
|
||||
limits,
|
||||
onSelectTutorial,
|
||||
}: FormatTutorialToImageTileComponentProps): ImageTileComponent => {
|
||||
const isLocked = !canAccessTutorial(
|
||||
tutorial,
|
||||
limits ? limits.capabilities : null
|
||||
);
|
||||
return {
|
||||
title:
|
||||
selectMessageByLocale(i18n, tutorial.titleByLocale) || tutorial.title,
|
||||
description:
|
||||
selectMessageByLocale(i18n, tutorial.descriptionByLocale) ||
|
||||
tutorial.description,
|
||||
isLocked,
|
||||
onClick: () => {
|
||||
if (tutorial.isPrivateTutorial) {
|
||||
onSelectTutorial(tutorial);
|
||||
return;
|
||||
}
|
||||
|
||||
sendTutorialOpened(tutorial.id);
|
||||
Window.openExternalURL(
|
||||
selectMessageByLocale(i18n, tutorial.linkByLocale) || tutorial.link
|
||||
);
|
||||
},
|
||||
imageUrl:
|
||||
selectMessageByLocale(i18n, tutorial.thumbnailUrlByLocale) ||
|
||||
tutorial.thumbnailUrl,
|
||||
overlayText: tutorial.duration
|
||||
? secondsToMinutesAndSeconds(tutorial.duration)
|
||||
: '\u{1F4D8}',
|
||||
overlayTextPosition: 'bottomRight',
|
||||
};
|
||||
};
|
||||
|
||||
const styles = {
|
||||
paper: {
|
||||
@@ -83,12 +120,14 @@ type Props = {|
|
||||
onOpenExampleStore: () => void,
|
||||
onTabChange: (tab: HomeTab) => void,
|
||||
selectInAppTutorial: (tutorialId: string) => void,
|
||||
initialCategory: TutorialCategory | null,
|
||||
|};
|
||||
|
||||
const LearnSection = ({
|
||||
onOpenExampleStore,
|
||||
onTabChange,
|
||||
selectInAppTutorial,
|
||||
initialCategory,
|
||||
}: Props) => {
|
||||
const {
|
||||
tutorials,
|
||||
@@ -106,7 +145,16 @@ const LearnSection = ({
|
||||
const [
|
||||
selectedCategory,
|
||||
setSelectedCategory,
|
||||
] = React.useState<?TutorialCategory>(null);
|
||||
] = React.useState<?TutorialCategory>(initialCategory || null);
|
||||
|
||||
React.useEffect(
|
||||
() => {
|
||||
if (initialCategory) {
|
||||
setSelectedCategory(initialCategory);
|
||||
}
|
||||
},
|
||||
[initialCategory]
|
||||
);
|
||||
|
||||
if (tutorialLoadingError)
|
||||
return (
|
||||
|
@@ -20,6 +20,7 @@ type Props = {|
|
||||
onOpenPrivateGameTemplateListingData: (
|
||||
privateGameTemplateListingData: PrivateGameTemplateListingData
|
||||
) => void,
|
||||
onOpenProfile: () => void,
|
||||
|};
|
||||
|
||||
const StoreSection = ({
|
||||
@@ -27,6 +28,7 @@ const StoreSection = ({
|
||||
resourceManagementProps,
|
||||
canInstallPrivateAsset,
|
||||
onOpenPrivateGameTemplateListingData,
|
||||
onOpenProfile,
|
||||
}: Props) => {
|
||||
const [
|
||||
isAssetPackDialogInstallOpen,
|
||||
@@ -84,6 +86,7 @@ const StoreSection = ({
|
||||
onOpenPrivateGameTemplateListingData
|
||||
}
|
||||
displayPromotions
|
||||
onOpenProfile={onOpenProfile}
|
||||
/>
|
||||
{(openedAssetPack || openedAssetShortHeader) && (
|
||||
<Line justifyContent="flex-end">
|
||||
|
@@ -16,8 +16,6 @@ const getDefaultUser = ({ id }: { id: string }) => ({
|
||||
isCreator: true,
|
||||
isPlayer: false,
|
||||
isEmailAutogenerated: false,
|
||||
isStudent: false,
|
||||
isTeacher: false,
|
||||
});
|
||||
|
||||
const getDefaultMembership = ({
|
||||
|
@@ -4,7 +4,7 @@ import { Trans, t } from '@lingui/macro';
|
||||
import { type I18n as I18nType } from '@lingui/core';
|
||||
|
||||
import List from '@material-ui/core/List';
|
||||
import { Line, Column } from '../../../../UI/Grid';
|
||||
import { Line, Column, Spacer } from '../../../../UI/Grid';
|
||||
|
||||
import {
|
||||
type FileMetadataAndStorageProviderName,
|
||||
@@ -42,6 +42,7 @@ import ContextMenu, {
|
||||
} from '../../../../UI/Menu/ContextMenu';
|
||||
import type { ClientCoordinates } from '../../../../Utils/UseLongTouch';
|
||||
import { type MenuItemTemplate } from '../../../../UI/Menu/Menu.flow';
|
||||
import { EducationCard } from '../LearnSection/EducationCard';
|
||||
|
||||
const PADDING = 16;
|
||||
|
||||
@@ -63,6 +64,7 @@ type Props = {|
|
||||
currentFileMetadata: ?FileMetadata,
|
||||
onOpenRecentFile: (file: FileMetadataAndStorageProviderName) => Promise<void>,
|
||||
storageProviders: Array<StorageProvider>,
|
||||
onOpenTeachingResources: () => void,
|
||||
|};
|
||||
|
||||
export type TeamSectionInterface = {|
|
||||
@@ -71,7 +73,13 @@ export type TeamSectionInterface = {|
|
||||
|
||||
const TeamSection = React.forwardRef<Props, TeamSectionInterface>(
|
||||
(
|
||||
{ project, onOpenRecentFile, storageProviders, currentFileMetadata },
|
||||
{
|
||||
project,
|
||||
onOpenRecentFile,
|
||||
storageProviders,
|
||||
currentFileMetadata,
|
||||
onOpenTeachingResources,
|
||||
},
|
||||
ref
|
||||
) => {
|
||||
const {
|
||||
@@ -272,6 +280,8 @@ const TeamSection = React.forwardRef<Props, TeamSectionInterface>(
|
||||
}
|
||||
>
|
||||
<SectionRow>
|
||||
<EducationCard onSeeResources={onOpenTeachingResources} />
|
||||
<Spacer />
|
||||
{membersNotInAGroupToDisplay && (
|
||||
<Paper background="medium" style={styles.lobbyContainer}>
|
||||
<Line noMargin>
|
||||
|
@@ -15,6 +15,7 @@ import PlaySection from './PlaySection';
|
||||
import ManageSection from './ManageSection';
|
||||
import CommunitySection from './CommunitySection';
|
||||
import StoreSection from './StoreSection';
|
||||
import { type TutorialCategory } from '../../../Utils/GDevelopServices/Tutorial';
|
||||
import { TutorialContext } from '../../../Tutorial/TutorialContext';
|
||||
import { ExampleStoreContext } from '../../../AssetStore/ExampleStore/ExampleStoreContext';
|
||||
import { HomePageHeader } from './HomePageHeader';
|
||||
@@ -199,6 +200,10 @@ export const HomePage = React.memo<Props>(
|
||||
: 'build';
|
||||
|
||||
const [activeTab, setActiveTab] = React.useState<HomeTab>(initialTab);
|
||||
const [
|
||||
learnInitialCategory,
|
||||
setLearnInitialCategory,
|
||||
] = React.useState<TutorialCategory | null>(null);
|
||||
|
||||
const { setInitialPackUserFriendlySlug } = React.useContext(
|
||||
AssetStoreContext
|
||||
@@ -449,6 +454,7 @@ export const HomePage = React.memo<Props>(
|
||||
subscriptionPlansWithPricingSystems={
|
||||
subscriptionPlansWithPricingSystems
|
||||
}
|
||||
onOpenProfile={onOpenProfile}
|
||||
/>
|
||||
)}
|
||||
{activeTab === 'build' && (
|
||||
@@ -475,6 +481,7 @@ export const HomePage = React.memo<Props>(
|
||||
onOpenExampleStore={onOpenExampleStore}
|
||||
onTabChange={setActiveTab}
|
||||
selectInAppTutorial={selectInAppTutorial}
|
||||
initialCategory={learnInitialCategory}
|
||||
/>
|
||||
)}
|
||||
{activeTab === 'play' && <PlaySection />}
|
||||
@@ -487,6 +494,7 @@ export const HomePage = React.memo<Props>(
|
||||
onOpenPrivateGameTemplateListingData={
|
||||
onOpenPrivateGameTemplateListingData
|
||||
}
|
||||
onOpenProfile={onOpenProfile}
|
||||
/>
|
||||
)}
|
||||
{activeTab === 'team-view' && (
|
||||
@@ -495,6 +503,10 @@ export const HomePage = React.memo<Props>(
|
||||
onOpenRecentFile={onOpenRecentFile}
|
||||
storageProviders={storageProviders}
|
||||
currentFileMetadata={fileMetadata}
|
||||
onOpenTeachingResources={() => {
|
||||
setLearnInitialCategory('education-curriculum');
|
||||
setActiveTab('learn');
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
@@ -6,7 +6,6 @@ import type { PrivateGameTemplateListingData } from '../Utils/GDevelopServices/S
|
||||
import ExampleStoreDialog from '../AssetStore/ExampleStore/ExampleStoreDialog';
|
||||
import { ExampleDialog } from '../AssetStore/ExampleStore/ExampleDialog';
|
||||
import PrivateGameTemplateInformationDialog from '../AssetStore/PrivateGameTemplates/PrivateGameTemplateInformationDialog';
|
||||
import PrivateGameTemplatePurchaseDialog from '../AssetStore/PrivateGameTemplates/PrivateGameTemplatePurchaseDialog';
|
||||
import { PrivateGameTemplateStoreContext } from '../AssetStore/PrivateGameTemplates/PrivateGameTemplateStoreContext';
|
||||
import AuthenticatedUserContext from '../Profile/AuthenticatedUserContext';
|
||||
|
||||
@@ -42,10 +41,6 @@ const useExampleOrGameTemplateDialogs = ({
|
||||
*/
|
||||
openDialog: boolean,
|
||||
|}>(null);
|
||||
const [
|
||||
purchasingGameTemplateListingData,
|
||||
setPurchasingGameTemplateListingData,
|
||||
] = React.useState<?PrivateGameTemplateListingData>(null);
|
||||
|
||||
const { receivedGameTemplates } = React.useContext(AuthenticatedUserContext);
|
||||
const { privateGameTemplateListingDatas } = React.useContext(
|
||||
@@ -147,7 +142,6 @@ const useExampleOrGameTemplateDialogs = ({
|
||||
privateGameTemplateListingData={
|
||||
selectedPrivateGameTemplate.privateGameTemplateListingData
|
||||
}
|
||||
isPurchaseDialogOpen={!!purchasingGameTemplateListingData}
|
||||
onCreateWithGameTemplate={onOpenNewProjectSetupDialog}
|
||||
onGameTemplateOpen={privateGameTemplateListingData =>
|
||||
setSelectedPrivateGameTemplate({
|
||||
@@ -155,23 +149,12 @@ const useExampleOrGameTemplateDialogs = ({
|
||||
openDialog: true,
|
||||
})
|
||||
}
|
||||
onOpenPurchaseDialog={() => {
|
||||
setPurchasingGameTemplateListingData(
|
||||
selectedPrivateGameTemplate.privateGameTemplateListingData
|
||||
);
|
||||
}}
|
||||
onClose={() => setSelectedPrivateGameTemplate(null)}
|
||||
privateGameTemplateListingDatasFromSameCreator={
|
||||
privateGameTemplateListingDatasFromSameCreator
|
||||
}
|
||||
/>
|
||||
)}
|
||||
{!!purchasingGameTemplateListingData && (
|
||||
<PrivateGameTemplatePurchaseDialog
|
||||
privateGameTemplateListingData={purchasingGameTemplateListingData}
|
||||
onClose={() => setPurchasingGameTemplateListingData(null)}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
@@ -175,7 +175,7 @@ import useCreateProject from '../Utils/UseCreateProject';
|
||||
import newNameGenerator from '../Utils/NewNameGenerator';
|
||||
import { addDefaultLightToAllLayers } from '../ProjectCreation/CreateProject';
|
||||
import useEditorTabsStateSaving from './EditorTabs/UseEditorTabsStateSaving';
|
||||
import PixiResourcesLoader from '../ObjectsRendering/PixiResourcesLoader';
|
||||
import { pixiResourcesLoader } from '../ObjectsRendering/PixiResourcesLoader';
|
||||
import useResourcesWatcher from './ResourcesWatcher';
|
||||
import { extractGDevelopApiErrorStatusAndCode } from '../Utils/GDevelopServices/Errors';
|
||||
import useVersionHistory from '../VersionHistory/UseVersionHistory';
|
||||
@@ -901,7 +901,7 @@ const MainFrame = (props: Props) => {
|
||||
// the URL to a resource with a name in the old project is not re-used
|
||||
// for another resource with the same name in the new project.
|
||||
ResourcesLoader.burstAllUrlsCache();
|
||||
PixiResourcesLoader.burstCache();
|
||||
pixiResourcesLoader.burstCache();
|
||||
|
||||
const state = await setState(state => ({
|
||||
...state,
|
||||
@@ -1059,10 +1059,10 @@ const MainFrame = (props: Props) => {
|
||||
// Try to find an autosave (and ask user if found)
|
||||
try {
|
||||
await delay(150);
|
||||
const autoSaveFileMetadata = await checkForAutosave();
|
||||
let content;
|
||||
let openingError: Error | null = null;
|
||||
try {
|
||||
const autoSaveFileMetadata = await checkForAutosave();
|
||||
const result = await onOpen(
|
||||
autoSaveFileMetadata,
|
||||
setLoaderModalProgress
|
||||
|
@@ -20,9 +20,9 @@ import {
|
||||
import Text from '../UI/Text';
|
||||
import Link from '../UI/Link';
|
||||
import Window from '../Utils/Window';
|
||||
import Basic from './Icons/Basic';
|
||||
import Pro from './Icons/Pro';
|
||||
import Premium from './Icons/Premium';
|
||||
import Speaker from './Icons/Speaker';
|
||||
import Speedometer from './Icons/Speedometer';
|
||||
import Stars from './Icons/Stars';
|
||||
import GDevelopThemeContext from '../UI/Theme/GDevelopThemeContext';
|
||||
import { Column, Line } from '../UI/Grid';
|
||||
import RaisedButton from '../UI/RaisedButton';
|
||||
@@ -56,13 +56,13 @@ const styles = {
|
||||
};
|
||||
|
||||
const getIconForMarketingPlan = (marketingPlan: MarketingPlan) => {
|
||||
switch (marketingPlan.id) {
|
||||
case 'featuring-basic':
|
||||
return <Basic style={styles.iconStyle} />;
|
||||
case 'featuring-pro':
|
||||
return <Pro style={styles.iconStyle} />;
|
||||
case 'featuring-premium':
|
||||
return <Premium style={styles.iconStyle} />;
|
||||
switch (marketingPlan.icon) {
|
||||
case 'speaker':
|
||||
return <Speaker style={styles.iconStyle} />;
|
||||
case 'speedometer':
|
||||
return <Speedometer style={styles.iconStyle} />;
|
||||
case 'stars':
|
||||
return <Stars style={styles.iconStyle} />;
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
@@ -93,55 +93,43 @@ const MarketingPlans = ({ game }: Props) => {
|
||||
null
|
||||
);
|
||||
|
||||
const {
|
||||
activeBasicFeaturing,
|
||||
activeProFeaturing,
|
||||
activePremiumFeaturing,
|
||||
}: {|
|
||||
activeBasicFeaturing: ?GameFeaturing,
|
||||
activeProFeaturing: ?GameFeaturing,
|
||||
activePremiumFeaturing: ?GameFeaturing,
|
||||
|} = React.useMemo(
|
||||
const activeGameFeaturings: ?(GameFeaturing[]) = React.useMemo(
|
||||
() => {
|
||||
if (!gameFeaturings)
|
||||
return {
|
||||
activeBasicFeaturing: null,
|
||||
activeProFeaturing: null,
|
||||
activePremiumFeaturing: null,
|
||||
};
|
||||
if (!gameFeaturings) return null;
|
||||
|
||||
const activeGameFeaturings = gameFeaturings.filter(
|
||||
return gameFeaturings.filter(
|
||||
gameFeaturing => gameFeaturing.expiresAt > Date.now() / 1000
|
||||
);
|
||||
return {
|
||||
activeBasicFeaturing: activeGameFeaturings.filter(
|
||||
gameFeaturing => gameFeaturing.featuring === 'games-platform-home'
|
||||
)[0],
|
||||
activeProFeaturing: activeGameFeaturings.filter(
|
||||
gameFeaturing => gameFeaturing.featuring === 'socials-newsletter'
|
||||
)[0],
|
||||
activePremiumFeaturing: activeGameFeaturings.filter(
|
||||
gameFeaturing => gameFeaturing.featuring === 'gdevelop-banner'
|
||||
)[0],
|
||||
};
|
||||
},
|
||||
[gameFeaturings]
|
||||
);
|
||||
|
||||
const getActiveFeaturing = React.useCallback(
|
||||
const isMarketingPlanActive = React.useCallback(
|
||||
(marketingPlan: MarketingPlan) => {
|
||||
switch (marketingPlan.id) {
|
||||
case 'featuring-basic':
|
||||
return activeBasicFeaturing;
|
||||
case 'featuring-pro':
|
||||
return activeProFeaturing;
|
||||
case 'featuring-premium':
|
||||
return activePremiumFeaturing;
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
if (!activeGameFeaturings) return false;
|
||||
const includedMarketingPlanFeaturings = marketingPlan.includedFeaturings;
|
||||
|
||||
// A marketing plan is considered active if it has all the included featurings active.
|
||||
return includedMarketingPlanFeaturings.every(
|
||||
includedMarketingPlanFeaturing =>
|
||||
activeGameFeaturings.some(
|
||||
activeGameFeaturing =>
|
||||
activeGameFeaturing.featuring === includedMarketingPlanFeaturing
|
||||
)
|
||||
);
|
||||
},
|
||||
[activeBasicFeaturing, activePremiumFeaturing, activeProFeaturing]
|
||||
[activeGameFeaturings]
|
||||
);
|
||||
|
||||
const getActiveFeaturingsOfMarketingPlan = React.useCallback(
|
||||
(marketingPlan: MarketingPlan) => {
|
||||
if (!activeGameFeaturings) return [];
|
||||
|
||||
return activeGameFeaturings.filter(activeGameFeaturing =>
|
||||
marketingPlan.includedFeaturings.includes(activeGameFeaturing.featuring)
|
||||
);
|
||||
},
|
||||
[activeGameFeaturings]
|
||||
);
|
||||
|
||||
const getMarketingPlanPrice = React.useCallback(
|
||||
@@ -199,24 +187,28 @@ const MarketingPlans = ({ game }: Props) => {
|
||||
async (i18n: I18nType, marketingPlan: MarketingPlan) => {
|
||||
if (!profile || !limits) return;
|
||||
|
||||
const { id, nameByLocale } = marketingPlan;
|
||||
const {
|
||||
id,
|
||||
nameByLocale,
|
||||
canExtend,
|
||||
requiresManualContact,
|
||||
additionalSuccessMessageByLocale,
|
||||
} = marketingPlan;
|
||||
const planCreditsAmount = getMarketingPlanPrice(marketingPlan);
|
||||
if (!planCreditsAmount) return;
|
||||
|
||||
const translatedName = selectMessageByLocale(i18n, nameByLocale);
|
||||
|
||||
const activeFeaturing = getActiveFeaturing(marketingPlan);
|
||||
if (
|
||||
activeFeaturing &&
|
||||
(marketingPlan.id === 'featuring-pro' ||
|
||||
marketingPlan.id === 'featuring-premium')
|
||||
) {
|
||||
await showAlert({
|
||||
title: t`Featuring already active`,
|
||||
message: t`You already have an active ${translatedName} featuring for your game ${
|
||||
game.gameName
|
||||
}. Check your emails or discord, we will get in touch with you to get the campaign up!`,
|
||||
});
|
||||
const isPlanActive = isMarketingPlanActive(marketingPlan);
|
||||
if (isPlanActive && !canExtend) {
|
||||
if (requiresManualContact) {
|
||||
await showAlert({
|
||||
title: t`Featuring already active`,
|
||||
message: t`You already have an active ${translatedName} featuring for your game ${
|
||||
game.gameName
|
||||
}. Check your emails or discord, we will get in touch with you to get the campaign up!`,
|
||||
});
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -229,12 +221,13 @@ const MarketingPlans = ({ game }: Props) => {
|
||||
}
|
||||
|
||||
openCreditsUsageDialog({
|
||||
title: activeFeaturing ? (
|
||||
<Trans>Extend Featuring</Trans>
|
||||
) : (
|
||||
<Trans>Get Featuring</Trans>
|
||||
),
|
||||
message: activeFeaturing ? (
|
||||
title:
|
||||
isPlanActive && canExtend ? (
|
||||
<Trans>Extend Featuring</Trans>
|
||||
) : (
|
||||
<Trans>Get Featuring</Trans>
|
||||
),
|
||||
message: canExtend ? (
|
||||
<Trans>
|
||||
You are about to use {planCreditsAmount} credits to extend the game
|
||||
featuring {translatedName} for your game {game.gameName} and push it
|
||||
@@ -255,21 +248,15 @@ const MarketingPlans = ({ game }: Props) => {
|
||||
});
|
||||
await fetchGameFeaturings();
|
||||
},
|
||||
successMessage:
|
||||
marketingPlan.id === 'featuring-basic' ? (
|
||||
successMessage: (
|
||||
<span>
|
||||
<Trans>
|
||||
🎉 Congrats on getting the {translatedName} featuring for your
|
||||
game {game.gameName}! Ensure that your game is public and you have
|
||||
configured a thumbnail for gd.games. This can take a few minutes
|
||||
for your game to be visible on the platform.
|
||||
</Trans>
|
||||
) : (
|
||||
<Trans>
|
||||
🎉 Congrats on getting the {translatedName} featuring for your
|
||||
game {game.gameName}. We will get in touch with you to get the
|
||||
campaign up!
|
||||
</Trans>
|
||||
),
|
||||
game {game.gameName}!
|
||||
</Trans>{' '}
|
||||
{selectMessageByLocale(i18n, additionalSuccessMessageByLocale)}
|
||||
</span>
|
||||
),
|
||||
});
|
||||
},
|
||||
[
|
||||
@@ -278,7 +265,7 @@ const MarketingPlans = ({ game }: Props) => {
|
||||
limits,
|
||||
profile,
|
||||
showAlert,
|
||||
getActiveFeaturing,
|
||||
isMarketingPlanActive,
|
||||
fetchGameFeaturings,
|
||||
openCreditsPackageDialog,
|
||||
openCreditsUsageDialog,
|
||||
@@ -288,18 +275,17 @@ const MarketingPlans = ({ game }: Props) => {
|
||||
|
||||
const getRequirementsErrors = (marketingPlan: MarketingPlan) => {
|
||||
const requirementsErrors = [];
|
||||
if (marketingPlan.id === 'featuring-basic') {
|
||||
if (!game.thumbnailUrl) {
|
||||
requirementsErrors.push(<Trans>You don't have a thumbnail</Trans>);
|
||||
}
|
||||
if (!game.publicWebBuildId) {
|
||||
requirementsErrors.push(
|
||||
<Trans>Your game does not have a public build</Trans>
|
||||
);
|
||||
}
|
||||
if (!game.discoverable) {
|
||||
requirementsErrors.push(<Trans>Your game is not discoverable</Trans>);
|
||||
}
|
||||
const marketingPlanGameRequirements = marketingPlan.gameRequirements;
|
||||
if (!!marketingPlanGameRequirements.hasThumbnail && !game.thumbnailUrl) {
|
||||
requirementsErrors.push(<Trans>You don't have a thumbnail</Trans>);
|
||||
}
|
||||
if (!marketingPlanGameRequirements.isPublished && !game.publicWebBuildId) {
|
||||
requirementsErrors.push(
|
||||
<Trans>Your game does not have a public build</Trans>
|
||||
);
|
||||
}
|
||||
if (!!marketingPlanGameRequirements.isDiscoverable && !game.discoverable) {
|
||||
requirementsErrors.push(<Trans>Your game is not discoverable</Trans>);
|
||||
}
|
||||
|
||||
return requirementsErrors;
|
||||
@@ -307,12 +293,10 @@ const MarketingPlans = ({ game }: Props) => {
|
||||
|
||||
const getActiveMessage = ({
|
||||
marketingPlan,
|
||||
activeFeaturing,
|
||||
i18n,
|
||||
hasErrors,
|
||||
}: {|
|
||||
marketingPlan: MarketingPlan,
|
||||
activeFeaturing: GameFeaturing,
|
||||
i18n: I18nType,
|
||||
hasErrors: boolean,
|
||||
|}) => {
|
||||
@@ -320,10 +304,24 @@ const MarketingPlans = ({ game }: Props) => {
|
||||
return <Trans>Fix those issues to get the campaign up!</Trans>;
|
||||
}
|
||||
|
||||
return activeFeaturing.featuring === 'games-platform-home' ? (
|
||||
const activeFeaturingsForPlan = getActiveFeaturingsOfMarketingPlan(
|
||||
marketingPlan
|
||||
);
|
||||
|
||||
if (activeFeaturingsForPlan.length === 0) {
|
||||
// Should not happen.
|
||||
return null;
|
||||
}
|
||||
|
||||
// Assume they will all have the same expiration date, so pick the first one.
|
||||
const activeFeaturing = activeFeaturingsForPlan[0];
|
||||
|
||||
return !marketingPlan.requiresManualContact ? (
|
||||
<Trans>Active until {i18n.date(activeFeaturing.expiresAt * 1000)}</Trans>
|
||||
) : (
|
||||
) : marketingPlan.requiresManualContact ? (
|
||||
<Trans>Active, we will get in touch to get the campaign up!</Trans>
|
||||
) : (
|
||||
<Trans>Active</Trans>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -370,6 +368,7 @@ const MarketingPlans = ({ game }: Props) => {
|
||||
const {
|
||||
id,
|
||||
nameByLocale,
|
||||
canExtend,
|
||||
descriptionByLocale,
|
||||
bulletPointsByLocale,
|
||||
} = marketingPlan;
|
||||
@@ -380,8 +379,8 @@ const MarketingPlans = ({ game }: Props) => {
|
||||
);
|
||||
return null;
|
||||
}
|
||||
const activeFeaturing = getActiveFeaturing(marketingPlan);
|
||||
const requirementsErrors = activeFeaturing
|
||||
const isPlanActive = isMarketingPlanActive(marketingPlan);
|
||||
const requirementsErrors = isPlanActive
|
||||
? getRequirementsErrors(marketingPlan)
|
||||
: [];
|
||||
const hasErrors = requirementsErrors.length > 0;
|
||||
@@ -389,7 +388,7 @@ const MarketingPlans = ({ game }: Props) => {
|
||||
<div
|
||||
style={{
|
||||
...styles.campaign,
|
||||
border: activeFeaturing
|
||||
border: isPlanActive
|
||||
? `2px solid ${gdevelopTheme.message.valid}`
|
||||
: `1px solid ${gdevelopTheme.palette.secondary}`,
|
||||
}}
|
||||
@@ -441,7 +440,7 @@ const MarketingPlans = ({ game }: Props) => {
|
||||
<CheckCircle
|
||||
style={{
|
||||
...styles.bulletIcon,
|
||||
...(activeFeaturing
|
||||
...(isPlanActive
|
||||
? {
|
||||
color:
|
||||
gdevelopTheme.message.valid,
|
||||
@@ -473,9 +472,8 @@ const MarketingPlans = ({ game }: Props) => {
|
||||
color="secondary"
|
||||
align="left"
|
||||
>
|
||||
{activeFeaturing
|
||||
{isPlanActive
|
||||
? getActiveMessage({
|
||||
activeFeaturing,
|
||||
marketingPlan,
|
||||
i18n,
|
||||
hasErrors,
|
||||
@@ -484,14 +482,13 @@ const MarketingPlans = ({ game }: Props) => {
|
||||
</Text>
|
||||
</Column>
|
||||
<RaisedButton
|
||||
primary={!activeFeaturing}
|
||||
primary={!isPlanActive || canExtend}
|
||||
onClick={() => onPurchase(i18n, marketingPlan)}
|
||||
label={
|
||||
!gameFeaturings ? (
|
||||
<Trans>Loading...</Trans>
|
||||
) : activeFeaturing ? (
|
||||
activeFeaturing.featuring ===
|
||||
'games-platform-home' ? (
|
||||
) : isPlanActive ? (
|
||||
canExtend ? (
|
||||
<Trans>Extend</Trans>
|
||||
) : (
|
||||
<Trans>Activated</Trans>
|
||||
|
@@ -25,7 +25,7 @@ import { makeDragSourceAndDropTarget } from '../../UI/DragAndDrop/DragSourceAndD
|
||||
import { DragHandleIcon } from '../../UI/DragHandle';
|
||||
import DropIndicator from '../../UI/SortableVirtualizedItemList/DropIndicator';
|
||||
import GDevelopThemeContext from '../../UI/Theme/GDevelopThemeContext';
|
||||
import PixiResourcesLoader from '../../ObjectsRendering/PixiResourcesLoader';
|
||||
import { pixiResourcesLoader } from '../../ObjectsRendering/PixiResourcesLoader';
|
||||
import useAlertDialog from '../../UI/Alert/useAlertDialog';
|
||||
import { type GLTF } from 'three/examples/jsm/loaders/GLTFLoader';
|
||||
import * as SkeletonUtils from 'three/examples/jsm/utils/SkeletonUtils';
|
||||
@@ -155,7 +155,7 @@ const Model3DEditor = ({
|
||||
const [gltf, setGltf] = React.useState<GLTF | null>(null);
|
||||
const loadGltf = React.useCallback(
|
||||
async (modelResourceName: string) => {
|
||||
const newModel3d = await PixiResourcesLoader.get3DModel(
|
||||
const newModel3d = await pixiResourcesLoader.get3DModel(
|
||||
project,
|
||||
modelResourceName
|
||||
);
|
||||
|
@@ -24,6 +24,7 @@ const ObjectPropertiesEditor = (props: Props) => {
|
||||
project,
|
||||
resourceManagementProps,
|
||||
unsavedChanges,
|
||||
renderObjectNameField,
|
||||
} = props;
|
||||
|
||||
// TODO: Workaround a bad design of ObjectJsImplementation. When getProperties
|
||||
@@ -53,6 +54,7 @@ const ObjectPropertiesEditor = (props: Props) => {
|
||||
<I18n>
|
||||
{({ i18n }) => (
|
||||
<ColumnStackLayout noMargin>
|
||||
{renderObjectNameField && renderObjectNameField()}
|
||||
{tutorialIds.map(tutorialId => (
|
||||
<DismissableTutorialMessage
|
||||
key={tutorialId}
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user