mirror of
https://github.com/4ian/GDevelop.git
synced 2025-10-15 10:19:04 +00:00
Compare commits
1 Commits
cursor/add
...
cursor/ext
Author | SHA1 | Date | |
---|---|---|---|
![]() |
c0ab3ccc50 |
1
.gitignore
vendored
1
.gitignore
vendored
@@ -33,4 +33,3 @@
|
||||
.Spotlight-V100
|
||||
.Trashes
|
||||
Thumbs.db
|
||||
.claude
|
||||
|
3
.vscode/tasks.json
vendored
3
.vscode/tasks.json
vendored
@@ -38,7 +38,8 @@
|
||||
"presentation": {
|
||||
"reveal": "silent"
|
||||
},
|
||||
"isBackground": true
|
||||
"isBackground": true,
|
||||
"runOptions": { "instanceLimit": 1, "runOn": "folderOpen" }
|
||||
},
|
||||
{
|
||||
"type": "npm",
|
||||
|
@@ -18,7 +18,7 @@ void GD_CORE_API BuiltinExtensionsImplementer::ImplementsAnimatableExtension(
|
||||
gd::PlatformExtension& extension) {
|
||||
extension
|
||||
.SetExtensionInformation("AnimatableCapability",
|
||||
_("Actions and conditions for objects having animations (sprite, 3D models...)."),
|
||||
_("Animatable capability"),
|
||||
_("Animate objects."),
|
||||
"Florian Rival",
|
||||
"Open source (MIT License)")
|
||||
@@ -32,7 +32,7 @@ void GD_CORE_API BuiltinExtensionsImplementer::ImplementsAnimatableExtension(
|
||||
"AnimatableBehavior",
|
||||
_("Animatable capability"),
|
||||
"Animation",
|
||||
_("Actions and conditions for objects having animations (sprite, 3D models...).."),
|
||||
_("Animate objects."),
|
||||
"",
|
||||
"res/actions/animation24.png",
|
||||
"AnimatableBehavior",
|
||||
|
@@ -19,7 +19,7 @@ void GD_CORE_API BuiltinExtensionsImplementer::ImplementsEffectExtension(
|
||||
extension
|
||||
.SetExtensionInformation("EffectCapability",
|
||||
_("Effect capability"),
|
||||
_("Actions/conditions to enable/disable and change parameters of visual effects applied on objects."),
|
||||
_("Apply visual effects to objects."),
|
||||
"Florian Rival",
|
||||
"Open source (MIT License)")
|
||||
.SetExtensionHelpPath("/objects");
|
||||
@@ -30,7 +30,7 @@ void GD_CORE_API BuiltinExtensionsImplementer::ImplementsEffectExtension(
|
||||
"EffectBehavior",
|
||||
_("Effect capability"),
|
||||
"Effect",
|
||||
_("Actions/conditions to enable/disable and change parameters of visual effects applied on objects."),
|
||||
_("Apply visual effects to objects."),
|
||||
"",
|
||||
"res/actions/effect_black.svg",
|
||||
"EffectBehavior",
|
||||
|
@@ -19,7 +19,7 @@ void GD_CORE_API BuiltinExtensionsImplementer::ImplementsFlippableExtension(
|
||||
extension
|
||||
.SetExtensionInformation("FlippableCapability",
|
||||
_("Flippable capability"),
|
||||
_("Actions/conditions for objects which can be flipped horizontally or vertically."),
|
||||
_("Flip objects."),
|
||||
"Florian Rival",
|
||||
"Open source (MIT License)")
|
||||
.SetExtensionHelpPath("/objects");
|
||||
@@ -30,7 +30,7 @@ void GD_CORE_API BuiltinExtensionsImplementer::ImplementsFlippableExtension(
|
||||
"FlippableBehavior",
|
||||
_("Flippable capability"),
|
||||
"Flippable",
|
||||
_("Actions/conditions for objects which can be flipped horizontally or vertically."),
|
||||
_("Flip objects."),
|
||||
"",
|
||||
"res/actions/flipX24.png",
|
||||
"FlippableBehavior",
|
||||
|
@@ -19,8 +19,7 @@ void GD_CORE_API BuiltinExtensionsImplementer::ImplementsOpacityExtension(
|
||||
extension
|
||||
.SetExtensionInformation("OpacityCapability",
|
||||
_("Opacity capability"),
|
||||
_("Action/condition/expression to change or "
|
||||
"check the opacity of an object (0-255)."),
|
||||
_("Change the object opacity."),
|
||||
"Florian Rival",
|
||||
"Open source (MIT License)")
|
||||
.SetExtensionHelpPath("/objects");
|
||||
@@ -29,19 +28,17 @@ void GD_CORE_API BuiltinExtensionsImplementer::ImplementsOpacityExtension(
|
||||
extension.AddInstructionOrExpressionGroupMetadata(_("Visibility"))
|
||||
.SetIcon("res/actions/opacity24.png");
|
||||
|
||||
gd::BehaviorMetadata& aut =
|
||||
extension
|
||||
.AddBehavior("OpacityBehavior",
|
||||
_("Opacity capability"),
|
||||
"Opacity",
|
||||
_("Action/condition/expression to change or check the "
|
||||
"opacity of an object (0-255)."),
|
||||
"",
|
||||
"res/actions/opacity24.png",
|
||||
"OpacityBehavior",
|
||||
std::make_shared<gd::Behavior>(),
|
||||
std::make_shared<gd::BehaviorsSharedData>())
|
||||
.SetHidden();
|
||||
gd::BehaviorMetadata& aut = extension.AddBehavior(
|
||||
"OpacityBehavior",
|
||||
_("Opacity capability"),
|
||||
"Opacity",
|
||||
_("Change the object opacity."),
|
||||
"",
|
||||
"res/actions/opacity24.png",
|
||||
"OpacityBehavior",
|
||||
std::make_shared<gd::Behavior>(),
|
||||
std::make_shared<gd::BehaviorsSharedData>())
|
||||
.SetHidden();
|
||||
|
||||
aut.AddExpressionAndConditionAndAction(
|
||||
"number",
|
||||
@@ -55,9 +52,8 @@ void GD_CORE_API BuiltinExtensionsImplementer::ImplementsOpacityExtension(
|
||||
.AddParameter("object", _("Object"))
|
||||
.AddParameter("behavior", _("Behavior"), "OpacityBehavior")
|
||||
.UseStandardParameters(
|
||||
"number",
|
||||
gd::ParameterOptions::MakeNewOptions().SetDescription(
|
||||
_("Opacity (0-255)")))
|
||||
"number", gd::ParameterOptions::MakeNewOptions().SetDescription(
|
||||
_("Opacity (0-255)")))
|
||||
.SetFunctionName("setOpacity")
|
||||
.SetGetter("getOpacity");
|
||||
aut.GetAllExpressions()["Value"].SetGroup("");
|
||||
|
@@ -16,13 +16,11 @@ namespace gd {
|
||||
void GD_CORE_API BuiltinExtensionsImplementer::ImplementsResizableExtension(
|
||||
gd::PlatformExtension &extension) {
|
||||
extension
|
||||
.SetExtensionInformation(
|
||||
"ResizableCapability",
|
||||
_("Resizable capability"),
|
||||
_("Change or compare the size (width/height) of an object which can "
|
||||
"be resized (i.e: most objects)."),
|
||||
"Florian Rival",
|
||||
"Open source (MIT License)")
|
||||
.SetExtensionInformation("ResizableCapability",
|
||||
_("Resizable capability"),
|
||||
_("Change the object dimensions."),
|
||||
"Florian Rival",
|
||||
"Open source (MIT License)")
|
||||
.SetExtensionHelpPath("/objects");
|
||||
extension.AddInstructionOrExpressionGroupMetadata(_("Size")).SetIcon(
|
||||
"res/actions/scale24_black.png");
|
||||
@@ -32,8 +30,7 @@ void GD_CORE_API BuiltinExtensionsImplementer::ImplementsResizableExtension(
|
||||
.AddBehavior("ResizableBehavior",
|
||||
_("Resizable capability"),
|
||||
"Resizable",
|
||||
_("Change or compare the size (width/height) of an "
|
||||
"object which can be resized (i.e: most objects)."),
|
||||
_("Change the object dimensions."),
|
||||
"",
|
||||
"res/actions/scale24_black.png",
|
||||
"ResizableBehavior",
|
||||
|
@@ -19,29 +19,26 @@ void GD_CORE_API BuiltinExtensionsImplementer::ImplementsScalableExtension(
|
||||
extension
|
||||
.SetExtensionInformation("ScalableCapability",
|
||||
_("Scalable capability"),
|
||||
_("Actions/conditions/expression to change or "
|
||||
"check the scale of an object (default: 1)."),
|
||||
_("Change the object scale."),
|
||||
"Florian Rival",
|
||||
"Open source (MIT License)")
|
||||
.SetExtensionHelpPath("/objects");
|
||||
extension.AddInstructionOrExpressionGroupMetadata(_("Scalable capability"))
|
||||
.SetIcon("res/actions/scale24_black.png");
|
||||
extension.AddInstructionOrExpressionGroupMetadata(_("Size")).SetIcon(
|
||||
"res/actions/scale24_black.png");
|
||||
extension.AddInstructionOrExpressionGroupMetadata(_("Size"))
|
||||
.SetIcon("res/actions/scale24_black.png");
|
||||
|
||||
gd::BehaviorMetadata& aut =
|
||||
extension
|
||||
.AddBehavior("ScalableBehavior",
|
||||
_("Scalable capability"),
|
||||
"Scale",
|
||||
_("Actions/conditions/expression to change or check the "
|
||||
"scale of an object (default: 1)."),
|
||||
"",
|
||||
"res/actions/scale24_black.png",
|
||||
"ResizableBehavior",
|
||||
std::make_shared<gd::Behavior>(),
|
||||
std::make_shared<gd::BehaviorsSharedData>())
|
||||
.SetHidden();
|
||||
gd::BehaviorMetadata& aut = extension.AddBehavior(
|
||||
"ScalableBehavior",
|
||||
_("Scalable capability"),
|
||||
"Scale",
|
||||
_("Change the object scale."),
|
||||
"",
|
||||
"res/actions/scale24_black.png",
|
||||
"ResizableBehavior",
|
||||
std::make_shared<gd::Behavior>(),
|
||||
std::make_shared<gd::BehaviorsSharedData>())
|
||||
.SetHidden();
|
||||
|
||||
aut.AddExpressionAndConditionAndAction(
|
||||
"number",
|
||||
|
@@ -16,9 +16,7 @@ BuiltinExtensionsImplementer::ImplementsCommonConversionsExtension(
|
||||
.SetExtensionInformation(
|
||||
"BuiltinCommonConversions",
|
||||
_("Conversion"),
|
||||
"Expressions to convert numbers to string, strings to numbers, "
|
||||
"angles (degrees from/to radians) and a GDevelop variable to/from a "
|
||||
"JSON string.",
|
||||
"Expressions to convert number, texts and quantities.",
|
||||
"Florian Rival",
|
||||
"Open source (MIT License)")
|
||||
.SetExtensionHelpPath("/all-features/common-conversions");
|
||||
@@ -43,7 +41,7 @@ BuiltinExtensionsImplementer::ImplementsCommonConversionsExtension(
|
||||
|
||||
extension
|
||||
.AddStrExpression("LargeNumberToString",
|
||||
_("Number > Text (without scientific notation)"),
|
||||
_("Number > Text ( without scientific notation )"),
|
||||
_("Convert the result of the expression to text, "
|
||||
"without using the scientific notation"),
|
||||
"",
|
||||
@@ -74,8 +72,7 @@ BuiltinExtensionsImplementer::ImplementsCommonConversionsExtension(
|
||||
_("Convert a variable to JSON"),
|
||||
_("JSON"),
|
||||
"res/conditions/toujours24_black.png")
|
||||
.AddParameter("variable",
|
||||
_("The variable to be stringified"),
|
||||
.AddParameter("variable", _("The variable to be stringified"),
|
||||
"AllowUndeclaredVariable");
|
||||
|
||||
// Deprecated
|
||||
|
@@ -15,11 +15,10 @@ void GD_CORE_API BuiltinExtensionsImplementer::ImplementsKeyboardExtension(
|
||||
.SetExtensionInformation(
|
||||
"BuiltinKeyboard",
|
||||
_("Keyboard"),
|
||||
_("Conditions to check keys pressed on a keyboard. Note that this "
|
||||
_("Allows your game to respond to keyboard input. Note that this "
|
||||
"does not work with on-screen keyboard on touch devices: use "
|
||||
"instead mouse/touch conditions when making a game for "
|
||||
"mobile/touchscreen devices or when making a new game from "
|
||||
"scratch."),
|
||||
"instead conditions related to touch when making a game for "
|
||||
"mobile/touchscreen devices."),
|
||||
"Florian Rival",
|
||||
"Open source (MIT License)")
|
||||
.SetExtensionHelpPath("/all-features/keyboard")
|
||||
@@ -85,7 +84,7 @@ void GD_CORE_API BuiltinExtensionsImplementer::ImplementsKeyboardExtension(
|
||||
"res/conditions/keyboard.png")
|
||||
.AddCodeOnlyParameter("currentScene", "");
|
||||
|
||||
extension
|
||||
extension
|
||||
.AddCondition("AnyKeyReleased",
|
||||
_("Any key released"),
|
||||
_("Check if any key is released"),
|
||||
|
@@ -16,11 +16,8 @@ void GD_CORE_API BuiltinExtensionsImplementer::ImplementsMouseExtension(
|
||||
.SetExtensionInformation(
|
||||
"BuiltinMouse",
|
||||
_("Mouse and touch"),
|
||||
"Conditions, actions and expressions to handle either the mouse or "
|
||||
"touches on a touchscreen. Notably: cursor position, mouse wheel, "
|
||||
"mouse buttons, touch positions, started/end touches, etc...\n"
|
||||
"\n"
|
||||
"By default, conditions related to the mouse will also "
|
||||
"Conditions and actions to handle either the mouse or touches on "
|
||||
"touchscreen. By default, conditions related to the mouse will also "
|
||||
"handle the touches - so that it's easier to handle both in your "
|
||||
"game. You can disable this behavior if you want to handle them "
|
||||
"separately in different events.",
|
||||
@@ -276,26 +273,28 @@ void GD_CORE_API BuiltinExtensionsImplementer::ImplementsMouseExtension(
|
||||
.SetHidden();
|
||||
|
||||
extension
|
||||
.AddCondition("MouseButtonFromTextPressed",
|
||||
_("Mouse button pressed or touch held"),
|
||||
_("Check if the specified mouse button is pressed or "
|
||||
"if a touch is in contact with the screen."),
|
||||
_("Touch or _PARAM1_ mouse button is down"),
|
||||
"",
|
||||
"res/conditions/mouse24.png",
|
||||
"res/conditions/mouse.png")
|
||||
.AddCondition(
|
||||
"MouseButtonFromTextPressed",
|
||||
_("Mouse button pressed or touch held"),
|
||||
_("Check if the specified mouse button is pressed or "
|
||||
"if a touch is in contact with the screen."),
|
||||
_("Touch or _PARAM1_ mouse button is down"),
|
||||
"",
|
||||
"res/conditions/mouse24.png",
|
||||
"res/conditions/mouse.png")
|
||||
.AddCodeOnlyParameter("currentScene", "")
|
||||
.AddParameter("mouseButton", _("Button to check"))
|
||||
.MarkAsSimple();
|
||||
|
||||
extension
|
||||
.AddCondition("MouseButtonFromTextReleased",
|
||||
_("Mouse button released"),
|
||||
_("Check if the specified mouse button was released."),
|
||||
_("Touch or _PARAM1_ mouse button is released"),
|
||||
"",
|
||||
"res/conditions/mouse24.png",
|
||||
"res/conditions/mouse.png")
|
||||
.AddCondition(
|
||||
"MouseButtonFromTextReleased",
|
||||
_("Mouse button released"),
|
||||
_("Check if the specified mouse button was released."),
|
||||
_("Touch or _PARAM1_ mouse button is released"),
|
||||
"",
|
||||
"res/conditions/mouse24.png",
|
||||
"res/conditions/mouse.png")
|
||||
.AddCodeOnlyParameter("currentScene", "")
|
||||
.AddParameter("mouseButton", _("Button to check"))
|
||||
.MarkAsSimple();
|
||||
|
@@ -15,9 +15,8 @@ void GD_CORE_API BuiltinExtensionsImplementer::ImplementsNetworkExtension(
|
||||
.SetExtensionInformation(
|
||||
"BuiltinNetwork",
|
||||
_("Network"),
|
||||
_("Actions to send web requests, communicate with external \"APIs\" "
|
||||
"and other network related tasks. Also contains an action to open "
|
||||
"a URL on the device browser."),
|
||||
_("Features to send web requests, communicate with external \"APIs\" "
|
||||
"and other network related tasks."),
|
||||
"Florian Rival",
|
||||
"Open source (MIT License)")
|
||||
.SetExtensionHelpPath("/all-features/network")
|
||||
|
@@ -4,8 +4,8 @@
|
||||
* reserved. This project is released under the MIT License.
|
||||
*/
|
||||
#include "AllBuiltinExtensions.h"
|
||||
#include "GDCore/Extensions/Metadata/MultipleInstructionMetadata.h"
|
||||
#include "GDCore/Tools/Localization.h"
|
||||
#include "GDCore/Extensions/Metadata/MultipleInstructionMetadata.h"
|
||||
|
||||
using namespace std;
|
||||
namespace gd {
|
||||
@@ -16,11 +16,7 @@ void GD_CORE_API BuiltinExtensionsImplementer::ImplementsSceneExtension(
|
||||
.SetExtensionInformation(
|
||||
"BuiltinScene",
|
||||
_("Scene"),
|
||||
_("Actions/conditions to change the current scene (or pause it and "
|
||||
"launch another one, or go back to the previous one), check if a "
|
||||
"scene or the game has just started/resumed, preload assets of a "
|
||||
"scene, get the current scene name or loading progress, quit the "
|
||||
"game, set background color, or disable input when focus is lost."),
|
||||
_("Actions and conditions to manipulate the scenes during the game."),
|
||||
"Florian Rival",
|
||||
"Open source (MIT License)")
|
||||
.SetExtensionHelpPath("" /*TODO: Add a documentation page for this */);
|
||||
@@ -170,28 +166,25 @@ void GD_CORE_API BuiltinExtensionsImplementer::ImplementsSceneExtension(
|
||||
.AddCodeOnlyParameter("currentScene", "");
|
||||
|
||||
extension
|
||||
.AddAction(
|
||||
"PrioritizeLoadingOfScene",
|
||||
_("Preload scene"),
|
||||
_("Preload a scene resources as soon as possible in background."),
|
||||
_("Preload scene _PARAM1_ in background"),
|
||||
"",
|
||||
"res/actions/hourglass_black.svg",
|
||||
"res/actions/hourglass_black.svg")
|
||||
.AddAction("PrioritizeLoadingOfScene",
|
||||
_("Preload scene"),
|
||||
_("Preload a scene resources as soon as possible in background."),
|
||||
_("Preload scene _PARAM1_ in background"),
|
||||
"",
|
||||
"res/actions/hourglass_black.svg",
|
||||
"res/actions/hourglass_black.svg")
|
||||
.SetHelpPath("/all-features/resources-loading")
|
||||
.AddCodeOnlyParameter("currentScene", "")
|
||||
.AddParameter("sceneName", _("Name of the new scene"))
|
||||
.MarkAsAdvanced();
|
||||
|
||||
extension
|
||||
.AddExpressionAndCondition("number",
|
||||
"SceneLoadingProgress",
|
||||
_("Scene loading progress"),
|
||||
_("The progress of resources loading in "
|
||||
"background for a scene (between 0 and 1)."),
|
||||
_("_PARAM1_ loading progress"),
|
||||
_(""),
|
||||
"res/actions/hourglass_black.svg")
|
||||
extension.AddExpressionAndCondition("number",
|
||||
"SceneLoadingProgress",
|
||||
_("Scene loading progress"),
|
||||
_("The progress of resources loading in background for a scene (between 0 and 1)."),
|
||||
_("_PARAM1_ loading progress"),
|
||||
_(""),
|
||||
"res/actions/hourglass_black.svg")
|
||||
.SetHelpPath("/all-features/resources-loading")
|
||||
.AddCodeOnlyParameter("currentScene", "")
|
||||
.AddParameter("sceneName", _("Scene name"))
|
||||
@@ -199,14 +192,13 @@ void GD_CORE_API BuiltinExtensionsImplementer::ImplementsSceneExtension(
|
||||
.MarkAsAdvanced();
|
||||
|
||||
extension
|
||||
.AddCondition(
|
||||
"AreSceneAssetsLoaded",
|
||||
_("Scene preloaded"),
|
||||
_("Check if scene resources have finished to load in background."),
|
||||
_("Scene _PARAM1_ was preloaded in background"),
|
||||
"",
|
||||
"res/actions/hourglass_black.svg",
|
||||
"res/actions/hourglass_black.svg")
|
||||
.AddCondition("AreSceneAssetsLoaded",
|
||||
_("Scene preloaded"),
|
||||
_("Check if scene resources have finished to load in background."),
|
||||
_("Scene _PARAM1_ was preloaded in background"),
|
||||
"",
|
||||
"res/actions/hourglass_black.svg",
|
||||
"res/actions/hourglass_black.svg")
|
||||
.SetHelpPath("/all-features/resources-loading")
|
||||
.AddCodeOnlyParameter("currentScene", "")
|
||||
.AddParameter("sceneName", _("Scene name"))
|
||||
|
@@ -15,13 +15,12 @@ namespace gd {
|
||||
void GD_CORE_API BuiltinExtensionsImplementer::ImplementsSpriteExtension(
|
||||
gd::PlatformExtension& extension) {
|
||||
extension
|
||||
.SetExtensionInformation(
|
||||
"Sprite",
|
||||
_("Sprite"),
|
||||
_("Sprite are animated objects which can be used "
|
||||
"for most elements of a 2D game."),
|
||||
"Florian Rival",
|
||||
"Open source (MIT License)")
|
||||
.SetExtensionInformation("Sprite",
|
||||
_("Sprite"),
|
||||
_("Sprite are animated object which can be used "
|
||||
"for most elements of a game."),
|
||||
"Florian Rival",
|
||||
"Open source (MIT License)")
|
||||
.SetExtensionHelpPath("/objects/sprite");
|
||||
extension.AddInstructionOrExpressionGroupMetadata(_("Sprite"))
|
||||
.SetIcon("CppPlatform/Extensions/spriteicon.png");
|
||||
@@ -31,7 +30,7 @@ void GD_CORE_API BuiltinExtensionsImplementer::ImplementsSpriteExtension(
|
||||
.AddObject<SpriteObject>("Sprite",
|
||||
_("Sprite"),
|
||||
_("Animated object which can be used for "
|
||||
"most elements of a 2D game."),
|
||||
"most elements of a game."),
|
||||
"CppPlatform/Extensions/spriteicon.png")
|
||||
.SetCategoryFullName(_("General"))
|
||||
.SetOpenFullEditorLabel(_("Edit animations"))
|
||||
@@ -646,12 +645,11 @@ void GD_CORE_API BuiltinExtensionsImplementer::ImplementsSpriteExtension(
|
||||
"res/actions/sprite.png")
|
||||
.AddParameter("object", _("Object"), "Sprite");
|
||||
|
||||
obj.AddExpression(
|
||||
"AnimationFrameCount",
|
||||
_("Number of frames"),
|
||||
_("Number of frames in the current animation of the object"),
|
||||
_("Animations and images"),
|
||||
"res/actions/sprite.png")
|
||||
obj.AddExpression("AnimationFrameCount",
|
||||
_("Number of frames"),
|
||||
_("Number of frames in the current animation of the object"),
|
||||
_("Animations and images"),
|
||||
"res/actions/sprite.png")
|
||||
.AddParameter("object", _("Object"), "Sprite");
|
||||
|
||||
// Deprecated
|
||||
|
@@ -16,8 +16,7 @@ BuiltinExtensionsImplementer::ImplementsStringInstructionsExtension(
|
||||
.SetExtensionInformation(
|
||||
"BuiltinStringInstructions",
|
||||
_("Text manipulation"),
|
||||
"Provides expressions to manipulate strings (also called texts): new "
|
||||
"line, upper/lowercase, substring, find, replace, etc...",
|
||||
"Provides expressions to manipulate strings (also called texts).",
|
||||
"Florian Rival",
|
||||
"Open source (MIT License)")
|
||||
.SetExtensionHelpPath("" /*TODO: Add a documentation page for this */);
|
||||
@@ -192,8 +191,7 @@ BuiltinExtensionsImplementer::ImplementsStringInstructionsExtension(
|
||||
"res/conditions/toujours24_black.png")
|
||||
.AddParameter("string", _("Text in which the replacement must be done"))
|
||||
.AddParameter("string", _("Text to find inside the first text"))
|
||||
.AddParameter("string",
|
||||
_("Replacement to put instead of the text to find"));
|
||||
.AddParameter("string", _("Replacement to put instead of the text to find"));
|
||||
|
||||
extension
|
||||
.AddStrExpression("StrReplaceAll",
|
||||
@@ -201,11 +199,10 @@ BuiltinExtensionsImplementer::ImplementsStringInstructionsExtension(
|
||||
_("Replace all occurrences of a text by another."),
|
||||
"",
|
||||
"res/conditions/toujours24_black.png")
|
||||
.AddParameter("string",
|
||||
_("Text in which the replacement(s) must be done"))
|
||||
.AddParameter("string", _("Text in which the replacement(s) must be done"))
|
||||
.AddParameter("string", _("Text to find inside the first text"))
|
||||
.AddParameter("string",
|
||||
_("Replacement to put instead of the text to find"));
|
||||
.AddParameter("string", _("Replacement to put instead of the text to find"));
|
||||
|
||||
}
|
||||
|
||||
} // namespace gd
|
||||
|
@@ -15,12 +15,9 @@ void GD_CORE_API BuiltinExtensionsImplementer::ImplementsTimeExtension(
|
||||
.SetExtensionInformation(
|
||||
"BuiltinTime",
|
||||
_("Timers and time"),
|
||||
"Actions and conditions to start, pause or reset scene timers, "
|
||||
"modify the time scale (speed at which the game "
|
||||
"is running - useful for slow motion effects). Also contains an "
|
||||
"action that wait for a delay before running the next actions and "
|
||||
"sub-events and expressions to read the time scale, time delta of "
|
||||
"the last frame or timer elapsed time.",
|
||||
"Actions and conditions to run timers, get the current time or "
|
||||
"modify the time scale (speed at which the game is running - useful "
|
||||
"for slow motion effects).",
|
||||
"Florian Rival",
|
||||
"Open source (MIT License)")
|
||||
.SetExtensionHelpPath("/all-features/timers-and-time");
|
||||
@@ -195,28 +192,26 @@ void GD_CORE_API BuiltinExtensionsImplementer::ImplementsTimeExtension(
|
||||
extension
|
||||
.AddExpression("TimerElapsedTime",
|
||||
_("Scene timer value"),
|
||||
_("Value of a scene timer (in seconds)"),
|
||||
_("Value of a scene timer"),
|
||||
"",
|
||||
"res/actions/time.png")
|
||||
.AddCodeOnlyParameter("currentScene", "")
|
||||
.AddParameter("identifier", _("Timer's name"), "sceneTimer");
|
||||
|
||||
extension
|
||||
.AddExpression(
|
||||
"TimeFromStart",
|
||||
_("Time elapsed since the beginning of the scene (in seconds)."),
|
||||
_("Time elapsed since the beginning of the scene (in seconds)."),
|
||||
"",
|
||||
"res/actions/time.png")
|
||||
.AddExpression("TimeFromStart",
|
||||
_("Time elapsed since the beginning of the scene"),
|
||||
_("Time elapsed since the beginning of the scene"),
|
||||
"",
|
||||
"res/actions/time.png")
|
||||
.AddCodeOnlyParameter("currentScene", "");
|
||||
|
||||
extension
|
||||
.AddExpression(
|
||||
"TempsDebut",
|
||||
_("Time elapsed since the beginning of the scene (in seconds)."),
|
||||
_("Time elapsed since the beginning of the scene (in seconds)."),
|
||||
"",
|
||||
"res/actions/time.png")
|
||||
.AddExpression("TempsDebut",
|
||||
_("Time elapsed since the beginning of the scene"),
|
||||
_("Time elapsed since the beginning of the scene"),
|
||||
"",
|
||||
"res/actions/time.png")
|
||||
.SetHidden()
|
||||
.AddCodeOnlyParameter("currentScene", "");
|
||||
|
||||
@@ -231,21 +226,16 @@ void GD_CORE_API BuiltinExtensionsImplementer::ImplementsTimeExtension(
|
||||
extension
|
||||
.AddExpression("Time",
|
||||
_("Current time"),
|
||||
_("Gives the current time"),
|
||||
_("Current time"),
|
||||
"",
|
||||
"res/actions/time.png")
|
||||
.AddCodeOnlyParameter("currentScene", "")
|
||||
.AddParameter(
|
||||
"stringWithSelector",
|
||||
_("- Hour of the day: \"hour\"\n"
|
||||
"- Minutes: \"min\"\n"
|
||||
"- Seconds: \"sec\"\n"
|
||||
"- Day of month: \"mday\"\n"
|
||||
"- Months since January: \"mon\"\n"
|
||||
"- Year since 1900: \"year\"\n"
|
||||
"- Days since Sunday: \"wday\"\n"
|
||||
"- Days since Jan 1st: \"yday\"\n"
|
||||
"- Timestamp (ms): \"timestamp\""),
|
||||
_("Hour: hour - Minutes: min - Seconds: sec - Day of month: "
|
||||
"mday - Months since January: mon - Year since 1900: year - Days "
|
||||
"since Sunday: wday - Days since Jan 1st: yday - Timestamp (ms): "
|
||||
"timestamp\""),
|
||||
"[\"hour\", \"min\", \"sec\", \"mon\", \"year\", \"wday\", \"mday\", "
|
||||
"\"yday\", \"timestamp\"]");
|
||||
}
|
||||
|
@@ -15,17 +15,16 @@ void GD_CORE_API BuiltinExtensionsImplementer::ImplementsWindowExtension(
|
||||
.SetExtensionInformation(
|
||||
"BuiltinWindow",
|
||||
_("Game window and resolution"),
|
||||
"Actions and conditions to manipulate the game window or change how "
|
||||
"the game is resized according to the screen size. "
|
||||
"Provides actions and conditions to manipulate the game window. "
|
||||
"Depending on the platform on which the game is running, not all of "
|
||||
"these features can be applied.\n"
|
||||
"Also contains expressions to read the screen size.",
|
||||
"these features can be applied.",
|
||||
"Florian Rival",
|
||||
"Open source (MIT License)")
|
||||
.SetCategory("User interface")
|
||||
.SetExtensionHelpPath("/all-features/window");
|
||||
extension
|
||||
.AddInstructionOrExpressionGroupMetadata(_("Game window and resolution"))
|
||||
.AddInstructionOrExpressionGroupMetadata(
|
||||
_("Game window and resolution"))
|
||||
.SetIcon("res/actions/window24.png");
|
||||
|
||||
extension
|
||||
|
@@ -63,6 +63,7 @@ void EventsBasedObjectVariantHelper::ComplyVariantsToEventsBasedObject(
|
||||
}
|
||||
|
||||
// Copy missing behaviors
|
||||
auto &behaviors = object.GetAllBehaviorContents();
|
||||
for (const auto &pair : defaultBehaviors) {
|
||||
const auto &behaviorName = pair.first;
|
||||
const auto &defaultBehavior = pair.second;
|
||||
@@ -81,9 +82,11 @@ void EventsBasedObjectVariantHelper::ComplyVariantsToEventsBasedObject(
|
||||
}
|
||||
}
|
||||
// Delete extra behaviors
|
||||
for (auto &behaviorName : object.GetAllBehaviorNames()) {
|
||||
for (auto it = behaviors.begin(); it != behaviors.end(); ++it) {
|
||||
const auto &behaviorName = it->first;
|
||||
if (!defaultObject->HasBehaviorNamed(behaviorName)) {
|
||||
object.RemoveBehavior(behaviorName);
|
||||
--it;
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -8,8 +8,6 @@
|
||||
#include "GDCore/Serialization/SerializerElement.h"
|
||||
|
||||
namespace gd {
|
||||
|
||||
gd::String Effect::badStringParameterValue;
|
||||
|
||||
void Effect::SerializeTo(SerializerElement& element) const {
|
||||
element.SetAttribute("name", GetName());
|
||||
|
@@ -34,42 +34,39 @@ class GD_CORE_API Effect {
|
||||
void SetFolded(bool fold = true) { folded = fold; }
|
||||
bool IsFolded() const { return folded; }
|
||||
|
||||
void SetDoubleParameter(const gd::String &name, double value) {
|
||||
void SetDoubleParameter(const gd::String& name, double value) {
|
||||
doubleParameters[name] = value;
|
||||
}
|
||||
|
||||
double GetDoubleParameter(const gd::String &name) const {
|
||||
auto itr = doubleParameters.find(name);
|
||||
return itr == doubleParameters.end() ? 0 : itr->second;
|
||||
double GetDoubleParameter(const gd::String& name) {
|
||||
return doubleParameters[name];
|
||||
}
|
||||
|
||||
bool HasDoubleParameter(const gd::String &name) const {
|
||||
bool HasDoubleParameter(const gd::String& name) {
|
||||
return doubleParameters.find(name) != doubleParameters.end();
|
||||
}
|
||||
|
||||
void SetStringParameter(const gd::String &name, const gd::String &value) {
|
||||
void SetStringParameter(const gd::String& name, const gd::String& value) {
|
||||
stringParameters[name] = value;
|
||||
}
|
||||
|
||||
const gd::String &GetStringParameter(const gd::String &name) const {
|
||||
auto itr = stringParameters.find(name);
|
||||
return itr == stringParameters.end() ? badStringParameterValue : itr->second;
|
||||
const gd::String& GetStringParameter(const gd::String& name) {
|
||||
return stringParameters[name];
|
||||
}
|
||||
|
||||
bool HasStringParameter(const gd::String &name) const {
|
||||
bool HasStringParameter(const gd::String& name) {
|
||||
return stringParameters.find(name) != stringParameters.end();
|
||||
}
|
||||
|
||||
void SetBooleanParameter(const gd::String &name, bool value) {
|
||||
void SetBooleanParameter(const gd::String& name, bool value) {
|
||||
booleanParameters[name] = value;
|
||||
}
|
||||
|
||||
bool GetBooleanParameter(const gd::String &name) const {
|
||||
auto itr = booleanParameters.find(name);
|
||||
return itr == booleanParameters.end() ? false : itr->second;
|
||||
bool GetBooleanParameter(const gd::String& name) {
|
||||
return booleanParameters[name];
|
||||
}
|
||||
|
||||
bool HasBooleanParameter(const gd::String &name) const {
|
||||
bool HasBooleanParameter(const gd::String& name) {
|
||||
return booleanParameters.find(name) != booleanParameters.end();
|
||||
}
|
||||
|
||||
@@ -108,9 +105,6 @@ class GD_CORE_API Effect {
|
||||
std::map<gd::String, double> doubleParameters; ///< Values of parameters being doubles, keyed by names.
|
||||
std::map<gd::String, gd::String> stringParameters; ///< Values of parameters being strings, keyed by names.
|
||||
std::map<gd::String, bool> booleanParameters; ///< Values of parameters being booleans, keyed by names.
|
||||
|
||||
static gd::String badStringParameterValue; ///< Empty string returned by
|
||||
///< GeStringParameter
|
||||
};
|
||||
|
||||
} // namespace gd
|
||||
|
@@ -5,6 +5,8 @@ namespace gdjs {
|
||||
type Object3DNetworkSyncDataType = {
|
||||
// z is position on the Z axis, different from zo, which is Z order
|
||||
z: number;
|
||||
w: number;
|
||||
h: number;
|
||||
d: number;
|
||||
rx: number;
|
||||
ry: number;
|
||||
@@ -114,6 +116,8 @@ namespace gdjs {
|
||||
return {
|
||||
...super.getNetworkSyncData(),
|
||||
z: this.getZ(),
|
||||
w: this.getWidth(),
|
||||
h: this.getHeight(),
|
||||
d: this.getDepth(),
|
||||
rx: this.getRotationX(),
|
||||
ry: this.getRotationY(),
|
||||
@@ -126,6 +130,8 @@ namespace gdjs {
|
||||
updateFromNetworkSyncData(networkSyncData: Object3DNetworkSyncData) {
|
||||
super.updateFromNetworkSyncData(networkSyncData);
|
||||
if (networkSyncData.z !== undefined) this.setZ(networkSyncData.z);
|
||||
if (networkSyncData.w !== undefined) this.setWidth(networkSyncData.w);
|
||||
if (networkSyncData.h !== undefined) this.setHeight(networkSyncData.h);
|
||||
if (networkSyncData.d !== undefined) this.setDepth(networkSyncData.d);
|
||||
if (networkSyncData.rx !== undefined)
|
||||
this.setRotationX(networkSyncData.rx);
|
||||
|
@@ -1,12 +1,4 @@
|
||||
namespace gdjs {
|
||||
type CustomObject3DNetworkSyncDataType = CustomObjectNetworkSyncDataType & {
|
||||
z: float;
|
||||
d: float;
|
||||
rx: float;
|
||||
ry: float;
|
||||
ifz: boolean;
|
||||
};
|
||||
|
||||
/**
|
||||
* Base class for 3D custom objects.
|
||||
*/
|
||||
@@ -85,30 +77,6 @@ namespace gdjs {
|
||||
}
|
||||
}
|
||||
|
||||
getNetworkSyncData(): CustomObject3DNetworkSyncDataType {
|
||||
return {
|
||||
...super.getNetworkSyncData(),
|
||||
z: this.getZ(),
|
||||
d: this.getDepth(),
|
||||
rx: this.getRotationX(),
|
||||
ry: this.getRotationY(),
|
||||
ifz: this.isFlippedZ(),
|
||||
};
|
||||
}
|
||||
|
||||
updateFromNetworkSyncData(
|
||||
networkSyncData: CustomObject3DNetworkSyncDataType
|
||||
): void {
|
||||
super.updateFromNetworkSyncData(networkSyncData);
|
||||
if (networkSyncData.z !== undefined) this.setZ(networkSyncData.z);
|
||||
if (networkSyncData.d !== undefined) this.setDepth(networkSyncData.d);
|
||||
if (networkSyncData.rx !== undefined)
|
||||
this.setRotationX(networkSyncData.rx);
|
||||
if (networkSyncData.ry !== undefined)
|
||||
this.setRotationY(networkSyncData.ry);
|
||||
if (networkSyncData.ifz !== undefined) this.flipZ(networkSyncData.ifz);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the object position on the Z axis.
|
||||
*/
|
||||
|
@@ -242,7 +242,7 @@ module.exports = {
|
||||
.addObject(
|
||||
'Model3DObject',
|
||||
_('3D Model'),
|
||||
_('An animated 3D model, useful for most elements of a 3D game.'),
|
||||
_('An animated 3D model.'),
|
||||
'JsPlatform/Extensions/3d_model.svg',
|
||||
new gd.Model3DObjectConfiguration()
|
||||
)
|
||||
@@ -1125,7 +1125,7 @@ module.exports = {
|
||||
topFaceResourceName: '',
|
||||
bottomFaceResourceName: '',
|
||||
frontFaceVisible: true,
|
||||
backFaceVisible: true,
|
||||
backFaceVisible: false,
|
||||
leftFaceVisible: true,
|
||||
rightFaceVisible: true,
|
||||
topFaceVisible: true,
|
||||
|
@@ -94,16 +94,6 @@ module.exports = {
|
||||
.setLabel(_('Vertical alignment'))
|
||||
.setGroup(_('Appearance'));
|
||||
|
||||
if (!objectContent.lineHeight) {
|
||||
objectContent.lineHeight = 0;
|
||||
}
|
||||
objectProperties
|
||||
.getOrCreate('lineHeight')
|
||||
.setValue(objectContent.lineHeight.toString())
|
||||
.setType('number')
|
||||
.setLabel(_('Line height (0 = default)'))
|
||||
.setGroup(_('Font'));
|
||||
|
||||
objectProperties
|
||||
.getOrCreate('fontFamily')
|
||||
.setValue(objectContent.fontFamily)
|
||||
@@ -404,19 +394,6 @@ module.exports = {
|
||||
expressionLabel: _('Get the wrapping width'),
|
||||
expressionDescription: _('Get the wrapping width'),
|
||||
},
|
||||
{
|
||||
functionName: 'LineHeight',
|
||||
iconPath: 'res/actions/characterSize24.png',
|
||||
type: 'number',
|
||||
instructionLabel: _('Line height'),
|
||||
paramLabel: _('Line height (0 = default)'),
|
||||
conditionDescription: _('Compare the base line height of the text.'),
|
||||
conditionSentence: _('the base line height'),
|
||||
actionDescription: _('Set base line height'),
|
||||
actionSentence: _('the base line height'),
|
||||
expressionLabel: _('Get the base line height'),
|
||||
expressionDescription: _('Get the base line height'),
|
||||
},
|
||||
];
|
||||
|
||||
addSettersAndGettersToObject(object, setterAndGetterProperties, 'BBText');
|
||||
@@ -531,7 +508,7 @@ module.exports = {
|
||||
associatedObjectConfiguration,
|
||||
pixiContainer,
|
||||
pixiResourcesLoader,
|
||||
getPropertyOverridings
|
||||
propertyOverridings
|
||||
) {
|
||||
super(
|
||||
project,
|
||||
@@ -539,7 +516,7 @@ module.exports = {
|
||||
associatedObjectConfiguration,
|
||||
pixiContainer,
|
||||
pixiResourcesLoader,
|
||||
getPropertyOverridings
|
||||
propertyOverridings
|
||||
);
|
||||
|
||||
const bbTextStyles = {
|
||||
@@ -578,11 +555,9 @@ module.exports = {
|
||||
gd.ObjectJsImplementation
|
||||
);
|
||||
|
||||
const propertyOverridings = this.getPropertyOverridings();
|
||||
const rawText =
|
||||
propertyOverridings && propertyOverridings.has('Text')
|
||||
? propertyOverridings.get('Text')
|
||||
: object.content.text;
|
||||
const rawText = this._propertyOverridings.has('Text')
|
||||
? this._propertyOverridings.get('Text')
|
||||
: object.content.text;
|
||||
if (rawText !== this._pixiObject.text) {
|
||||
this._pixiObject.text = rawText;
|
||||
}
|
||||
|
@@ -32,7 +32,6 @@ namespace gdjs {
|
||||
wordWrap: runtimeObject._wrapping,
|
||||
wordWrapWidth: runtimeObject._wrappingWidth,
|
||||
align: runtimeObject._textAlign as PIXI.TextStyleAlign | undefined,
|
||||
lineHeight: runtimeObject._lineHeight > 0 ? runtimeObject._lineHeight : undefined,
|
||||
},
|
||||
});
|
||||
instanceContainer
|
||||
@@ -103,13 +102,6 @@ namespace gdjs {
|
||||
this._pixiObject.dirty = true;
|
||||
}
|
||||
|
||||
updateLineHeight(): void {
|
||||
//@ts-ignore Private member usage.
|
||||
this._pixiObject.textStyles.default.lineHeight =
|
||||
this._object._lineHeight;
|
||||
this._pixiObject.dirty = true;
|
||||
}
|
||||
|
||||
updatePosition(): void {
|
||||
if (this._object.isWrapping() && this._pixiObject.width !== 0) {
|
||||
const alignmentX =
|
||||
|
@@ -20,8 +20,6 @@ namespace gdjs {
|
||||
/** Alignment of the text: "left", "center" or "right" */
|
||||
align: 'left' | 'center' | 'right';
|
||||
verticalTextAlignment: 'top' | 'center' | 'bottom';
|
||||
/** Line height for multiline text (0 = default) */
|
||||
lineHeight: number;
|
||||
};
|
||||
};
|
||||
export type BBTextObjectData = ObjectData & BBTextObjectDataType;
|
||||
@@ -37,7 +35,6 @@ namespace gdjs {
|
||||
align: string;
|
||||
vta: string;
|
||||
hidden: boolean;
|
||||
lh: number;
|
||||
};
|
||||
|
||||
export type BBTextObjectNetworkSyncData = ObjectNetworkSyncData &
|
||||
@@ -64,7 +61,6 @@ namespace gdjs {
|
||||
|
||||
_textAlign: string;
|
||||
_verticalTextAlignment: string;
|
||||
_lineHeight: float;
|
||||
|
||||
_renderer: gdjs.BBTextRuntimeObjectRenderer;
|
||||
|
||||
@@ -91,7 +87,6 @@ namespace gdjs {
|
||||
this._textAlign = objectData.content.align;
|
||||
this._verticalTextAlignment =
|
||||
objectData.content.verticalTextAlignment || 'top';
|
||||
this._lineHeight = objectData.content.lineHeight || 0;
|
||||
this.hidden = !objectData.content.visible;
|
||||
|
||||
this._renderer = new gdjs.BBTextRuntimeObjectRenderer(
|
||||
@@ -147,11 +142,6 @@ namespace gdjs {
|
||||
newObjectData.content.verticalTextAlignment
|
||||
);
|
||||
}
|
||||
if (
|
||||
oldObjectData.content.lineHeight !== newObjectData.content.lineHeight
|
||||
) {
|
||||
this.setLineHeight(newObjectData.content.lineHeight);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -168,7 +158,6 @@ namespace gdjs {
|
||||
align: this._textAlign,
|
||||
vta: this._verticalTextAlignment,
|
||||
hidden: this.hidden,
|
||||
lh: this._lineHeight,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -207,9 +196,6 @@ namespace gdjs {
|
||||
if (this.hidden !== undefined) {
|
||||
this.hide(networkSyncData.hidden);
|
||||
}
|
||||
if (networkSyncData.lh !== undefined) {
|
||||
this.setLineHeight(networkSyncData.lh);
|
||||
}
|
||||
}
|
||||
|
||||
override extraInitializationFromInitialInstance(
|
||||
@@ -397,10 +383,6 @@ namespace gdjs {
|
||||
return this._renderer.getHeight();
|
||||
}
|
||||
|
||||
override setWidth(width: float): void {
|
||||
this.setWrappingWidth(width);
|
||||
}
|
||||
|
||||
override getDrawableY(): float {
|
||||
return (
|
||||
this.getY() -
|
||||
@@ -411,23 +393,6 @@ namespace gdjs {
|
||||
: 0)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get line height of the BBText object.
|
||||
* @return line height (0 = default)
|
||||
*/
|
||||
getLineHeight(): number {
|
||||
return this._lineHeight;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set line height of the BBText object.
|
||||
* @param value line height (0 = default)
|
||||
*/
|
||||
setLineHeight(value: float): void {
|
||||
this._lineHeight = value;
|
||||
this._renderer.updateLineHeight();
|
||||
}
|
||||
}
|
||||
// @ts-ignore
|
||||
gdjs.registerObject('BBText::BBText', gdjs.BBTextRuntimeObject);
|
||||
|
@@ -631,7 +631,7 @@ module.exports = {
|
||||
associatedObjectConfiguration,
|
||||
pixiContainer,
|
||||
pixiResourcesLoader,
|
||||
getPropertyOverridings
|
||||
propertyOverridings
|
||||
) {
|
||||
super(
|
||||
project,
|
||||
@@ -639,7 +639,7 @@ module.exports = {
|
||||
associatedObjectConfiguration,
|
||||
pixiContainer,
|
||||
pixiResourcesLoader,
|
||||
getPropertyOverridings
|
||||
propertyOverridings
|
||||
);
|
||||
|
||||
// We'll track changes of the font to trigger the loading of the new font.
|
||||
@@ -665,11 +665,9 @@ module.exports = {
|
||||
|
||||
// Update the rendered text properties (note: Pixi is only
|
||||
// applying changes if there were changed).
|
||||
const propertyOverridings = this.getPropertyOverridings();
|
||||
this._pixiObject.text =
|
||||
propertyOverridings && propertyOverridings.has('Text')
|
||||
? propertyOverridings.get('Text')
|
||||
: object.content.text;
|
||||
this._pixiObject.text = this._propertyOverridings.has('Text')
|
||||
? this._propertyOverridings.get('Text')
|
||||
: object.content.text;
|
||||
|
||||
const align = object.content.align;
|
||||
this._pixiObject.align = align;
|
||||
|
@@ -426,10 +426,6 @@ namespace gdjs {
|
||||
return this._renderer.getHeight();
|
||||
}
|
||||
|
||||
override setWidth(width: float): void {
|
||||
this.setWrappingWidth(width);
|
||||
}
|
||||
|
||||
override getDrawableY(): float {
|
||||
return (
|
||||
this.getY() -
|
||||
|
@@ -21,9 +21,7 @@ module.exports = {
|
||||
.setExtensionInformation(
|
||||
'FileSystem',
|
||||
_('File system'),
|
||||
_(
|
||||
'Access the filesystem of the operating system - only works on native, desktop games exported to Windows, Linux or macOS.'
|
||||
),
|
||||
_('Access the filesystem of the operating system.'),
|
||||
'Matthias Meike',
|
||||
'Open source (MIT License)'
|
||||
)
|
||||
|
@@ -5,25 +5,23 @@ Copyright (c) 2008-2016 Florian Rival (Florian.Rival@gmail.com)
|
||||
This project is released under the MIT License.
|
||||
*/
|
||||
|
||||
#include <iostream>
|
||||
|
||||
#include "GDCore/Extensions/PlatformExtension.h"
|
||||
#include "GDCore/Tools/Localization.h"
|
||||
|
||||
#include <iostream>
|
||||
|
||||
void DeclareInventoryExtension(gd::PlatformExtension& extension) {
|
||||
extension
|
||||
.SetExtensionInformation(
|
||||
"Inventory",
|
||||
_("Inventories"),
|
||||
_("Actions and conditions to store named inventories in memory, "
|
||||
"with items (indexed by their name), a count for each of them, "
|
||||
"a maximum count and an equipped state. Can be loaded/saved "
|
||||
"from/to a GDevelop variable."),
|
||||
"Florian Rival",
|
||||
"Open source (MIT License)")
|
||||
extension.SetExtensionInformation(
|
||||
"Inventory",
|
||||
_("Inventories"),
|
||||
_("Provides actions and conditions to add an inventory to your game, "
|
||||
"with items in memory."),
|
||||
"Florian Rival",
|
||||
"Open source (MIT License)")
|
||||
.SetExtensionHelpPath("/all-features/inventory")
|
||||
.SetCategory("Game mechanic");
|
||||
extension.AddInstructionOrExpressionGroupMetadata(_("Inventories"))
|
||||
extension
|
||||
.AddInstructionOrExpressionGroupMetadata(_("Inventories"))
|
||||
.SetIcon("CppPlatform/Extensions/Inventoryicon.png");
|
||||
|
||||
extension
|
||||
@@ -166,15 +164,14 @@ void DeclareInventoryExtension(gd::PlatformExtension& extension) {
|
||||
.SetFunctionName("InventoryTools::IsEquipped");
|
||||
|
||||
extension
|
||||
.AddAction(
|
||||
"SerializeToVariable",
|
||||
_("Save an inventory in a scene variable"),
|
||||
_("Save all the items of the inventory in a scene variable, so that "
|
||||
"it can be restored later."),
|
||||
_("Save inventory _PARAM1_ in variable _PARAM2_"),
|
||||
_("Variables"),
|
||||
"CppPlatform/Extensions/Inventoryicon.png",
|
||||
"CppPlatform/Extensions/Inventoryicon.png")
|
||||
.AddAction("SerializeToVariable",
|
||||
_("Save an inventory in a scene variable"),
|
||||
_("Save all the items of the inventory in a scene variable, so that "
|
||||
"it can be restored later."),
|
||||
_("Save inventory _PARAM1_ in variable _PARAM2_"),
|
||||
_("Variables"),
|
||||
"CppPlatform/Extensions/Inventoryicon.png",
|
||||
"CppPlatform/Extensions/Inventoryicon.png")
|
||||
|
||||
.AddCodeOnlyParameter("currentScene", "")
|
||||
.AddParameter("string", _("Inventory name"))
|
||||
@@ -207,14 +204,13 @@ void DeclareInventoryExtension(gd::PlatformExtension& extension) {
|
||||
.SetFunctionName("InventoryTools::Count");
|
||||
|
||||
extension
|
||||
.AddExpression("Maximum",
|
||||
_("Item maximum"),
|
||||
_("Get the maximum of an item in the inventory, or 0 if "
|
||||
"it is unlimited"),
|
||||
"",
|
||||
"CppPlatform/Extensions/Inventoryicon.png")
|
||||
.AddCodeOnlyParameter("currentScene", "")
|
||||
.AddParameter("string", _("Inventory name"))
|
||||
.AddParameter("string", _("Item name"))
|
||||
.SetFunctionName("InventoryTools::Maximum");
|
||||
.AddExpression("Maximum",
|
||||
_("Item maximum"),
|
||||
_("Get the maximum of an item in the inventory, or 0 if it is unlimited"),
|
||||
"",
|
||||
"CppPlatform/Extensions/Inventoryicon.png")
|
||||
.AddCodeOnlyParameter("currentScene", "")
|
||||
.AddParameter("string", _("Inventory name"))
|
||||
.AddParameter("string", _("Item name"))
|
||||
.SetFunctionName("InventoryTools::Maximum");
|
||||
}
|
||||
|
9
Extensions/JsExtensionTypes.d.ts
vendored
9
Extensions/JsExtensionTypes.d.ts
vendored
@@ -27,7 +27,7 @@ class RenderedInstance {
|
||||
associatedObjectConfiguration: gdObjectConfiguration,
|
||||
pixiContainer: PIXI.Container,
|
||||
pixiResourcesLoader: Class<PixiResourcesLoader>,
|
||||
getPropertyOverridings: (() => Map<string, string>) | null = null
|
||||
propertyOverridings: Map<string, string> = new Map<string, string>()
|
||||
);
|
||||
|
||||
/**
|
||||
@@ -80,8 +80,6 @@ class RenderedInstance {
|
||||
getDefaultHeight(): number;
|
||||
|
||||
getDefaultDepth(): number;
|
||||
|
||||
getPropertyOverridings(): Map<string, string> | null;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -109,8 +107,7 @@ class Rendered3DInstance {
|
||||
associatedObjectConfiguration: gdObjectConfiguration,
|
||||
pixiContainer: PIXI.Container,
|
||||
threeGroup: THREE.Group,
|
||||
pixiResourcesLoader: Class<PixiResourcesLoader>,
|
||||
getPropertyOverridings: (() => Map<string, string>) | null = null
|
||||
pixiResourcesLoader: Class<PixiResourcesLoader>
|
||||
);
|
||||
|
||||
/**
|
||||
@@ -177,8 +174,6 @@ class Rendered3DInstance {
|
||||
* Return the depth of the instance when the instance doesn't have a custom size.
|
||||
*/
|
||||
getDefaultDepth(): number;
|
||||
|
||||
getPropertyOverridings(): Map<string, string> | null;
|
||||
}
|
||||
|
||||
declare type ObjectsRenderingService = {
|
||||
|
@@ -21,9 +21,7 @@ module.exports = {
|
||||
.setExtensionInformation(
|
||||
'Leaderboards',
|
||||
_('Leaderboards'),
|
||||
_(
|
||||
'Allow your game to send scores to your leaderboards (anonymously or from the logged-in player) or display existing leaderboards to the player.'
|
||||
),
|
||||
_('Allow your game to send scores to your leaderboards.'),
|
||||
'Florian Rival',
|
||||
'Open source (MIT License)'
|
||||
)
|
||||
@@ -32,12 +30,6 @@ module.exports = {
|
||||
.addInstructionOrExpressionGroupMetadata(_('Leaderboards'))
|
||||
.setIcon('JsPlatform/Extensions/leaderboard.svg');
|
||||
|
||||
extension
|
||||
.addDependency()
|
||||
.setName('Safari View Controller Cordova plugin')
|
||||
.setDependencyType('cordova')
|
||||
.setExportName('@gdevelop/cordova-plugin-safariviewcontroller');
|
||||
|
||||
extension
|
||||
.addAction(
|
||||
'SavePlayerScore',
|
||||
|
@@ -22,7 +22,7 @@ module.exports = {
|
||||
'Lighting',
|
||||
_('Lights'),
|
||||
|
||||
'This provides a 2D light object, and a behavior to mark other 2D objects as being obstacles for the lights. This is a great way to create a special atmosphere to your game, along with effects, make it more realistic or to create gameplays based on lights.',
|
||||
'This provides a light object, and a behavior to mark other objects as being obstacles for the lights. This is a great way to create a special atmosphere to your game, along with effects, make it more realistic or to create gameplays based on lights.',
|
||||
'Harsimran Virk',
|
||||
'MIT'
|
||||
)
|
||||
@@ -51,7 +51,7 @@ module.exports = {
|
||||
_('Light Obstacle Behavior'),
|
||||
'LightObstacleBehavior',
|
||||
_(
|
||||
'Flag objects as being obstacles to 2D lights. The light emitted by light objects will be stopped by the object. This does not work on 3D objects and 3D games.'
|
||||
'Flag objects as being obstacles to light. The light emitted by light objects will be stopped by the object.'
|
||||
),
|
||||
'',
|
||||
'CppPlatform/Extensions/lightObstacleIcon32.png',
|
||||
@@ -164,7 +164,7 @@ module.exports = {
|
||||
'LightObject',
|
||||
_('Light'),
|
||||
_(
|
||||
'Displays a 2D light on the scene, with a customizable radius and color. Add then the Light Obstacle behavior to the objects that must act as obstacle to the lights.'
|
||||
'Displays a light on the scene, with a customizable radius and color. Add then the Light Obstacle behavior to the objects that must act as obstacle to the lights.'
|
||||
),
|
||||
'CppPlatform/Extensions/lightIcon32.png',
|
||||
lightObject
|
||||
|
@@ -239,7 +239,7 @@ namespace gdjs {
|
||||
instanceContainer: gdjs.RuntimeInstanceContainer,
|
||||
objectsLists: Hashtable<gdjs.RuntimeObject[]>,
|
||||
obj: gdjs.RuntimeObject | null,
|
||||
eventsFunctionContext: EventsFunctionContext | null | undefined
|
||||
eventsFunctionContext: EventsFunctionContext | undefined
|
||||
) {
|
||||
if (obj === null) {
|
||||
return false;
|
||||
|
@@ -21,9 +21,7 @@ module.exports = {
|
||||
.setExtensionInformation(
|
||||
'Multiplayer',
|
||||
_('Multiplayer'),
|
||||
_(
|
||||
'This allows players to join online lobbies and synchronize gameplay across devices without needing to manage servers or networking.\n\nUse the "Open game lobbies" action to let players join a game, and use conditions like "Lobby game has just started" to begin gameplay. Add the "Multiplayer object" behavior to game objects that should be synchronized, and assign or change their ownership using player numbers. Variables and game state (like scenes, scores, or timers) are automatically synced by the host, with options to change ownership or disable sync when needed. Common multiplayer logic —like handling joins/leaves, collisions, and host migration— is supported out-of-the-box for up to 8 players per game.'
|
||||
),
|
||||
_('Allow players to connect to lobbies and play together.'),
|
||||
'Florian Rival',
|
||||
'Open source (MIT License)'
|
||||
)
|
||||
@@ -33,79 +31,6 @@ module.exports = {
|
||||
.addInstructionOrExpressionGroupMetadata(_('Multiplayer'))
|
||||
.setIcon('JsPlatform/Extensions/multiplayer.svg');
|
||||
|
||||
extension
|
||||
.addDependency()
|
||||
.setName('Safari View Controller Cordova plugin')
|
||||
.setDependencyType('cordova')
|
||||
.setExportName('@gdevelop/cordova-plugin-safariviewcontroller');
|
||||
|
||||
extension
|
||||
.addStrExpression(
|
||||
'CurrentLobbyID',
|
||||
_('Current lobby ID'),
|
||||
_('Returns current lobby ID.'),
|
||||
_('Lobbies'),
|
||||
'JsPlatform/Extensions/multiplayer.svg'
|
||||
)
|
||||
.getCodeExtraInformation()
|
||||
.setIncludeFile('Extensions/Multiplayer/peer.js')
|
||||
.addIncludeFile('Extensions/Multiplayer/peerJsHelper.js')
|
||||
.addIncludeFile(
|
||||
'Extensions/PlayerAuthentication/playerauthenticationcomponents.js'
|
||||
)
|
||||
.addIncludeFile(
|
||||
'Extensions/PlayerAuthentication/playerauthenticationtools.js'
|
||||
)
|
||||
.addIncludeFile('Extensions/Multiplayer/multiplayercomponents.js')
|
||||
.addIncludeFile('Extensions/Multiplayer/messageManager.js')
|
||||
.addIncludeFile('Extensions/Multiplayer/multiplayerVariablesManager.js')
|
||||
.addIncludeFile('Extensions/Multiplayer/multiplayertools.js')
|
||||
.setFunctionName('gdjs.multiplayer.getLobbyID');
|
||||
|
||||
extension
|
||||
.addAction(
|
||||
'QuickJoinWithLobbyID',
|
||||
_('Join a specific lobby by its ID'),
|
||||
_(
|
||||
'Join a specific lobby. The player will join the game instantly if this is possible.'
|
||||
),
|
||||
_('Join a specific lobby by its ID _PARAM1_'),
|
||||
_('Lobbies'),
|
||||
'JsPlatform/Extensions/multiplayer.svg',
|
||||
'JsPlatform/Extensions/multiplayer.svg'
|
||||
)
|
||||
.addCodeOnlyParameter('currentScene', '')
|
||||
.addParameter('string', _('Lobby ID'), '', false)
|
||||
.addParameter(
|
||||
'yesorno',
|
||||
_('Display loader while joining a lobby.'),
|
||||
'',
|
||||
true
|
||||
)
|
||||
.setDefaultValue('yes')
|
||||
.addParameter(
|
||||
'yesorno',
|
||||
_('Display game lobbies if unable to join a specific one.'),
|
||||
'',
|
||||
true
|
||||
)
|
||||
.setDefaultValue('yes')
|
||||
.setHelpPath('/all-features/multiplayer')
|
||||
.getCodeExtraInformation()
|
||||
.setIncludeFile('Extensions/Multiplayer/peer.js')
|
||||
.addIncludeFile('Extensions/Multiplayer/peerJsHelper.js')
|
||||
.addIncludeFile(
|
||||
'Extensions/PlayerAuthentication/playerauthenticationcomponents.js'
|
||||
)
|
||||
.addIncludeFile(
|
||||
'Extensions/PlayerAuthentication/playerauthenticationtools.js'
|
||||
)
|
||||
.addIncludeFile('Extensions/Multiplayer/multiplayercomponents.js')
|
||||
.addIncludeFile('Extensions/Multiplayer/messageManager.js')
|
||||
.addIncludeFile('Extensions/Multiplayer/multiplayerVariablesManager.js')
|
||||
.addIncludeFile('Extensions/Multiplayer/multiplayertools.js')
|
||||
.setFunctionName('gdjs.multiplayer.authenticateAndQuickJoinWithLobbyID');
|
||||
|
||||
extension
|
||||
.addAction(
|
||||
'QuickJoinLobby',
|
||||
|
@@ -293,8 +293,6 @@ namespace gdjs {
|
||||
x: objectNetworkSyncData.x,
|
||||
y: objectNetworkSyncData.y,
|
||||
z: objectNetworkSyncData.z,
|
||||
w: objectNetworkSyncData.w,
|
||||
h: objectNetworkSyncData.h,
|
||||
zo: objectNetworkSyncData.zo,
|
||||
a: objectNetworkSyncData.a,
|
||||
hid: objectNetworkSyncData.hid,
|
||||
@@ -371,9 +369,6 @@ namespace gdjs {
|
||||
this._lastSentBasicObjectSyncData = {
|
||||
x: objectNetworkSyncData.x,
|
||||
y: objectNetworkSyncData.y,
|
||||
z: objectNetworkSyncData.z,
|
||||
w: objectNetworkSyncData.w,
|
||||
h: objectNetworkSyncData.h,
|
||||
zo: objectNetworkSyncData.zo,
|
||||
a: objectNetworkSyncData.a,
|
||||
hid: objectNetworkSyncData.hid,
|
||||
|
@@ -17,29 +17,9 @@ namespace gdjs {
|
||||
}[];
|
||||
};
|
||||
|
||||
type LobbyStatus =
|
||||
| 'waiting'
|
||||
| 'starting'
|
||||
| 'playing'
|
||||
| 'migrating'
|
||||
| 'migrated';
|
||||
type LobbyConnectionStatus = 'waiting' | 'ready' | 'connected';
|
||||
type InGamePlayerStatus = 'playing' | 'left';
|
||||
type PlayerStatus = LobbyConnectionStatus | InGamePlayerStatus;
|
||||
|
||||
type LobbyPlayer = {
|
||||
playerId: string;
|
||||
status: PlayerStatus;
|
||||
playerNumber: number;
|
||||
};
|
||||
|
||||
type Lobby = {
|
||||
id: string;
|
||||
minPlayers: number;
|
||||
maxPlayers: number;
|
||||
canJoinAfterStart: boolean;
|
||||
players: LobbyPlayer[];
|
||||
status: LobbyStatus;
|
||||
status: 'waiting' | 'starting' | 'playing' | 'migrating' | 'migrated';
|
||||
};
|
||||
|
||||
type QuickJoinLobbyResponse =
|
||||
@@ -125,7 +105,6 @@ namespace gdjs {
|
||||
let _quickJoinLobbyFailureReason:
|
||||
| 'FULL'
|
||||
| 'NOT_ENOUGH_PLAYERS'
|
||||
| 'DOES_NOT_EXIST'
|
||||
| 'UNKNOWN'
|
||||
| null = null;
|
||||
let _lobbyId: string | null = null;
|
||||
@@ -1718,87 +1697,11 @@ namespace gdjs {
|
||||
}
|
||||
};
|
||||
|
||||
export const getLobbyID = (): string => {
|
||||
return _lobbyId || '';
|
||||
};
|
||||
|
||||
const quickJoinWithLobbyID = async (
|
||||
export const authenticateAndQuickJoinLobby = async (
|
||||
runtimeScene: gdjs.RuntimeScene,
|
||||
lobbyID: string,
|
||||
displayLoader: boolean,
|
||||
openLobbiesPageIfFailure: boolean
|
||||
) => {
|
||||
if (_isQuickJoiningOrStartingAGame) return;
|
||||
const _gameId = gdjs.projectData.properties.projectUuid;
|
||||
if (!_gameId) {
|
||||
logger.error(
|
||||
'The game ID is missing, the quick join lobby action cannot continue.'
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
_quickJoinLobbyFailureReason = null;
|
||||
_isQuickJoiningOrStartingAGame = true;
|
||||
if (displayLoader) {
|
||||
gdjs.multiplayerComponents.displayLoader(runtimeScene, true);
|
||||
}
|
||||
|
||||
const quickJoinWithLobbyIDRelativeUrl = `/play/game/${_gameId}/public-lobby/${lobbyID}`;
|
||||
|
||||
try {
|
||||
const lobby: Lobby = await gdjs.evtTools.network.retryIfFailed(
|
||||
{ times: 2 },
|
||||
() =>
|
||||
fetchAsPlayer({
|
||||
relativeUrl: quickJoinWithLobbyIDRelativeUrl,
|
||||
method: 'GET',
|
||||
dev: isUsingGDevelopDevelopmentEnvironment,
|
||||
})
|
||||
);
|
||||
|
||||
const isFull = lobby.players.length === lobby.maxPlayers;
|
||||
if (isFull) {
|
||||
logger.error('Lobby is full - cannot quick join it.');
|
||||
_quickJoinLobbyJustFailed = true;
|
||||
_quickJoinLobbyFailureReason = 'FULL';
|
||||
onLobbyQuickJoinFinished(runtimeScene);
|
||||
if (openLobbiesPageIfFailure) {
|
||||
openLobbiesWindow(runtimeScene);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (lobby.status === 'playing') {
|
||||
_actionAfterJoiningLobby = 'JOIN_GAME';
|
||||
} else if (lobby.status === 'waiting') {
|
||||
if (lobby.players.length === 0) {
|
||||
_actionAfterJoiningLobby = 'START_GAME';
|
||||
} else {
|
||||
_actionAfterJoiningLobby = 'OPEN_LOBBY_PAGE';
|
||||
}
|
||||
} else {
|
||||
throw new Error(`Lobby in wrong status: ${lobby.status}`);
|
||||
}
|
||||
|
||||
handleJoinLobbyEvent(runtimeScene, lobbyID);
|
||||
} catch (error) {
|
||||
const errorCode = parseInt(error.message.match(/\d{3}/)?.[0]);
|
||||
if (errorCode === 404) {
|
||||
logger.error('Lobby does not exist.');
|
||||
_quickJoinLobbyFailureReason = 'DOES_NOT_EXIST';
|
||||
} else {
|
||||
logger.error('An error occurred while joining a lobby:', error);
|
||||
_quickJoinLobbyFailureReason = 'UNKNOWN';
|
||||
}
|
||||
_quickJoinLobbyJustFailed = true;
|
||||
onLobbyQuickJoinFinished(runtimeScene);
|
||||
if (openLobbiesPageIfFailure) {
|
||||
openLobbiesWindow(runtimeScene);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const isQuickJoiningTooFast = () => {
|
||||
const requestDoneAt = Date.now();
|
||||
if (_lastQuickJoinRequestDoneAt) {
|
||||
if (requestDoneAt - _lastQuickJoinRequestDoneAt < 500) {
|
||||
@@ -1806,18 +1709,12 @@ namespace gdjs {
|
||||
logger.warn(
|
||||
'Last request to quick join a lobby was sent too little time ago. Ignoring this one.'
|
||||
);
|
||||
return true;
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
_lastQuickJoinRequestDoneAt = requestDoneAt;
|
||||
}
|
||||
|
||||
return false;
|
||||
};
|
||||
|
||||
const isNotCorrectlyAuthenticatedForQuickJoin = async (
|
||||
runtimeScene: RuntimeScene
|
||||
) => {
|
||||
const playerId = gdjs.playerAuthentication.getUserId();
|
||||
const playerToken = gdjs.playerAuthentication.getUserToken();
|
||||
if (!playerId || !playerToken) {
|
||||
@@ -1827,43 +1724,14 @@ namespace gdjs {
|
||||
.promise;
|
||||
_isWaitingForLogin = false;
|
||||
|
||||
if (status !== 'logged') {
|
||||
return true;
|
||||
if (status === 'logged') {
|
||||
await quickJoinLobby(
|
||||
runtimeScene,
|
||||
displayLoader,
|
||||
openLobbiesPageIfFailure
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
};
|
||||
|
||||
export const authenticateAndQuickJoinWithLobbyID = async (
|
||||
runtimeScene: gdjs.RuntimeScene,
|
||||
lobbyID: string,
|
||||
displayLoader: boolean,
|
||||
openLobbiesPageIfFailure: boolean
|
||||
) => {
|
||||
if (isQuickJoiningTooFast()) {
|
||||
return;
|
||||
}
|
||||
if (await isNotCorrectlyAuthenticatedForQuickJoin(runtimeScene)) {
|
||||
return;
|
||||
}
|
||||
await quickJoinWithLobbyID(
|
||||
runtimeScene,
|
||||
lobbyID,
|
||||
displayLoader,
|
||||
openLobbiesPageIfFailure
|
||||
);
|
||||
};
|
||||
|
||||
export const authenticateAndQuickJoinLobby = async (
|
||||
runtimeScene: gdjs.RuntimeScene,
|
||||
displayLoader: boolean,
|
||||
openLobbiesPageIfFailure: boolean
|
||||
) => {
|
||||
if (isQuickJoiningTooFast()) {
|
||||
return;
|
||||
}
|
||||
if (await isNotCorrectlyAuthenticatedForQuickJoin(runtimeScene)) {
|
||||
return;
|
||||
}
|
||||
await quickJoinLobby(
|
||||
|
@@ -25,6 +25,8 @@ namespace gdjs {
|
||||
export type PanelSpriteObjectData = ObjectData & PanelSpriteObjectDataType;
|
||||
|
||||
export type PanelSpriteNetworkSyncDataType = {
|
||||
wid: number;
|
||||
hei: number;
|
||||
op: number;
|
||||
color: string;
|
||||
};
|
||||
@@ -122,6 +124,8 @@ namespace gdjs {
|
||||
getNetworkSyncData(): PanelSpriteNetworkSyncData {
|
||||
return {
|
||||
...super.getNetworkSyncData(),
|
||||
wid: this.getWidth(),
|
||||
hei: this.getHeight(),
|
||||
op: this.getOpacity(),
|
||||
color: this.getColor(),
|
||||
};
|
||||
@@ -134,6 +138,12 @@ namespace gdjs {
|
||||
|
||||
// Texture is not synchronized, see if this is asked or not.
|
||||
|
||||
if (networkSyncData.wid !== undefined) {
|
||||
this.setWidth(networkSyncData.wid);
|
||||
}
|
||||
if (networkSyncData.hei !== undefined) {
|
||||
this.setHeight(networkSyncData.hei);
|
||||
}
|
||||
if (networkSyncData.op !== undefined) {
|
||||
this.setOpacity(networkSyncData.op);
|
||||
}
|
||||
|
@@ -927,54 +927,6 @@ module.exports = {
|
||||
.setFunctionName('setDensity')
|
||||
.setGetter('getDensity');
|
||||
|
||||
aut
|
||||
.addExpressionAndConditionAndAction(
|
||||
'number',
|
||||
'ShapeOffsetX',
|
||||
_('Shape offset X'),
|
||||
_('the object shape offset on X.'),
|
||||
_('the shape offset on X'),
|
||||
_('Body settings'),
|
||||
'JsPlatform/Extensions/physics3d.svg'
|
||||
)
|
||||
.addParameter('object', _('Object'), '', false)
|
||||
.addParameter('behavior', _('Behavior'), 'Physics3DBehavior')
|
||||
.useStandardParameters('number', gd.ParameterOptions.makeNewOptions())
|
||||
.setFunctionName('setShapeOffsetX')
|
||||
.setGetter('getShapeOffsetX');
|
||||
|
||||
aut
|
||||
.addExpressionAndConditionAndAction(
|
||||
'number',
|
||||
'ShapeOffsetY',
|
||||
_('Shape offset Y'),
|
||||
_('the object shape offset on Y.'),
|
||||
_('the shape offset on Y'),
|
||||
_('Body settings'),
|
||||
'JsPlatform/Extensions/physics3d.svg'
|
||||
)
|
||||
.addParameter('object', _('Object'), '', false)
|
||||
.addParameter('behavior', _('Behavior'), 'Physics3DBehavior')
|
||||
.useStandardParameters('number', gd.ParameterOptions.makeNewOptions())
|
||||
.setFunctionName('setShapeOffsetY')
|
||||
.setGetter('getShapeOffsetY');
|
||||
|
||||
aut
|
||||
.addExpressionAndConditionAndAction(
|
||||
'number',
|
||||
'ShapeOffsetZ',
|
||||
_('Shape offset Z'),
|
||||
_('the object shape offset on Z.'),
|
||||
_('the shape offset on Z'),
|
||||
_('Body settings'),
|
||||
'JsPlatform/Extensions/physics3d.svg'
|
||||
)
|
||||
.addParameter('object', _('Object'), '', false)
|
||||
.addParameter('behavior', _('Behavior'), 'Physics3DBehavior')
|
||||
.useStandardParameters('number', gd.ParameterOptions.makeNewOptions())
|
||||
.setFunctionName('setShapeOffsetZ')
|
||||
.setGetter('getShapeOffsetZ');
|
||||
|
||||
aut
|
||||
.addExpressionAndConditionAndAction(
|
||||
'number',
|
||||
|
@@ -630,34 +630,24 @@ namespace gdjs {
|
||||
|
||||
override onDeActivate() {
|
||||
this._sharedData.removeFromBehaviorsList(this);
|
||||
this._destroyBody();
|
||||
}
|
||||
|
||||
override onActivate() {
|
||||
this._sharedData.addToBehaviorsList(this);
|
||||
}
|
||||
|
||||
override onDestroy() {
|
||||
this._destroyedDuringFrameLogic = true;
|
||||
this.onDeActivate();
|
||||
}
|
||||
|
||||
_destroyBody() {
|
||||
this.bodyUpdater.destroyBody();
|
||||
this._contactsEndedThisFrame.length = 0;
|
||||
this._contactsStartedThisFrame.length = 0;
|
||||
this._currentContacts.length = 0;
|
||||
}
|
||||
|
||||
resetToDefaultBodyUpdater() {
|
||||
this.bodyUpdater = new gdjs.Physics3DRuntimeBehavior.DefaultBodyUpdater(
|
||||
this
|
||||
);
|
||||
override onActivate() {
|
||||
this._sharedData.addToBehaviorsList(this);
|
||||
|
||||
this._contactsEndedThisFrame.length = 0;
|
||||
this._contactsStartedThisFrame.length = 0;
|
||||
this._currentContacts.length = 0;
|
||||
this.updateBodyFromObject();
|
||||
}
|
||||
|
||||
resetToDefaultCollisionChecker() {
|
||||
this.collisionChecker =
|
||||
new gdjs.Physics3DRuntimeBehavior.DefaultCollisionChecker(this);
|
||||
override onDestroy() {
|
||||
this._destroyedDuringFrameLogic = true;
|
||||
this.onDeActivate();
|
||||
}
|
||||
|
||||
createShape(): Jolt.Shape {
|
||||
@@ -1186,33 +1176,6 @@ namespace gdjs {
|
||||
this._needToRecreateBody = true;
|
||||
}
|
||||
|
||||
getShapeOffsetX(): float {
|
||||
return this.shapeOffsetX;
|
||||
}
|
||||
|
||||
setShapeOffsetX(shapeOffsetX: float): void {
|
||||
this.shapeOffsetX = shapeOffsetX;
|
||||
this._needToRecreateShape = true;
|
||||
}
|
||||
|
||||
getShapeOffsetY(): float {
|
||||
return this.shapeOffsetY;
|
||||
}
|
||||
|
||||
setShapeOffsetY(shapeOffsetY: float): void {
|
||||
this.shapeOffsetY = shapeOffsetY;
|
||||
this._needToRecreateShape = true;
|
||||
}
|
||||
|
||||
getShapeOffsetZ(): float {
|
||||
return this.shapeOffsetZ;
|
||||
}
|
||||
|
||||
setShapeOffsetZ(shapeOffsetZ: float): void {
|
||||
this.shapeOffsetZ = shapeOffsetZ;
|
||||
this._needToRecreateShape = true;
|
||||
}
|
||||
|
||||
getFriction(): float {
|
||||
return this.friction;
|
||||
}
|
||||
@@ -1577,9 +1540,9 @@ namespace gdjs {
|
||||
}
|
||||
const body = this._body!;
|
||||
|
||||
const deltaX = towardX - this.owner3D.getX();
|
||||
const deltaY = towardY - this.owner3D.getY();
|
||||
const deltaZ = towardZ - this.owner3D.getZ();
|
||||
const deltaX = towardX - body.GetPosition().GetX();
|
||||
const deltaY = towardY - body.GetPosition().GetY();
|
||||
const deltaZ = towardZ - body.GetPosition().GetZ();
|
||||
const distanceSq = deltaX * deltaX + deltaY * deltaY + deltaZ * deltaZ;
|
||||
if (distanceSq === 0) {
|
||||
return;
|
||||
@@ -1637,16 +1600,19 @@ namespace gdjs {
|
||||
length: float,
|
||||
towardX: float,
|
||||
towardY: float,
|
||||
towardZ: float
|
||||
towardZ: float,
|
||||
originX: float,
|
||||
originY: float,
|
||||
originZ: float
|
||||
): void {
|
||||
if (this._body === null) {
|
||||
if (!this._createBody()) return;
|
||||
}
|
||||
const body = this._body!;
|
||||
|
||||
const deltaX = towardX - this.owner3D.getX();
|
||||
const deltaY = towardY - this.owner3D.getY();
|
||||
const deltaZ = towardZ - this.owner3D.getZ();
|
||||
const deltaX = towardX - originX;
|
||||
const deltaY = towardY - originY;
|
||||
const deltaZ = towardZ - originZ;
|
||||
const distanceSq = deltaX * deltaX + deltaY * deltaY + deltaZ * deltaZ;
|
||||
if (distanceSq === 0) {
|
||||
return;
|
||||
@@ -1655,7 +1621,12 @@ namespace gdjs {
|
||||
|
||||
this._sharedData.bodyInterface.AddImpulse(
|
||||
body.GetID(),
|
||||
this.getVec3(deltaX * ratio, deltaY * ratio, deltaZ * ratio)
|
||||
this.getVec3(deltaX * ratio, deltaY * ratio, deltaZ * ratio),
|
||||
this.getRVec3(
|
||||
originX * this._sharedData.worldInvScale,
|
||||
originY * this._sharedData.worldInvScale,
|
||||
originZ * this._sharedData.worldInvScale
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
|
@@ -29,7 +29,6 @@ namespace gdjs {
|
||||
owner3D: gdjs.RuntimeObject3D;
|
||||
private _physics3DBehaviorName: string;
|
||||
private _physics3D: Physics3D | null = null;
|
||||
private _isHookedToPhysicsStep = false;
|
||||
_vehicleController: Jolt.WheeledVehicleController | null = null;
|
||||
_stepListener: Jolt.VehicleConstraintStepListener | null = null;
|
||||
_vehicleCollisionTester: Jolt.VehicleCollisionTesterCastCylinder | null =
|
||||
@@ -154,19 +153,13 @@ namespace gdjs {
|
||||
const behavior = this.owner.getBehavior(
|
||||
this._physics3DBehaviorName
|
||||
) as gdjs.Physics3DRuntimeBehavior;
|
||||
if (!behavior.activated()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const sharedData = behavior._sharedData;
|
||||
|
||||
this._physics3D = {
|
||||
behavior,
|
||||
};
|
||||
if (!this._isHookedToPhysicsStep) {
|
||||
sharedData.registerHook(this);
|
||||
this._isHookedToPhysicsStep = true;
|
||||
}
|
||||
sharedData.registerHook(this);
|
||||
|
||||
behavior.bodyUpdater =
|
||||
new gdjs.PhysicsCar3DRuntimeBehavior.VehicleBodyUpdater(
|
||||
@@ -337,33 +330,25 @@ namespace gdjs {
|
||||
}
|
||||
|
||||
override onDeActivate() {
|
||||
if (!this._physics3D) {
|
||||
return;
|
||||
if (this._stepListener) {
|
||||
this._sharedData.physicsSystem.RemoveStepListener(this._stepListener);
|
||||
}
|
||||
this._destroyBody();
|
||||
}
|
||||
|
||||
override onActivate() {
|
||||
const behavior = this.owner.getBehavior(
|
||||
this._physics3DBehaviorName
|
||||
) as gdjs.Physics3DRuntimeBehavior;
|
||||
if (!behavior) {
|
||||
return;
|
||||
if (this._stepListener) {
|
||||
this._sharedData.physicsSystem.AddStepListener(this._stepListener);
|
||||
}
|
||||
behavior._destroyBody();
|
||||
}
|
||||
|
||||
override onDestroy() {
|
||||
this._destroyedDuringFrameLogic = true;
|
||||
this._destroyBody();
|
||||
}
|
||||
|
||||
_destroyBody() {
|
||||
if (!this._vehicleController) {
|
||||
return;
|
||||
}
|
||||
this._destroyedDuringFrameLogic = true;
|
||||
this.onDeActivate();
|
||||
if (this._stepListener) {
|
||||
this._sharedData.physicsSystem.RemoveStepListener(this._stepListener);
|
||||
// stepListener is removed by onDeActivate
|
||||
Jolt.destroy(this._stepListener);
|
||||
this._stepListener = null;
|
||||
}
|
||||
@@ -375,8 +360,6 @@ namespace gdjs {
|
||||
// It is destroyed with the constraint.
|
||||
this._vehicleCollisionTester = null;
|
||||
if (this._physics3D) {
|
||||
const { behavior } = this._physics3D;
|
||||
behavior.resetToDefaultBodyUpdater();
|
||||
this._physics3D = null;
|
||||
}
|
||||
}
|
||||
@@ -1127,7 +1110,7 @@ namespace gdjs {
|
||||
}
|
||||
|
||||
destroyBody() {
|
||||
this.carBehavior._destroyBody();
|
||||
this.carBehavior.onDestroy();
|
||||
this.physicsBodyUpdater.destroyBody();
|
||||
}
|
||||
}
|
||||
|
@@ -41,7 +41,6 @@ namespace gdjs {
|
||||
owner3D: gdjs.RuntimeObject3D;
|
||||
private _physics3DBehaviorName: string;
|
||||
private _physics3D: Physics3D | null = null;
|
||||
private _isHookedToPhysicsStep = false;
|
||||
character: Jolt.CharacterVirtual | null = null;
|
||||
/**
|
||||
* sharedData is a reference to the shared data of the scene, that registers
|
||||
@@ -170,15 +169,10 @@ namespace gdjs {
|
||||
if (this._physics3D) {
|
||||
return this._physics3D;
|
||||
}
|
||||
if (!this.activated()) {
|
||||
return null;
|
||||
}
|
||||
const behavior = this.owner.getBehavior(
|
||||
this._physics3DBehaviorName
|
||||
) as gdjs.Physics3DRuntimeBehavior;
|
||||
if (!behavior.activated()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const sharedData = behavior._sharedData;
|
||||
const jolt = sharedData.jolt;
|
||||
const extendedUpdateSettings = new Jolt.ExtendedUpdateSettings();
|
||||
@@ -202,10 +196,7 @@ namespace gdjs {
|
||||
shapeFilter,
|
||||
};
|
||||
this.setStairHeightMax(this._stairHeightMax);
|
||||
if (!this._isHookedToPhysicsStep) {
|
||||
sharedData.registerHook(this);
|
||||
this._isHookedToPhysicsStep = true;
|
||||
}
|
||||
sharedData.registerHook(this);
|
||||
|
||||
behavior.bodyUpdater =
|
||||
new gdjs.PhysicsCharacter3DRuntimeBehavior.CharacterBodyUpdater(this);
|
||||
@@ -399,48 +390,36 @@ namespace gdjs {
|
||||
}
|
||||
|
||||
override onDeActivate() {
|
||||
if (!this._physics3D) {
|
||||
return;
|
||||
}
|
||||
this._destroyBody();
|
||||
this.collisionChecker.clearContacts();
|
||||
}
|
||||
|
||||
override onActivate() {
|
||||
const behavior = this.owner.getBehavior(
|
||||
this._physics3DBehaviorName
|
||||
) as gdjs.Physics3DRuntimeBehavior;
|
||||
if (!behavior) {
|
||||
return;
|
||||
}
|
||||
behavior._destroyBody();
|
||||
}
|
||||
override onActivate() {}
|
||||
|
||||
override onDestroy() {
|
||||
this._destroyedDuringFrameLogic = true;
|
||||
this.onDeActivate();
|
||||
this._destroyCharacter();
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove the character and its body from the physics engine.
|
||||
* This method is called when:
|
||||
* - The Physics3D behavior is deactivated
|
||||
* - This behavior is deactivated
|
||||
* - The object is destroyed
|
||||
*
|
||||
* Only deactivating the character behavior won't destroy the character.
|
||||
* Indeed, deactivated characters don't move as characters but still have collisions.
|
||||
*/
|
||||
_destroyBody() {
|
||||
_destroyCharacter() {
|
||||
if (this.character) {
|
||||
if (this._canBePushed) {
|
||||
this.charactersManager.removeCharacter(this.character);
|
||||
Jolt.destroy(this.character.GetListener());
|
||||
}
|
||||
this.collisionChecker.clearContacts();
|
||||
// The body is destroyed with the character.
|
||||
Jolt.destroy(this.character);
|
||||
this.character = null;
|
||||
if (this._physics3D) {
|
||||
const { behavior } = this._physics3D;
|
||||
behavior.resetToDefaultBodyUpdater();
|
||||
behavior.resetToDefaultCollisionChecker();
|
||||
this._physics3D.behavior._body = null;
|
||||
const {
|
||||
extendedUpdateSettings,
|
||||
@@ -1801,7 +1780,7 @@ namespace gdjs {
|
||||
}
|
||||
|
||||
destroyBody() {
|
||||
this.characterBehavior._destroyBody();
|
||||
this.characterBehavior._destroyCharacter();
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -35,7 +35,7 @@ module.exports = {
|
||||
.addDependency()
|
||||
.setName('Safari View Controller Cordova plugin')
|
||||
.setDependencyType('cordova')
|
||||
.setExportName('@gdevelop/cordova-plugin-safariviewcontroller');
|
||||
.setExportName('cordova-plugin-safariviewcontroller');
|
||||
|
||||
extension
|
||||
.addAction(
|
||||
|
@@ -834,51 +834,34 @@ namespace gdjs {
|
||||
authWindowOptions,
|
||||
});
|
||||
|
||||
if (typeof SafariViewController === 'undefined') {
|
||||
logger.error(
|
||||
'Cordova plugin SafariViewController is not installed.'
|
||||
);
|
||||
resolve('errored');
|
||||
return;
|
||||
}
|
||||
|
||||
SafariViewController.isAvailable(function (available: boolean) {
|
||||
if (!available) {
|
||||
logger.error(
|
||||
'Cordova plugin SafariViewController is installed but not available'
|
||||
);
|
||||
resolve('errored');
|
||||
return;
|
||||
}
|
||||
|
||||
logger.info(
|
||||
'Opening authentication window for Cordova with SafariViewController.'
|
||||
);
|
||||
SafariViewController.show(
|
||||
{
|
||||
url: targetUrl,
|
||||
hidden: false,
|
||||
animated: true,
|
||||
transition: 'slide',
|
||||
enterReaderModeIfAvailable: false,
|
||||
barColor: '#000000',
|
||||
tintColor: '#ffffff',
|
||||
controlTintColor: '#ffffff',
|
||||
},
|
||||
function (result: any) {
|
||||
// Other events are `opened` and `loaded`.
|
||||
if (result.event === 'closed') {
|
||||
resolve('dismissed');
|
||||
if (available) {
|
||||
SafariViewController.show(
|
||||
{
|
||||
url: targetUrl,
|
||||
hidden: false,
|
||||
animated: true,
|
||||
transition: 'slide',
|
||||
enterReaderModeIfAvailable: false,
|
||||
barColor: '#000000',
|
||||
tintColor: '#ffffff',
|
||||
controlTintColor: '#ffffff',
|
||||
},
|
||||
function (result: any) {
|
||||
// Other events are `opened` and `loaded`.
|
||||
if (result.event === 'closed') {
|
||||
resolve('dismissed');
|
||||
}
|
||||
},
|
||||
function (error: any) {
|
||||
logger.log('Error opening webview: ' + JSON.stringify(error));
|
||||
resolve('errored');
|
||||
}
|
||||
},
|
||||
function (error: any) {
|
||||
logger.log(
|
||||
'Error opening authentication window: ' +
|
||||
JSON.stringify(error)
|
||||
);
|
||||
resolve('errored');
|
||||
}
|
||||
);
|
||||
);
|
||||
} else {
|
||||
logger.error('Plugin SafariViewController is not available');
|
||||
resolve('errored');
|
||||
}
|
||||
});
|
||||
}
|
||||
);
|
||||
|
@@ -15,7 +15,7 @@ void DeclarePrimitiveDrawingExtension(gd::PlatformExtension& extension) {
|
||||
.SetExtensionInformation(
|
||||
"PrimitiveDrawing",
|
||||
_("Shape painter"),
|
||||
_("An object that can be used to draw arbitrary 2D shapes "
|
||||
_("This provides an object that can be used to draw arbitrary shapes "
|
||||
"on the screen using events."),
|
||||
"Florian Rival and Aurélien Vivet",
|
||||
"Open source (MIT License)")
|
||||
@@ -28,7 +28,7 @@ void DeclarePrimitiveDrawingExtension(gd::PlatformExtension& extension) {
|
||||
.AddObject<ShapePainterObject>(
|
||||
"Drawer", //"Drawer" is kept for compatibility with GD<=3.6.76
|
||||
_("Shape painter"),
|
||||
_("Allows to draw simple 2D shapes on the screen using the "
|
||||
_("Allows you to draw simple shapes on the screen using the "
|
||||
"events."),
|
||||
"CppPlatform/Extensions/primitivedrawingicon.png")
|
||||
.SetCategoryFullName(_("Advanced"))
|
||||
@@ -125,11 +125,11 @@ void DeclarePrimitiveDrawingExtension(gd::PlatformExtension& extension) {
|
||||
.SetFunctionName("DrawEllipse");
|
||||
|
||||
obj.AddAction("FilletRectangle",
|
||||
_("Fillet Rectangle"),
|
||||
_("Draw a fillet rectangle on screen"),
|
||||
_("Draw from _PARAM1_;_PARAM2_ to _PARAM3_;_PARAM4_ a fillet "
|
||||
"rectangle (fillet: _PARAM5_)"
|
||||
"with _PARAM0_"),
|
||||
_("Fillet Rectangle"),
|
||||
_("Draw a fillet rectangle on screen"),
|
||||
_("Draw from _PARAM1_;_PARAM2_ to _PARAM3_;_PARAM4_ a fillet "
|
||||
"rectangle (fillet: _PARAM5_)"
|
||||
"with _PARAM0_"),
|
||||
_("Drawing"),
|
||||
"res/actions/filletRectangle24.png",
|
||||
"res/actions/filletRectangle.png")
|
||||
@@ -142,6 +142,7 @@ void DeclarePrimitiveDrawingExtension(gd::PlatformExtension& extension) {
|
||||
.AddParameter("expression", _("Fillet (in pixels)"))
|
||||
.SetFunctionName("DrawFilletRectangle");
|
||||
|
||||
|
||||
obj.AddAction("RoundedRectangle",
|
||||
_("Rounded rectangle"),
|
||||
_("Draw a rounded rectangle on screen"),
|
||||
@@ -169,53 +170,54 @@ void DeclarePrimitiveDrawingExtension(gd::PlatformExtension& extension) {
|
||||
_("Drawing"),
|
||||
"res/actions/chamferRectangle24.png",
|
||||
"res/actions/chamferRectangle.png")
|
||||
.AddParameter("object", _("Shape Painter object"), "Drawer")
|
||||
.AddParameter("expression", _("Left X position"))
|
||||
.AddParameter("expression", _("Top Y position"))
|
||||
.AddParameter("expression", _("Right X position"))
|
||||
.AddParameter("expression", _("Bottom Y position"))
|
||||
.AddParameter("expression", _("Chamfer (in pixels)"))
|
||||
.SetFunctionName("DrawChamferRectangle");
|
||||
.AddParameter("object", _("Shape Painter object"), "Drawer")
|
||||
.AddParameter("expression", _("Left X position"))
|
||||
.AddParameter("expression", _("Top Y position"))
|
||||
.AddParameter("expression", _("Right X position"))
|
||||
.AddParameter("expression", _("Bottom Y position"))
|
||||
.AddParameter("expression", _("Chamfer (in pixels)"))
|
||||
.SetFunctionName("DrawChamferRectangle");
|
||||
|
||||
|
||||
obj.AddAction("Torus",
|
||||
_("Torus"),
|
||||
_("Draw a torus on screen"),
|
||||
_("Draw at _PARAM1_;_PARAM2_ a torus with "
|
||||
"inner radius: _PARAM3_, outer radius: _PARAM4_ and "
|
||||
"with start arc angle: _PARAM5_°, end angle: _PARAM6_° "
|
||||
"with _PARAM0_"),
|
||||
_("Drawing"),
|
||||
"res/actions/torus24.png",
|
||||
"res/actions/torus.png")
|
||||
|
||||
.AddParameter("object", _("Shape Painter object"), "Drawer")
|
||||
.AddParameter("expression", _("X position of center"))
|
||||
.AddParameter("expression", _("Y position of center"))
|
||||
.AddParameter("expression", _("Inner Radius (in pixels)"))
|
||||
.AddParameter("expression", _("Outer Radius (in pixels)"))
|
||||
.AddParameter("expression", _("Start Arc (in degrees)"))
|
||||
.AddParameter("expression", _("End Arc (in degrees)"))
|
||||
.SetFunctionName("DrawTorus");
|
||||
|
||||
_("Torus"),
|
||||
_("Draw a torus on screen"),
|
||||
_("Draw at _PARAM1_;_PARAM2_ a torus with "
|
||||
"inner radius: _PARAM3_, outer radius: _PARAM4_ and "
|
||||
"with start arc angle: _PARAM5_°, end angle: _PARAM6_° "
|
||||
"with _PARAM0_"),
|
||||
_("Drawing"),
|
||||
"res/actions/torus24.png",
|
||||
"res/actions/torus.png")
|
||||
|
||||
.AddParameter("object", _("Shape Painter object"), "Drawer")
|
||||
.AddParameter("expression", _("X position of center"))
|
||||
.AddParameter("expression", _("Y position of center"))
|
||||
.AddParameter("expression", _("Inner Radius (in pixels)"))
|
||||
.AddParameter("expression", _("Outer Radius (in pixels)"))
|
||||
.AddParameter("expression", _("Start Arc (in degrees)"))
|
||||
.AddParameter("expression", _("End Arc (in degrees)"))
|
||||
.SetFunctionName("DrawTorus");
|
||||
|
||||
|
||||
obj.AddAction("RegularPolygon",
|
||||
_("Regular Polygon"),
|
||||
_("Draw a regular polygon on screen"),
|
||||
_("Draw at _PARAM1_;_PARAM2_ a regular polygon with _PARAM3_ "
|
||||
"sides and radius: "
|
||||
_("Draw at _PARAM1_;_PARAM2_ a regular polygon with _PARAM3_ sides and radius: "
|
||||
"_PARAM4_ (rotation: _PARAM5_) "
|
||||
"with _PARAM0_"),
|
||||
_("Drawing"),
|
||||
"res/actions/regularPolygon24.png",
|
||||
"res/actions/regularPolygon.png")
|
||||
_("Drawing"),
|
||||
"res/actions/regularPolygon24.png",
|
||||
"res/actions/regularPolygon.png")
|
||||
|
||||
.AddParameter("object", _("Shape Painter object"), "Drawer")
|
||||
.AddParameter("expression", _("X position of center"))
|
||||
.AddParameter("expression", _("Y position of center"))
|
||||
.AddParameter("expression",
|
||||
_("Number of sides of the polygon (minimum: 3)"))
|
||||
.AddParameter("expression", _("Radius (in pixels)"))
|
||||
.AddParameter("expression", _("Rotation (in degrees)"))
|
||||
.SetFunctionName("DrawRegularPolygon");
|
||||
.AddParameter("object", _("Shape Painter object"), "Drawer")
|
||||
.AddParameter("expression", _("X position of center"))
|
||||
.AddParameter("expression", _("Y position of center"))
|
||||
.AddParameter("expression",
|
||||
_("Number of sides of the polygon (minimum: 3)"))
|
||||
.AddParameter("expression", _("Radius (in pixels)"))
|
||||
.AddParameter("expression", _("Rotation (in degrees)"))
|
||||
.SetFunctionName("DrawRegularPolygon");
|
||||
|
||||
obj.AddAction(
|
||||
"Star",
|
||||
|
@@ -14,6 +14,8 @@ namespace gdjs {
|
||||
|
||||
export type SpineNetworkSyncDataType = {
|
||||
opa: float;
|
||||
wid: float;
|
||||
hei: float;
|
||||
scaX: float;
|
||||
scaY: float;
|
||||
flipX: boolean;
|
||||
@@ -115,6 +117,8 @@ namespace gdjs {
|
||||
return {
|
||||
...super.getNetworkSyncData(),
|
||||
opa: this._opacity,
|
||||
wid: this.getWidth(),
|
||||
hei: this.getHeight(),
|
||||
scaX: this.getScaleX(),
|
||||
scaY: this.getScaleY(),
|
||||
flipX: this.isFlippedX(),
|
||||
@@ -133,6 +137,12 @@ namespace gdjs {
|
||||
if (syncData.opa !== undefined && syncData.opa !== this._opacity) {
|
||||
this.setOpacity(syncData.opa);
|
||||
}
|
||||
if (syncData.wid !== undefined && syncData.wid !== this.getWidth()) {
|
||||
this.setWidth(syncData.wid);
|
||||
}
|
||||
if (syncData.hei !== undefined && syncData.hei !== this.getHeight()) {
|
||||
this.setHeight(syncData.hei);
|
||||
}
|
||||
if (syncData.scaX !== undefined && syncData.scaX !== this.getScaleX()) {
|
||||
this.setScaleX(syncData.scaX);
|
||||
}
|
||||
|
@@ -13,8 +13,7 @@ void DeclareSystemInfoExtension(gd::PlatformExtension& extension) {
|
||||
.SetExtensionInformation(
|
||||
"SystemInfo",
|
||||
_("System information"),
|
||||
_("Conditions to check if the device has a touchscreen, is a mobile, "
|
||||
"or if the game runs as a preview."),
|
||||
_("Get information about the system and device running the game."),
|
||||
"Florian Rival",
|
||||
"Open source (MIT License)")
|
||||
.SetCategory("Advanced");
|
||||
|
@@ -78,9 +78,6 @@ module.exports = {
|
||||
} else if (propertyName === 'disabled') {
|
||||
objectContent.disabled = newValue === '1';
|
||||
return true;
|
||||
} else if (propertyName === 'spellCheck') {
|
||||
objectContent.spellCheck = newValue === '1';
|
||||
return true;
|
||||
} else if (propertyName === 'maxLength') {
|
||||
objectContent.maxLength = newValue;
|
||||
return true;
|
||||
@@ -163,13 +160,6 @@ module.exports = {
|
||||
.setLabel(_('Disabled'))
|
||||
.setGroup(_('Field'));
|
||||
|
||||
objectProperties
|
||||
.getOrCreate('spellCheck')
|
||||
.setValue(objectContent.spellCheck ? 'true' : 'false')
|
||||
.setType('boolean')
|
||||
.setLabel(_('Enable spell check'))
|
||||
.setGroup(_('Field'));
|
||||
|
||||
objectProperties
|
||||
.getOrCreate('textColor')
|
||||
.setValue(objectContent.textColor || '0;0;0')
|
||||
@@ -282,7 +272,6 @@ module.exports = {
|
||||
borderWidth: 1,
|
||||
readOnly: false,
|
||||
disabled: false,
|
||||
spellCheck: false,
|
||||
paddingX: 2,
|
||||
paddingY: 1,
|
||||
textAlign: 'left',
|
||||
@@ -603,21 +592,6 @@ module.exports = {
|
||||
.setFunctionName('setDisabled')
|
||||
.setGetter('isDisabled');
|
||||
|
||||
object
|
||||
.addExpressionAndConditionAndAction(
|
||||
'boolean',
|
||||
'SpellCheck',
|
||||
_('Spell check enabled'),
|
||||
_('spell check is enabled'),
|
||||
_('spell check enabled'),
|
||||
'',
|
||||
'res/conditions/text24_black.png'
|
||||
)
|
||||
.addParameter('object', _('Text input'), 'TextInputObject', false)
|
||||
.useStandardParameters('boolean', gd.ParameterOptions.makeNewOptions())
|
||||
.setFunctionName('setSpellCheck')
|
||||
.setGetter('isSpellCheckEnabled');
|
||||
|
||||
// Other expressions/conditions/actions:
|
||||
|
||||
// Deprecated
|
||||
|
@@ -106,7 +106,6 @@ namespace gdjs {
|
||||
this.updateBorderWidth();
|
||||
this.updateDisabled();
|
||||
this.updateReadOnly();
|
||||
this.updateSpellCheck();
|
||||
this.updateTextAlign();
|
||||
this.updateMaxLength();
|
||||
this.updatePadding();
|
||||
@@ -343,12 +342,6 @@ namespace gdjs {
|
||||
this._input.readOnly = this._object.isReadOnly();
|
||||
}
|
||||
|
||||
updateSpellCheck() {
|
||||
if (!this._input) return;
|
||||
|
||||
this._input.spellcheck = this._object.isSpellCheckEnabled();
|
||||
}
|
||||
|
||||
updateMaxLength() {
|
||||
const input = this._input;
|
||||
if (!input) return;
|
||||
|
@@ -54,7 +54,6 @@ namespace gdjs {
|
||||
disabled: boolean;
|
||||
readOnly: boolean;
|
||||
// ---- Values can be undefined because of support for these feature was added in v5.5.222.
|
||||
spellCheck?: boolean;
|
||||
paddingX?: float;
|
||||
paddingY?: float;
|
||||
textAlign?: SupportedTextAlign;
|
||||
@@ -65,6 +64,8 @@ namespace gdjs {
|
||||
|
||||
export type TextInputNetworkSyncDataType = {
|
||||
opa: float;
|
||||
wid: float;
|
||||
hei: float;
|
||||
txt: string;
|
||||
frn: string;
|
||||
fs: number;
|
||||
@@ -78,7 +79,6 @@ namespace gdjs {
|
||||
bw: float;
|
||||
dis: boolean;
|
||||
ro: boolean;
|
||||
sc: boolean;
|
||||
};
|
||||
|
||||
export type TextInputNetworkSyncData = ObjectNetworkSyncData &
|
||||
@@ -118,7 +118,6 @@ namespace gdjs {
|
||||
private _borderWidth: float;
|
||||
private _disabled: boolean;
|
||||
private _readOnly: boolean;
|
||||
private _spellCheck: boolean;
|
||||
private _isSubmitted: boolean;
|
||||
_renderer: TextInputRuntimeObjectRenderer;
|
||||
|
||||
@@ -143,10 +142,6 @@ namespace gdjs {
|
||||
this._borderWidth = objectData.content.borderWidth;
|
||||
this._disabled = objectData.content.disabled;
|
||||
this._readOnly = objectData.content.readOnly;
|
||||
this._spellCheck =
|
||||
objectData.content.spellCheck !== undefined
|
||||
? objectData.content.spellCheck
|
||||
: false;
|
||||
this._textAlign = parseTextAlign(objectData.content.textAlign);
|
||||
this._maxLength = objectData.content.maxLength || 0;
|
||||
this._paddingX =
|
||||
@@ -233,12 +228,6 @@ namespace gdjs {
|
||||
if (oldObjectData.content.readOnly !== newObjectData.content.readOnly) {
|
||||
this.setReadOnly(newObjectData.content.readOnly);
|
||||
}
|
||||
if (
|
||||
newObjectData.content.spellCheck !== undefined &&
|
||||
oldObjectData.content.spellCheck !== newObjectData.content.spellCheck
|
||||
) {
|
||||
this.setSpellCheck(newObjectData.content.spellCheck);
|
||||
}
|
||||
if (
|
||||
newObjectData.content.maxLength !== undefined &&
|
||||
oldObjectData.content.maxLength !== newObjectData.content.maxLength
|
||||
@@ -271,6 +260,8 @@ namespace gdjs {
|
||||
return {
|
||||
...super.getNetworkSyncData(),
|
||||
opa: this.getOpacity(),
|
||||
wid: this.getWidth(),
|
||||
hei: this.getHeight(),
|
||||
txt: this.getText(),
|
||||
frn: this.getFontResourceName(),
|
||||
fs: this.getFontSize(),
|
||||
@@ -284,7 +275,6 @@ namespace gdjs {
|
||||
bw: this.getBorderWidth(),
|
||||
dis: this.isDisabled(),
|
||||
ro: this.isReadOnly(),
|
||||
sc: this.isSpellCheckEnabled(),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -292,6 +282,8 @@ namespace gdjs {
|
||||
super.updateFromNetworkSyncData(syncData);
|
||||
|
||||
if (syncData.opa !== undefined) this.setOpacity(syncData.opa);
|
||||
if (syncData.wid !== undefined) this.setWidth(syncData.wid);
|
||||
if (syncData.hei !== undefined) this.setHeight(syncData.hei);
|
||||
if (syncData.txt !== undefined) this.setText(syncData.txt);
|
||||
if (syncData.frn !== undefined) this.setFontResourceName(syncData.frn);
|
||||
if (syncData.fs !== undefined) this.setFontSize(syncData.fs);
|
||||
@@ -305,7 +297,6 @@ namespace gdjs {
|
||||
if (syncData.bw !== undefined) this.setBorderWidth(syncData.bw);
|
||||
if (syncData.dis !== undefined) this.setDisabled(syncData.dis);
|
||||
if (syncData.ro !== undefined) this.setReadOnly(syncData.ro);
|
||||
if (syncData.sc !== undefined) this.setSpellCheck(syncData.sc);
|
||||
}
|
||||
|
||||
updatePreRender(instanceContainer: RuntimeInstanceContainer): void {
|
||||
@@ -578,15 +569,6 @@ namespace gdjs {
|
||||
return this._readOnly;
|
||||
}
|
||||
|
||||
setSpellCheck(value: boolean) {
|
||||
this._spellCheck = value;
|
||||
this._renderer.updateSpellCheck();
|
||||
}
|
||||
|
||||
isSpellCheckEnabled(): boolean {
|
||||
return this._spellCheck;
|
||||
}
|
||||
|
||||
isFocused(): boolean {
|
||||
return this._renderer.isFocused();
|
||||
}
|
||||
|
@@ -449,16 +449,6 @@ void DeclareTextObjectExtension(gd::PlatformExtension& extension) {
|
||||
.AddParameter("object", _("Object"), "Text")
|
||||
.UseStandardParameters("number", gd::ParameterOptions::MakeNewOptions());
|
||||
|
||||
obj.AddExpressionAndConditionAndAction("number",
|
||||
"LineHeight",
|
||||
_("Line height"),
|
||||
_("the line height of a text object"),
|
||||
_("the line height"),
|
||||
_("Font"),
|
||||
"res/conditions/characterSize24.png")
|
||||
.AddParameter("object", _("Object"), "Text")
|
||||
.UseStandardParameters("number", gd::ParameterOptions::MakeNewOptions());
|
||||
|
||||
// Support for deprecated "Size" actions/conditions:
|
||||
obj.AddDuplicatedAction("Size", "Text::SetFontSize").SetHidden();
|
||||
obj.AddDuplicatedCondition("Size", "Text::FontSize").SetHidden();
|
||||
|
@@ -38,14 +38,6 @@ class TextObjectJsExtension : public gd::PlatformExtension {
|
||||
GetAllExpressionsForObject("TextObject::Text")["FontSize"]
|
||||
.SetFunctionName("getCharacterSize");
|
||||
|
||||
GetAllActionsForObject("TextObject::Text")["TextObject::Text::SetLineHeight"]
|
||||
.SetFunctionName("setLineHeight")
|
||||
.SetGetter("getLineHeight");
|
||||
GetAllConditionsForObject("TextObject::Text")["TextObject::Text::LineHeight"]
|
||||
.SetFunctionName("getLineHeight");
|
||||
GetAllExpressionsForObject("TextObject::Text")["LineHeight"]
|
||||
.SetFunctionName("getLineHeight");
|
||||
|
||||
GetAllActionsForObject("TextObject::Text")["TextObject::Font"]
|
||||
.SetFunctionName("setFontName");
|
||||
|
||||
|
@@ -36,8 +36,7 @@ TextObject::TextObject()
|
||||
shadowOpacity(127),
|
||||
shadowAngle(90),
|
||||
shadowDistance(4),
|
||||
shadowBlurRadius(2),
|
||||
lineHeight(0) {}
|
||||
shadowBlurRadius(2) {}
|
||||
|
||||
TextObject::~TextObject() {};
|
||||
|
||||
@@ -111,10 +110,6 @@ bool TextObject::UpdateProperty(const gd::String& propertyName,
|
||||
shadowBlurRadius = newValue.To<double>();
|
||||
return true;
|
||||
}
|
||||
if (propertyName == "lineHeight") {
|
||||
lineHeight = newValue.To<double>();
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
@@ -260,13 +255,6 @@ std::map<gd::String, gd::PropertyDescriptor> TextObject::GetProperties() const {
|
||||
.SetAdvanced()
|
||||
.SetQuickCustomizationVisibility(gd::QuickCustomization::Hidden);
|
||||
|
||||
objectProperties["lineHeight"]
|
||||
.SetValue(gd::String::From(lineHeight))
|
||||
.SetType("number")
|
||||
.SetLabel(_("Line height"))
|
||||
.SetMeasurementUnit(gd::MeasurementUnit::GetPixel())
|
||||
.SetGroup(_("Font"));
|
||||
|
||||
return objectProperties;
|
||||
}
|
||||
|
||||
@@ -316,8 +304,6 @@ void TextObject::DoUnserializeFrom(gd::Project& project,
|
||||
SetShadowAngle(content.GetIntAttribute("shadowAngle", 90));
|
||||
SetShadowDistance(content.GetIntAttribute("shadowDistance", 4));
|
||||
SetShadowBlurRadius(content.GetIntAttribute("shadowBlurRadius", 2));
|
||||
|
||||
SetLineHeight(content.GetDoubleAttribute("lineHeight", 0));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -370,7 +356,6 @@ void TextObject::DoSerializeTo(gd::SerializerElement& element) const {
|
||||
content.SetAttribute("shadowAngle", shadowAngle);
|
||||
content.SetAttribute("shadowDistance", shadowDistance);
|
||||
content.SetAttribute("shadowBlurRadius", shadowBlurRadius);
|
||||
content.SetAttribute("lineHeight", lineHeight);
|
||||
}
|
||||
|
||||
void TextObject::ExposeResources(gd::ArbitraryResourceWorker& worker) {
|
||||
|
@@ -113,9 +113,6 @@ class GD_EXTENSION_API TextObject : public gd::ObjectConfiguration {
|
||||
void SetShadowBlurRadius(double value) { shadowBlurRadius = value; };
|
||||
double GetShadowBlurRadius() const { return shadowBlurRadius; };
|
||||
|
||||
void SetLineHeight(double value) { lineHeight = value; };
|
||||
double GetLineHeight() const { return lineHeight; };
|
||||
|
||||
private:
|
||||
virtual void DoUnserializeFrom(gd::Project& project,
|
||||
const gd::SerializerElement& element) override;
|
||||
@@ -140,5 +137,4 @@ class GD_EXTENSION_API TextObject : public gd::ObjectConfiguration {
|
||||
double shadowAngle;
|
||||
double shadowDistance;
|
||||
double shadowBlurRadius;
|
||||
double lineHeight;
|
||||
};
|
||||
|
@@ -64,7 +64,6 @@ namespace gdjs {
|
||||
style.wordWrap = this._object._wrapping;
|
||||
style.wordWrapWidth = this._object._wrappingWidth;
|
||||
style.breakWords = true;
|
||||
style.lineHeight = this._object._lineHeight > 0 ? this._object._lineHeight : undefined;
|
||||
style.stroke = gdjs.rgbToHexNumber(
|
||||
this._object._outlineColor[0],
|
||||
this._object._outlineColor[1],
|
||||
|
@@ -22,7 +22,6 @@ namespace gdjs {
|
||||
text: string;
|
||||
textAlignment: string;
|
||||
verticalTextAlignment: string;
|
||||
lineHeight: number;
|
||||
|
||||
isOutlineEnabled: boolean;
|
||||
outlineThickness: float;
|
||||
@@ -35,7 +34,6 @@ namespace gdjs {
|
||||
shadowDistance: float;
|
||||
shadowAngle: float;
|
||||
shadowBlurRadius: float;
|
||||
lineHeight: float;
|
||||
};
|
||||
};
|
||||
|
||||
@@ -64,7 +62,6 @@ namespace gdjs {
|
||||
sha: float;
|
||||
shb: float;
|
||||
pad: integer;
|
||||
lh: float;
|
||||
};
|
||||
|
||||
export type TextObjectNetworkSyncData = ObjectNetworkSyncData &
|
||||
@@ -92,7 +89,6 @@ namespace gdjs {
|
||||
_wrapping: boolean = false;
|
||||
// A wrapping of 1 makes games crash on Firefox
|
||||
_wrappingWidth: float = 100;
|
||||
_lineHeight: float;
|
||||
|
||||
_isOutlineEnabled: boolean;
|
||||
_outlineThickness: float;
|
||||
@@ -106,7 +102,6 @@ namespace gdjs {
|
||||
_shadowBlur: float;
|
||||
|
||||
_padding: integer = 5;
|
||||
_lineHeight: float = 0;
|
||||
_str: string;
|
||||
_renderer: gdjs.TextRuntimeObjectRenderer;
|
||||
|
||||
@@ -133,7 +128,6 @@ namespace gdjs {
|
||||
this._str = content.text;
|
||||
this._textAlign = content.textAlignment || 'left';
|
||||
this._verticalTextAlignment = content.verticalTextAlignment || 'top';
|
||||
this._lineHeight = content.lineHeight || 0;
|
||||
|
||||
this._isOutlineEnabled = content.isOutlineEnabled;
|
||||
this._outlineThickness = content.outlineThickness;
|
||||
@@ -155,7 +149,7 @@ namespace gdjs {
|
||||
this.onCreated();
|
||||
}
|
||||
|
||||
override updateFromObjectData(
|
||||
updateFromObjectData(
|
||||
oldObjectData: TextObjectData,
|
||||
newObjectData: TextObjectData
|
||||
): boolean {
|
||||
@@ -217,13 +211,10 @@ namespace gdjs {
|
||||
if (oldContent.shadowBlurRadius !== newContent.shadowBlurRadius) {
|
||||
this.setShadowBlurRadius(newContent.shadowBlurRadius);
|
||||
}
|
||||
if (oldContent.lineHeight !== newContent.lineHeight) {
|
||||
this.setLineHeight(newContent.lineHeight);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
override getNetworkSyncData(): TextObjectNetworkSyncData {
|
||||
getNetworkSyncData(): TextObjectNetworkSyncData {
|
||||
return {
|
||||
...super.getNetworkSyncData(),
|
||||
str: this._str,
|
||||
@@ -248,11 +239,10 @@ namespace gdjs {
|
||||
sha: this._shadowAngle,
|
||||
shb: this._shadowBlur,
|
||||
pad: this._padding,
|
||||
lh: this._lineHeight,
|
||||
};
|
||||
}
|
||||
|
||||
override updateFromNetworkSyncData(
|
||||
updateFromNetworkSyncData(
|
||||
networkSyncData: TextObjectNetworkSyncData
|
||||
): void {
|
||||
super.updateFromNetworkSyncData(networkSyncData);
|
||||
@@ -325,27 +315,25 @@ namespace gdjs {
|
||||
if (networkSyncData.pad !== undefined) {
|
||||
this.setPadding(networkSyncData.pad);
|
||||
}
|
||||
if (networkSyncData.lh !== undefined) {
|
||||
this.setLineHeight(networkSyncData.lh);
|
||||
}
|
||||
}
|
||||
|
||||
override getRendererObject() {
|
||||
getRendererObject() {
|
||||
return this._renderer.getRendererObject();
|
||||
}
|
||||
|
||||
override update(instanceContainer: gdjs.RuntimeInstanceContainer): void {
|
||||
update(instanceContainer: gdjs.RuntimeInstanceContainer): void {
|
||||
this._renderer.ensureUpToDate();
|
||||
}
|
||||
|
||||
override onDestroyed(): void {
|
||||
onDestroyed(): void {
|
||||
super.onDestroyed();
|
||||
this._renderer.destroy();
|
||||
}
|
||||
|
||||
override extraInitializationFromInitialInstance(
|
||||
initialInstanceData: InstanceData
|
||||
) {
|
||||
/**
|
||||
* Initialize the extra parameters that could be set for an instance.
|
||||
*/
|
||||
extraInitializationFromInitialInstance(initialInstanceData: InstanceData) {
|
||||
if (initialInstanceData.customSize) {
|
||||
this.setWrappingWidth(initialInstanceData.width);
|
||||
this.setWrapping(true);
|
||||
@@ -365,17 +353,27 @@ namespace gdjs {
|
||||
this._renderer.updatePosition();
|
||||
}
|
||||
|
||||
override setX(x: float): void {
|
||||
/**
|
||||
* Set object position on X axis.
|
||||
*/
|
||||
setX(x: float): void {
|
||||
super.setX(x);
|
||||
this._updateTextPosition();
|
||||
}
|
||||
|
||||
override setY(y: float): void {
|
||||
/**
|
||||
* Set object position on Y axis.
|
||||
*/
|
||||
setY(y: float): void {
|
||||
super.setY(y);
|
||||
this._updateTextPosition();
|
||||
}
|
||||
|
||||
override setAngle(angle: float): void {
|
||||
/**
|
||||
* Set the angle of the object.
|
||||
* @param angle The new angle of the object
|
||||
*/
|
||||
setAngle(angle: float): void {
|
||||
super.setAngle(angle);
|
||||
this._renderer.updateAngle();
|
||||
}
|
||||
@@ -501,14 +499,14 @@ namespace gdjs {
|
||||
/**
|
||||
* Get width of the text.
|
||||
*/
|
||||
override getWidth(): float {
|
||||
getWidth(): float {
|
||||
return this._wrapping ? this._wrappingWidth : this._renderer.getWidth();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get height of the text.
|
||||
*/
|
||||
override getHeight(): float {
|
||||
getHeight(): float {
|
||||
return this._renderer.getHeight();
|
||||
}
|
||||
|
||||
@@ -687,11 +685,11 @@ namespace gdjs {
|
||||
}
|
||||
}
|
||||
|
||||
override setWidth(width: float): void {
|
||||
setWidth(width: float): void {
|
||||
this.setWrappingWidth(width);
|
||||
}
|
||||
|
||||
override getDrawableY(): float {
|
||||
getDrawableY(): float {
|
||||
return (
|
||||
this.getY() -
|
||||
(this._verticalTextAlignment === 'center'
|
||||
@@ -942,23 +940,6 @@ namespace gdjs {
|
||||
this._padding = value;
|
||||
this._renderer.updateStyle();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the line height of the text object.
|
||||
* @return the line height
|
||||
*/
|
||||
getLineHeight(): number {
|
||||
return this._lineHeight;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the line height of the text object.
|
||||
* @param value the line height
|
||||
*/
|
||||
setLineHeight(value: float): void {
|
||||
this._lineHeight = value;
|
||||
this._renderer.updateStyle();
|
||||
}
|
||||
}
|
||||
gdjs.registerObject('TextObject::Text', gdjs.TextRuntimeObject);
|
||||
}
|
||||
|
@@ -17,6 +17,8 @@ namespace gdjs {
|
||||
export type SimpleTileMapNetworkSyncDataType = {
|
||||
op: number;
|
||||
ai: string;
|
||||
wid: number;
|
||||
hei: number;
|
||||
// TODO: Support tilemap synchronization. Find an efficient way to send tiles changes.
|
||||
};
|
||||
|
||||
@@ -168,6 +170,8 @@ namespace gdjs {
|
||||
...super.getNetworkSyncData(),
|
||||
op: this._opacity,
|
||||
ai: this._atlasImage,
|
||||
wid: this.getWidth(),
|
||||
hei: this.getHeight(),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -182,6 +186,18 @@ namespace gdjs {
|
||||
) {
|
||||
this.setOpacity(networkSyncData.op);
|
||||
}
|
||||
if (
|
||||
networkSyncData.wid !== undefined &&
|
||||
networkSyncData.wid !== this.getWidth()
|
||||
) {
|
||||
this.setWidth(networkSyncData.wid);
|
||||
}
|
||||
if (
|
||||
networkSyncData.hei !== undefined &&
|
||||
networkSyncData.hei !== this.getHeight()
|
||||
) {
|
||||
this.setHeight(networkSyncData.hei);
|
||||
}
|
||||
if (networkSyncData.ai !== undefined) {
|
||||
// TODO: support changing the atlas texture
|
||||
}
|
||||
|
@@ -26,6 +26,8 @@ namespace gdjs {
|
||||
os: float;
|
||||
fo: float;
|
||||
oo: float;
|
||||
wid: float;
|
||||
hei: float;
|
||||
};
|
||||
|
||||
export type TilemapCollisionMaskNetworkSyncData = ObjectNetworkSyncData &
|
||||
@@ -200,6 +202,8 @@ namespace gdjs {
|
||||
os: this.getOutlineSize(),
|
||||
fo: this.getFillOpacity(),
|
||||
oo: this.getOutlineOpacity(),
|
||||
wid: this.getWidth(),
|
||||
hei: this.getHeight(),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -232,6 +236,12 @@ namespace gdjs {
|
||||
if (networkSyncData.oo !== undefined) {
|
||||
this.setOutlineOpacity(networkSyncData.oo);
|
||||
}
|
||||
if (networkSyncData.wid !== undefined) {
|
||||
this.setWidth(networkSyncData.wid);
|
||||
}
|
||||
if (networkSyncData.hei !== undefined) {
|
||||
this.setHeight(networkSyncData.hei);
|
||||
}
|
||||
}
|
||||
|
||||
extraInitializationFromInitialInstance(initialInstanceData): void {
|
||||
|
@@ -25,6 +25,8 @@ namespace gdjs {
|
||||
lai: number;
|
||||
lei: number;
|
||||
asps: number;
|
||||
wid: number;
|
||||
hei: number;
|
||||
};
|
||||
|
||||
export type TilemapNetworkSyncData = ObjectNetworkSyncData &
|
||||
@@ -156,6 +158,8 @@ namespace gdjs {
|
||||
lai: this._layerIndex,
|
||||
lei: this._levelIndex,
|
||||
asps: this._animationSpeedScale,
|
||||
wid: this.getWidth(),
|
||||
hei: this.getHeight(),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -186,6 +190,12 @@ namespace gdjs {
|
||||
if (networkSyncData.asps !== undefined) {
|
||||
this.setAnimationSpeedScale(networkSyncData.asps);
|
||||
}
|
||||
if (networkSyncData.wid !== undefined) {
|
||||
this.setWidth(networkSyncData.wid);
|
||||
}
|
||||
if (networkSyncData.hei !== undefined) {
|
||||
this.setHeight(networkSyncData.hei);
|
||||
}
|
||||
}
|
||||
|
||||
extraInitializationFromInitialInstance(initialInstanceData): void {
|
||||
|
@@ -17,7 +17,7 @@ void DeclareTiledSpriteObjectExtension(gd::PlatformExtension& extension) {
|
||||
.SetExtensionInformation(
|
||||
"TiledSpriteObject",
|
||||
_("Tiled Sprite Object"),
|
||||
"Displays a 2D image in a repeating pattern over an area. Useful for "
|
||||
"Displays an image in a repeating pattern over an area. Useful for "
|
||||
"making backgrounds, including background that are scrolling when "
|
||||
"the camera moves. This is more performant than using multiple "
|
||||
"Sprite objects.",
|
||||
|
@@ -15,6 +15,8 @@ namespace gdjs {
|
||||
export type TiledSpriteObjectData = ObjectData & TiledSpriteObjectDataType;
|
||||
|
||||
export type TiledSpriteNetworkSyncDataType = {
|
||||
wid: number;
|
||||
hei: number;
|
||||
xo: number;
|
||||
yo: number;
|
||||
op: number;
|
||||
@@ -81,6 +83,8 @@ namespace gdjs {
|
||||
getNetworkSyncData(): TiledSpriteNetworkSyncData {
|
||||
return {
|
||||
...super.getNetworkSyncData(),
|
||||
wid: this.getWidth(),
|
||||
hei: this.getHeight(),
|
||||
xo: this.getXOffset(),
|
||||
yo: this.getYOffset(),
|
||||
op: this.getOpacity(),
|
||||
@@ -95,6 +99,12 @@ namespace gdjs {
|
||||
|
||||
// Texture is not synchronized, see if this is asked or not.
|
||||
|
||||
if (networkSyncData.wid !== undefined) {
|
||||
this.setWidth(networkSyncData.wid);
|
||||
}
|
||||
if (networkSyncData.hei !== undefined) {
|
||||
this.setHeight(networkSyncData.hei);
|
||||
}
|
||||
if (networkSyncData.xo !== undefined) {
|
||||
this.setXOffset(networkSyncData.xo);
|
||||
}
|
||||
|
@@ -16,11 +16,8 @@ void DeclareTopDownMovementBehaviorExtension(gd::PlatformExtension& extension) {
|
||||
.SetExtensionInformation(
|
||||
"TopDownMovementBehavior",
|
||||
_("Top-down movement"),
|
||||
_("Allows to move an object in either 4 or 8 directions, with the "
|
||||
"keyboard (default), a virtual stick (for this, also add the "
|
||||
"\"Top-down multitouch controller mapper\" behavior and a"
|
||||
"\"Multitouch Joystick\" object), gamepad or manually using "
|
||||
"events."),
|
||||
_("Allows to move objects in either 4 or 8 directions, with the "
|
||||
"keyboard or using events."),
|
||||
"Florian Rival",
|
||||
"Open source (MIT License)")
|
||||
.SetCategory("Movement")
|
||||
@@ -29,17 +26,17 @@ void DeclareTopDownMovementBehaviorExtension(gd::PlatformExtension& extension) {
|
||||
extension.AddInstructionOrExpressionGroupMetadata(_("Top-down movement"))
|
||||
.SetIcon("CppPlatform/Extensions/topdownmovementicon16.png");
|
||||
|
||||
gd::BehaviorMetadata& aut =
|
||||
extension.AddBehavior("TopDownMovementBehavior",
|
||||
_("Top-down movement (4 or 8 directions)"),
|
||||
"TopDownMovement",
|
||||
_("Move objects left, up, right, and "
|
||||
"down (and, optionally, diagonally)."),
|
||||
"",
|
||||
"CppPlatform/Extensions/topdownmovementicon.png",
|
||||
"TopDownMovementBehavior",
|
||||
std::make_shared<TopDownMovementBehavior>(),
|
||||
std::make_shared<gd::BehaviorsSharedData>());
|
||||
gd::BehaviorMetadata& aut = extension.AddBehavior(
|
||||
"TopDownMovementBehavior",
|
||||
_("Top-down movement (4 or 8 directions)"),
|
||||
"TopDownMovement",
|
||||
_("Move objects left, up, right, and "
|
||||
"down (and, optionally, diagonally)."),
|
||||
"",
|
||||
"CppPlatform/Extensions/topdownmovementicon.png",
|
||||
"TopDownMovementBehavior",
|
||||
std::make_shared<TopDownMovementBehavior>(),
|
||||
std::make_shared<gd::BehaviorsSharedData>());
|
||||
|
||||
aut.AddAction("SimulateLeftKey",
|
||||
_("Simulate left key press"),
|
||||
@@ -122,8 +119,7 @@ void DeclareTopDownMovementBehaviorExtension(gd::PlatformExtension& extension) {
|
||||
aut.AddAction("SimulateStick",
|
||||
_("Simulate stick control"),
|
||||
_("Simulate a stick control."),
|
||||
_("Simulate a stick control for _PARAM0_ with a _PARAM2_ angle "
|
||||
"and a _PARAM3_ force"),
|
||||
_("Simulate a stick control for _PARAM0_ with a _PARAM2_ angle and a _PARAM3_ force"),
|
||||
_("Top-down controls"),
|
||||
"res/conditions/keyboard24.png",
|
||||
"res/conditions/keyboard.png")
|
||||
@@ -134,28 +130,25 @@ void DeclareTopDownMovementBehaviorExtension(gd::PlatformExtension& extension) {
|
||||
.MarkAsAdvanced()
|
||||
.SetFunctionName("SimulateStick");
|
||||
|
||||
aut.AddScopedCondition(
|
||||
"IsUsingControl",
|
||||
_("Control pressed or simulated"),
|
||||
_("A control was applied from a default control or simulated by an "
|
||||
"action."),
|
||||
_("_PARAM0_ has the _PARAM2_ key pressed or simulated"),
|
||||
_("Top-down state"),
|
||||
"res/conditions/keyboard24.png",
|
||||
"res/conditions/keyboard.png")
|
||||
.AddParameter("object", _("Object"))
|
||||
.AddParameter("behavior", _("Behavior"), "TopDownMovementBehavior")
|
||||
.AddParameter("stringWithSelector",
|
||||
aut.AddScopedCondition("IsUsingControl",
|
||||
_("Control pressed or simulated"),
|
||||
_("A control was applied from a default control or simulated by an action."),
|
||||
_("_PARAM0_ has the _PARAM2_ key pressed or simulated"),
|
||||
_("Top-down state"),
|
||||
"res/conditions/keyboard24.png",
|
||||
"res/conditions/keyboard.png")
|
||||
.AddParameter("object", _("Object"))
|
||||
.AddParameter("behavior", _("Behavior"), "TopDownMovementBehavior")
|
||||
.AddParameter("stringWithSelector",
|
||||
_("Key"),
|
||||
"[\"Left\", \"Right\", \"Up\", \"Down\", \"Stick\"]")
|
||||
.MarkAsAdvanced();
|
||||
.MarkAsAdvanced();
|
||||
|
||||
aut.AddExpression(
|
||||
"StickAngle",
|
||||
_("Stick angle"),
|
||||
_("Return the angle of the simulated stick input (in degrees)"),
|
||||
_("Top-down controls"),
|
||||
"CppPlatform/Extensions/topdownmovementicon16.png")
|
||||
aut.AddExpression("StickAngle",
|
||||
_("Stick angle"),
|
||||
_("Return the angle of the simulated stick input (in degrees)"),
|
||||
_("Top-down controls"),
|
||||
"CppPlatform/Extensions/topdownmovementicon16.png")
|
||||
.AddParameter("object", _("Object"))
|
||||
.AddParameter("behavior", _("Behavior"), "TopDownMovementBehavior");
|
||||
|
||||
@@ -368,19 +361,19 @@ void DeclareTopDownMovementBehaviorExtension(gd::PlatformExtension& extension) {
|
||||
.MarkAsAdvanced()
|
||||
.SetHidden()
|
||||
.SetFunctionName("GetAngle");
|
||||
|
||||
aut.AddScopedCondition(
|
||||
"IsMovementAngleAround",
|
||||
_("Angle of movement"),
|
||||
_("Compare the angle of the top-down movement of the object."),
|
||||
_("Angle of movement of _PARAM0_ is _PARAM2_ ± _PARAM3_°"),
|
||||
|
||||
aut.AddScopedCondition(
|
||||
"IsMovementAngleAround",
|
||||
_("Angle of movement"),
|
||||
_("Compare the angle of the top-down movement of the object."),
|
||||
_("Angle of movement of _PARAM0_ is _PARAM2_ ± _PARAM3_°"),
|
||||
_("Top-down state"),
|
||||
"CppPlatform/Extensions/topdownmovementicon24.png",
|
||||
"CppPlatform/Extensions/topdownmovementicon16.png")
|
||||
.AddParameter("object", _("Object"))
|
||||
.AddParameter("behavior", _("Behavior"), "TopDownMovementBehavior")
|
||||
.AddParameter("expression", _("Angle (in degrees)"))
|
||||
.AddParameter("expression", _("Tolerance (in degrees)"));
|
||||
.AddParameter("object", _("Object"))
|
||||
.AddParameter("behavior", _("Behavior"), "TopDownMovementBehavior")
|
||||
.AddParameter("expression", _("Angle (in degrees)"))
|
||||
.AddParameter("expression", _("Tolerance (in degrees)"));
|
||||
|
||||
aut.AddCondition("XVelocity",
|
||||
_("Speed on X axis"),
|
||||
|
@@ -507,6 +507,10 @@ namespace gdjs {
|
||||
cos = 0;
|
||||
}
|
||||
|
||||
const getAcceleratedSpeed = this._useLegacyTurnBack
|
||||
? TopDownMovementRuntimeBehavior.getLegacyAcceleratedSpeed
|
||||
: TopDownMovementRuntimeBehavior.getAcceleratedSpeed;
|
||||
|
||||
let currentSpeed = Math.hypot(this._xVelocity, this._yVelocity);
|
||||
const dotProduct = this._xVelocity * cos + this._yVelocity * sin;
|
||||
if (dotProduct < 0) {
|
||||
@@ -514,14 +518,13 @@ namespace gdjs {
|
||||
// Keep the negative velocity projected on the new direction.
|
||||
currentSpeed = dotProduct;
|
||||
}
|
||||
const speed = TopDownMovementRuntimeBehavior.getAcceleratedSpeed(
|
||||
const speed = getAcceleratedSpeed(
|
||||
currentSpeed,
|
||||
targetedSpeed,
|
||||
this._maxSpeed,
|
||||
this._acceleration,
|
||||
this._deceleration,
|
||||
timeDelta,
|
||||
this._useLegacyTurnBack
|
||||
timeDelta
|
||||
);
|
||||
this._xVelocity = speed * cos;
|
||||
this._yVelocity = speed * sin;
|
||||
@@ -596,13 +599,10 @@ namespace gdjs {
|
||||
speedMax: float,
|
||||
acceleration: float,
|
||||
deceleration: float,
|
||||
timeDelta: float,
|
||||
useLegacyTurnBack: boolean = false
|
||||
timeDelta: float
|
||||
): float {
|
||||
let newSpeed = currentSpeed;
|
||||
const turningBackAcceleration = useLegacyTurnBack
|
||||
? acceleration
|
||||
: Math.max(acceleration, deceleration);
|
||||
const turningBackAcceleration = Math.max(acceleration, deceleration);
|
||||
if (targetedSpeed < 0) {
|
||||
if (currentSpeed <= targetedSpeed) {
|
||||
// Reduce the speed to match the stick force.
|
||||
@@ -652,6 +652,62 @@ namespace gdjs {
|
||||
return newSpeed;
|
||||
}
|
||||
|
||||
private static getLegacyAcceleratedSpeed(
|
||||
currentSpeed: float,
|
||||
targetedSpeed: float,
|
||||
speedMax: float,
|
||||
acceleration: float,
|
||||
deceleration: float,
|
||||
timeDelta: float
|
||||
): float {
|
||||
let newSpeed = currentSpeed;
|
||||
if (targetedSpeed < 0) {
|
||||
if (currentSpeed <= targetedSpeed) {
|
||||
// Reduce the speed to match the stick force.
|
||||
newSpeed = Math.min(
|
||||
targetedSpeed,
|
||||
currentSpeed + deceleration * timeDelta
|
||||
);
|
||||
} else if (currentSpeed <= 0) {
|
||||
// Accelerate
|
||||
newSpeed -= Math.max(-speedMax, acceleration * timeDelta);
|
||||
} else {
|
||||
newSpeed = Math.max(
|
||||
targetedSpeed,
|
||||
currentSpeed - deceleration * timeDelta
|
||||
);
|
||||
}
|
||||
} else if (targetedSpeed > 0) {
|
||||
if (currentSpeed >= targetedSpeed) {
|
||||
// Reduce the speed to match the stick force.
|
||||
newSpeed = Math.max(
|
||||
targetedSpeed,
|
||||
currentSpeed - deceleration * timeDelta
|
||||
);
|
||||
} else if (currentSpeed >= 0) {
|
||||
// Accelerate
|
||||
newSpeed = Math.min(
|
||||
speedMax,
|
||||
currentSpeed + acceleration * timeDelta
|
||||
);
|
||||
} else {
|
||||
newSpeed = Math.min(
|
||||
targetedSpeed,
|
||||
currentSpeed + deceleration * timeDelta
|
||||
);
|
||||
}
|
||||
} else {
|
||||
// Decelerate and stop.
|
||||
if (currentSpeed < 0) {
|
||||
newSpeed = Math.min(currentSpeed + deceleration * timeDelta, 0);
|
||||
}
|
||||
if (currentSpeed > 0) {
|
||||
newSpeed = Math.max(currentSpeed - deceleration * timeDelta, 0);
|
||||
}
|
||||
}
|
||||
return newSpeed;
|
||||
}
|
||||
|
||||
simulateControl(input: string) {
|
||||
if (input === 'Left') {
|
||||
this._leftKey = true;
|
||||
|
@@ -18,6 +18,8 @@ namespace gdjs {
|
||||
|
||||
export type VideoNetworkSyncDataType = {
|
||||
op: float;
|
||||
wid: float;
|
||||
hei: float;
|
||||
// We don't sync volume, as it's probably a user setting?
|
||||
pla: boolean;
|
||||
loop: boolean;
|
||||
@@ -103,6 +105,8 @@ namespace gdjs {
|
||||
return {
|
||||
...super.getNetworkSyncData(),
|
||||
op: this._opacity,
|
||||
wid: this.getWidth(),
|
||||
hei: this.getHeight(),
|
||||
pla: this.isPlayed(),
|
||||
loop: this.isLooped(),
|
||||
ct: this.getCurrentTime(),
|
||||
@@ -116,6 +120,12 @@ namespace gdjs {
|
||||
if (this._opacity !== undefined && this._opacity && syncData.op) {
|
||||
this.setOpacity(syncData.op);
|
||||
}
|
||||
if (this.getWidth() !== undefined && this.getWidth() !== syncData.wid) {
|
||||
this.setWidth(syncData.wid);
|
||||
}
|
||||
if (this.getHeight() !== undefined && this.getHeight() !== syncData.hei) {
|
||||
this.setHeight(syncData.hei);
|
||||
}
|
||||
if (syncData.pla !== undefined && this.isPlayed() !== syncData.pla) {
|
||||
syncData.pla ? this.play() : this.pause();
|
||||
}
|
||||
|
@@ -1228,11 +1228,14 @@ gd::String EventsCodeGenerator::GenerateParameterCodes(
|
||||
// Code only parameter type
|
||||
else if (metadata.GetType() == "objectsContext") {
|
||||
argOutput =
|
||||
HasProjectAndLayout() ? "runtimeScene" : "eventsFunctionContext";
|
||||
"(typeof eventsFunctionContext !== 'undefined' ? eventsFunctionContext "
|
||||
": runtimeScene)";
|
||||
}
|
||||
// Code only parameter type
|
||||
else if (metadata.GetType() == "eventsFunctionContext") {
|
||||
argOutput = HasProjectAndLayout() ? "null" : "eventsFunctionContext";
|
||||
argOutput =
|
||||
"(typeof eventsFunctionContext !== 'undefined' ? eventsFunctionContext "
|
||||
": undefined)";
|
||||
} else
|
||||
return gd::EventsCodeGenerator::GenerateParameterCodes(
|
||||
parameter,
|
||||
|
@@ -21,9 +21,6 @@ AdvancedExtension::AdvancedExtension() {
|
||||
.SetCustomCodeGenerator([](gd::Instruction& instruction,
|
||||
gd::EventsCodeGenerator& codeGenerator,
|
||||
gd::EventsCodeGenerationContext& context) {
|
||||
if (codeGenerator.HasProjectAndLayout()) {
|
||||
return gd::String("");
|
||||
}
|
||||
gd::String expressionCode =
|
||||
gd::ExpressionCodeGenerator::GenerateExpressionCode(
|
||||
codeGenerator,
|
||||
@@ -31,16 +28,15 @@ AdvancedExtension::AdvancedExtension() {
|
||||
"number",
|
||||
instruction.GetParameter(0).GetPlainString());
|
||||
|
||||
return "eventsFunctionContext.returnValue = " + expressionCode + ";";
|
||||
return "if (typeof eventsFunctionContext !== 'undefined') { "
|
||||
"eventsFunctionContext.returnValue = " +
|
||||
expressionCode + "; }";
|
||||
});
|
||||
|
||||
GetAllActions()["SetReturnString"]
|
||||
.SetCustomCodeGenerator([](gd::Instruction& instruction,
|
||||
gd::EventsCodeGenerator& codeGenerator,
|
||||
gd::EventsCodeGenerationContext& context) {
|
||||
if (codeGenerator.HasProjectAndLayout()) {
|
||||
return gd::String("");
|
||||
}
|
||||
gd::String expressionCode =
|
||||
gd::ExpressionCodeGenerator::GenerateExpressionCode(
|
||||
codeGenerator,
|
||||
@@ -48,31 +44,29 @@ AdvancedExtension::AdvancedExtension() {
|
||||
"string",
|
||||
instruction.GetParameter(0).GetPlainString());
|
||||
|
||||
return "eventsFunctionContext.returnValue = " + expressionCode + ";";
|
||||
return "if (typeof eventsFunctionContext !== 'undefined') { "
|
||||
"eventsFunctionContext.returnValue = " +
|
||||
expressionCode + "; }";
|
||||
});
|
||||
|
||||
GetAllActions()["SetReturnBoolean"]
|
||||
.SetCustomCodeGenerator([](gd::Instruction& instruction,
|
||||
gd::EventsCodeGenerator& codeGenerator,
|
||||
gd::EventsCodeGenerationContext& context) {
|
||||
if (codeGenerator.HasProjectAndLayout()) {
|
||||
return gd::String("");
|
||||
}
|
||||
// This is duplicated from EventsCodeGenerator::GenerateParameterCodes
|
||||
gd::String parameter = instruction.GetParameter(0).GetPlainString();
|
||||
gd::String booleanCode =
|
||||
(parameter == "True" || parameter == "Vrai") ? "true" : "false";
|
||||
|
||||
return "eventsFunctionContext.returnValue = " + booleanCode + ";";
|
||||
return "if (typeof eventsFunctionContext !== 'undefined') { "
|
||||
"eventsFunctionContext.returnValue = " +
|
||||
booleanCode + "; }";
|
||||
});
|
||||
|
||||
GetAllActions()["CopyArgumentToVariable"]
|
||||
.SetCustomCodeGenerator([](gd::Instruction &instruction,
|
||||
gd::EventsCodeGenerator &codeGenerator,
|
||||
gd::EventsCodeGenerationContext &context) {
|
||||
if (codeGenerator.HasProjectAndLayout()) {
|
||||
return gd::String("");
|
||||
}
|
||||
// This is duplicated from EventsCodeGenerator::GenerateParameterCodes
|
||||
gd::String parameter = instruction.GetParameter(0).GetPlainString();
|
||||
gd::String variable =
|
||||
@@ -80,17 +74,17 @@ AdvancedExtension::AdvancedExtension() {
|
||||
codeGenerator, context, "scenevar", instruction.GetParameter(1),
|
||||
"");
|
||||
|
||||
return "gdjs.Variable.copy(eventsFunctionContext.getArgument(" +
|
||||
parameter + "), " + variable + ", false);\n";
|
||||
return "if (typeof eventsFunctionContext !== 'undefined') {\n"
|
||||
"gdjs.Variable.copy(eventsFunctionContext.getArgument(" +
|
||||
parameter + "), " + variable +
|
||||
", false);\n"
|
||||
"}\n";
|
||||
});
|
||||
|
||||
GetAllActions()["CopyArgumentToVariable2"]
|
||||
.SetCustomCodeGenerator([](gd::Instruction &instruction,
|
||||
gd::EventsCodeGenerator &codeGenerator,
|
||||
gd::EventsCodeGenerationContext &context) {
|
||||
if (codeGenerator.HasProjectAndLayout()) {
|
||||
return gd::String("");
|
||||
}
|
||||
// This is duplicated from EventsCodeGenerator::GenerateParameterCodes
|
||||
gd::String parameter = instruction.GetParameter(0).GetPlainString();
|
||||
gd::String variable =
|
||||
@@ -98,17 +92,17 @@ AdvancedExtension::AdvancedExtension() {
|
||||
codeGenerator, context, "variable", instruction.GetParameter(1),
|
||||
"");
|
||||
|
||||
return "gdjs.Variable.copy(eventsFunctionContext.getArgument(" +
|
||||
parameter + "), " + variable + ", false);\n";
|
||||
return "if (typeof eventsFunctionContext !== 'undefined') {\n"
|
||||
"gdjs.Variable.copy(eventsFunctionContext.getArgument(" +
|
||||
parameter + "), " + variable +
|
||||
", false);\n"
|
||||
"}\n";
|
||||
});
|
||||
|
||||
GetAllActions()["CopyVariableToArgument"]
|
||||
.SetCustomCodeGenerator([](gd::Instruction &instruction,
|
||||
gd::EventsCodeGenerator &codeGenerator,
|
||||
gd::EventsCodeGenerationContext &context) {
|
||||
if (codeGenerator.HasProjectAndLayout()) {
|
||||
return gd::String("");
|
||||
}
|
||||
// This is duplicated from EventsCodeGenerator::GenerateParameterCodes
|
||||
gd::String parameter = instruction.GetParameter(0).GetPlainString();
|
||||
gd::String variable =
|
||||
@@ -116,18 +110,17 @@ AdvancedExtension::AdvancedExtension() {
|
||||
codeGenerator, context, "scenevar", instruction.GetParameter(1),
|
||||
"");
|
||||
|
||||
return "gdjs.Variable.copy(" + variable +
|
||||
", eventsFunctionContext.getArgument(" + parameter +
|
||||
"), false);\n";
|
||||
return "if (typeof eventsFunctionContext !== 'undefined') {\n"
|
||||
"gdjs.Variable.copy(" +
|
||||
variable + ", eventsFunctionContext.getArgument(" + parameter +
|
||||
"), false);\n"
|
||||
"}\n";
|
||||
});
|
||||
|
||||
GetAllActions()["CopyVariableToArgument2"]
|
||||
.SetCustomCodeGenerator([](gd::Instruction &instruction,
|
||||
gd::EventsCodeGenerator &codeGenerator,
|
||||
gd::EventsCodeGenerationContext &context) {
|
||||
if (codeGenerator.HasProjectAndLayout()) {
|
||||
return gd::String("");
|
||||
}
|
||||
// This is duplicated from EventsCodeGenerator::GenerateParameterCodes
|
||||
gd::String parameter = instruction.GetParameter(0).GetPlainString();
|
||||
gd::String variable =
|
||||
@@ -135,18 +128,17 @@ AdvancedExtension::AdvancedExtension() {
|
||||
codeGenerator, context, "variable", instruction.GetParameter(1),
|
||||
"");
|
||||
|
||||
return "gdjs.Variable.copy(" + variable +
|
||||
", eventsFunctionContext.getArgument(" + parameter +
|
||||
"), false);\n";
|
||||
return "if (typeof eventsFunctionContext !== 'undefined') {\n"
|
||||
"gdjs.Variable.copy(" +
|
||||
variable + ", eventsFunctionContext.getArgument(" + parameter +
|
||||
"), false);\n"
|
||||
"}\n";
|
||||
});
|
||||
|
||||
GetAllConditions()["GetArgumentAsBoolean"]
|
||||
.SetCustomCodeGenerator([](gd::Instruction& instruction,
|
||||
gd::EventsCodeGenerator& codeGenerator,
|
||||
gd::EventsCodeGenerationContext& context) {
|
||||
if (codeGenerator.HasProjectAndLayout()) {
|
||||
return gd::String("false");
|
||||
}
|
||||
gd::String parameterNameCode =
|
||||
gd::ExpressionCodeGenerator::GenerateExpressionCode(
|
||||
codeGenerator,
|
||||
@@ -154,8 +146,10 @@ AdvancedExtension::AdvancedExtension() {
|
||||
"string",
|
||||
instruction.GetParameter(0).GetPlainString());
|
||||
gd::String valueCode =
|
||||
gd::String(instruction.IsInverted() ? "!" : "!!") +
|
||||
"eventsFunctionContext.getArgument(" + parameterNameCode + ")";
|
||||
gd::String(instruction.IsInverted() ? "!" : "") +
|
||||
"(typeof eventsFunctionContext !== 'undefined' ? "
|
||||
"!!eventsFunctionContext.getArgument(" +
|
||||
parameterNameCode + ") : false)";
|
||||
gd::String outputCode =
|
||||
codeGenerator.GenerateUpperScopeBooleanFullName("isConditionTrue", context) +
|
||||
" = " + valueCode + ";\n";
|
||||
@@ -167,9 +161,6 @@ AdvancedExtension::AdvancedExtension() {
|
||||
.SetCustomCodeGenerator([](const std::vector<gd::Expression>& parameters,
|
||||
gd::EventsCodeGenerator& codeGenerator,
|
||||
gd::EventsCodeGenerationContext& context) {
|
||||
if (codeGenerator.HasProjectAndLayout()) {
|
||||
return gd::String("0");
|
||||
}
|
||||
gd::String parameterNameCode =
|
||||
gd::ExpressionCodeGenerator::GenerateExpressionCode(
|
||||
codeGenerator,
|
||||
@@ -177,8 +168,9 @@ AdvancedExtension::AdvancedExtension() {
|
||||
"string",
|
||||
!parameters.empty() ? parameters[0].GetPlainString() : "");
|
||||
|
||||
return "(Number(eventsFunctionContext.getArgument(" + parameterNameCode +
|
||||
")) || 0)";
|
||||
return "(typeof eventsFunctionContext !== 'undefined' ? "
|
||||
"Number(eventsFunctionContext.getArgument(" +
|
||||
parameterNameCode + ")) || 0 : 0)";
|
||||
});
|
||||
|
||||
GetAllStrExpressions()["GetArgumentAsString"]
|
||||
@@ -186,9 +178,6 @@ AdvancedExtension::AdvancedExtension() {
|
||||
.SetCustomCodeGenerator([](const std::vector<gd::Expression>& parameters,
|
||||
gd::EventsCodeGenerator& codeGenerator,
|
||||
gd::EventsCodeGenerationContext& context) {
|
||||
if (codeGenerator.HasProjectAndLayout()) {
|
||||
return gd::String("\"\"");
|
||||
}
|
||||
gd::String parameterNameCode =
|
||||
gd::ExpressionCodeGenerator::GenerateExpressionCode(
|
||||
codeGenerator,
|
||||
@@ -196,8 +185,9 @@ AdvancedExtension::AdvancedExtension() {
|
||||
"string",
|
||||
!parameters.empty() ? parameters[0].GetPlainString() : "");
|
||||
|
||||
return "\"\" + eventsFunctionContext.getArgument(" + parameterNameCode +
|
||||
")";
|
||||
return "(typeof eventsFunctionContext !== 'undefined' ? \"\" + "
|
||||
"eventsFunctionContext.getArgument(" +
|
||||
parameterNameCode + ") : \"\")";
|
||||
});
|
||||
|
||||
GetAllConditions()["CompareArgumentAsNumber"]
|
||||
@@ -220,13 +210,12 @@ AdvancedExtension::AdvancedExtension() {
|
||||
codeGenerator.GenerateUpperScopeBooleanFullName("isConditionTrue", context);
|
||||
|
||||
return resultingBoolean + " = " +
|
||||
gd::String(instruction.IsInverted() ? "!" : "") + "(" +
|
||||
gd::String(instruction.IsInverted() ? "!" : "") +
|
||||
codeGenerator.GenerateRelationalOperation(
|
||||
operatorString,
|
||||
codeGenerator.HasProjectAndLayout()
|
||||
? "0"
|
||||
: "(Number(eventsFunctionContext.getArgument(" +
|
||||
parameterNameCode + ")) || 0)",
|
||||
"((typeof eventsFunctionContext !== 'undefined' ? "
|
||||
"Number(eventsFunctionContext.getArgument(" +
|
||||
parameterNameCode + ")) || 0 : 0)",
|
||||
operandCode) +
|
||||
");\n";
|
||||
});
|
||||
@@ -251,13 +240,12 @@ AdvancedExtension::AdvancedExtension() {
|
||||
codeGenerator.GenerateUpperScopeBooleanFullName("isConditionTrue", context);
|
||||
|
||||
return resultingBoolean + " = " +
|
||||
gd::String(instruction.IsInverted() ? "!" : "") + "(" +
|
||||
gd::String(instruction.IsInverted() ? "!" : "") +
|
||||
codeGenerator.GenerateRelationalOperation(
|
||||
operatorString,
|
||||
codeGenerator.HasProjectAndLayout()
|
||||
? "\"\""
|
||||
: "(\"\" + eventsFunctionContext.getArgument(" +
|
||||
parameterNameCode + "))",
|
||||
"((typeof eventsFunctionContext !== 'undefined' ? "
|
||||
"\"\" + eventsFunctionContext.getArgument(" +
|
||||
parameterNameCode + ") : \"\")",
|
||||
operandCode) +
|
||||
");\n";
|
||||
});
|
||||
|
@@ -821,7 +821,8 @@ CommonInstructionsExtension::CommonInstructionsExtension() {
|
||||
}
|
||||
if (!codeGenerator.HasProjectAndLayout()) {
|
||||
functionParameters += ", eventsFunctionContext";
|
||||
callArguments += ", eventsFunctionContext";
|
||||
callArguments += ", typeof eventsFunctionContext !== \'undefined\' ? "
|
||||
"eventsFunctionContext : undefined";
|
||||
}
|
||||
|
||||
// Generate the function code
|
||||
|
@@ -43,10 +43,6 @@ StringInstructionsExtension::StringInstructionsExtension() {
|
||||
"gdjs.evtTools.string.strFindLastFrom");
|
||||
GetAllExpressions()["StrFindLastFrom"].SetFunctionName(
|
||||
"gdjs.evtTools.string.strFindLastFrom");
|
||||
GetAllStrExpressions()["StrReplaceOne"].SetFunctionName(
|
||||
"gdjs.evtTools.string.strReplaceOne");
|
||||
GetAllStrExpressions()["StrReplaceAll"].SetFunctionName(
|
||||
"gdjs.evtTools.string.strReplaceAll");
|
||||
|
||||
StripUnimplementedInstructionsAndExpressions();
|
||||
}
|
||||
|
@@ -17,11 +17,6 @@ namespace gdjs {
|
||||
isInnerAreaFollowingParentSize: boolean;
|
||||
};
|
||||
|
||||
export type CustomObjectNetworkSyncDataType = ObjectNetworkSyncData & {
|
||||
ifx: boolean;
|
||||
ify: boolean;
|
||||
};
|
||||
|
||||
/**
|
||||
* An object that contains other object.
|
||||
*
|
||||
@@ -221,26 +216,6 @@ namespace gdjs {
|
||||
return true;
|
||||
}
|
||||
|
||||
getNetworkSyncData(): CustomObjectNetworkSyncDataType {
|
||||
return {
|
||||
...super.getNetworkSyncData(),
|
||||
ifx: this.isFlippedX(),
|
||||
ify: this.isFlippedY(),
|
||||
};
|
||||
}
|
||||
|
||||
updateFromNetworkSyncData(
|
||||
networkSyncData: CustomObjectNetworkSyncDataType
|
||||
) {
|
||||
super.updateFromNetworkSyncData(networkSyncData);
|
||||
if (networkSyncData.ifx !== undefined) {
|
||||
this.flipX(networkSyncData.ifx);
|
||||
}
|
||||
if (networkSyncData.ify !== undefined) {
|
||||
this.flipY(networkSyncData.ify);
|
||||
}
|
||||
}
|
||||
|
||||
override extraInitializationFromInitialInstance(
|
||||
initialInstanceData: InstanceData
|
||||
) {
|
||||
|
@@ -382,9 +382,11 @@ namespace gdjs {
|
||||
): FloatPoint {
|
||||
const position = result || [0, 0];
|
||||
this._customObject.applyObjectTransformation(sceneX, sceneY, position);
|
||||
return this._parent
|
||||
.getLayer(this._customObject.getLayer())
|
||||
.convertInverseCoords(position[0], position[1], 0, position);
|
||||
return this._parent.convertInverseCoords(
|
||||
position[0],
|
||||
position[1],
|
||||
position
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@@ -380,8 +380,11 @@ namespace gdjs {
|
||||
.isMouseInsideCanvas();
|
||||
};
|
||||
|
||||
const _cursorIsOnObject = function (obj: gdjs.RuntimeObject) {
|
||||
return obj.cursorOnObject();
|
||||
const _cursorIsOnObject = function (
|
||||
obj: gdjs.RuntimeObject,
|
||||
instanceContainer: gdjs.RuntimeInstanceContainer
|
||||
) {
|
||||
return obj.cursorOnObject(instanceContainer);
|
||||
};
|
||||
|
||||
export const cursorOnObject = function (
|
||||
@@ -394,7 +397,7 @@ namespace gdjs {
|
||||
_cursorIsOnObject,
|
||||
objectsLists,
|
||||
inverted,
|
||||
null
|
||||
instanceContainer
|
||||
);
|
||||
};
|
||||
|
||||
|
@@ -484,8 +484,6 @@ namespace gdjs {
|
||||
return {
|
||||
x: this.x,
|
||||
y: this.y,
|
||||
w: this.getWidth(),
|
||||
h: this.getHeight(),
|
||||
zo: this.zOrder,
|
||||
a: this.angle,
|
||||
hid: this.hidden,
|
||||
@@ -514,12 +512,6 @@ namespace gdjs {
|
||||
if (networkSyncData.y !== undefined) {
|
||||
this.setY(networkSyncData.y);
|
||||
}
|
||||
if (networkSyncData.w !== undefined) {
|
||||
this.setWidth(networkSyncData.w);
|
||||
}
|
||||
if (networkSyncData.h !== undefined) {
|
||||
this.setHeight(networkSyncData.h);
|
||||
}
|
||||
if (networkSyncData.zo !== undefined) {
|
||||
this.setZOrder(networkSyncData.zo);
|
||||
}
|
||||
@@ -2729,12 +2721,11 @@ namespace gdjs {
|
||||
*
|
||||
* @return true if the cursor, or any touch, is on the object.
|
||||
*/
|
||||
cursorOnObject(): boolean {
|
||||
cursorOnObject(instanceContainer: gdjs.RuntimeInstanceContainer): boolean {
|
||||
const workingPoint: FloatPoint = gdjs.staticArray(
|
||||
RuntimeObject.prototype.cursorOnObject
|
||||
) as FloatPoint;
|
||||
workingPoint.length = 2;
|
||||
const instanceContainer = this.getInstanceContainer();
|
||||
const inputManager = instanceContainer.getGame().getInputManager();
|
||||
const layer = instanceContainer.getLayer(this.layer);
|
||||
const mousePos = layer.convertCoords(
|
||||
|
4
GDJS/Runtime/types/project-data.d.ts
vendored
4
GDJS/Runtime/types/project-data.d.ts
vendored
@@ -52,10 +52,6 @@ declare type BasicObjectNetworkSyncData = {
|
||||
y: number;
|
||||
/** The position of the instance on the Z axis. Defined only for 3D games */
|
||||
z?: number;
|
||||
/** The width of the instance */
|
||||
w: number;
|
||||
/** The height of the instance */
|
||||
h: number;
|
||||
/** Z order of the instance */
|
||||
zo: number;
|
||||
/** The angle of the instance. */
|
||||
|
@@ -550,13 +550,13 @@ describe('libGD.js - GDJS related tests', function () {
|
||||
// GetArgumentAsString("MyString") should be generated code to query and cast as a string
|
||||
// the argument
|
||||
expect(code).toMatch(
|
||||
'("" + eventsFunctionContext.getArgument("MyString"))'
|
||||
'(typeof eventsFunctionContext !== \'undefined\' ? "" + eventsFunctionContext.getArgument("MyString") : "")'
|
||||
);
|
||||
|
||||
// GetArgumentAsNumber("MyNumber") should be generated code to query and cast as a string
|
||||
// the argument
|
||||
expect(code).toMatch(
|
||||
'(Number(eventsFunctionContext.getArgument("MyNumber")) || 0)'
|
||||
'(typeof eventsFunctionContext !== \'undefined\' ? Number(eventsFunctionContext.getArgument("MyNumber")) || 0 : 0)'
|
||||
);
|
||||
|
||||
// The loop is using a counter somewhere
|
||||
|
File diff suppressed because one or more lines are too long
@@ -3,8 +3,8 @@ import {
|
||||
type AiRequest,
|
||||
type AiRequestMessageAssistantFunctionCall,
|
||||
type AiRequestFunctionCallOutput,
|
||||
} from '../Utils/GDevelopServices/Generation';
|
||||
import { type EditorFunctionCallResult } from '../EditorFunctions/EditorFunctionCallRunner';
|
||||
} from '../../Utils/GDevelopServices/Generation';
|
||||
import { type EditorFunctionCallResult } from '../../EditorFunctions/EditorFunctionCallRunner';
|
||||
|
||||
export const getFunctionCallToFunctionCallOutputMap = ({
|
||||
aiRequest,
|
@@ -4,7 +4,7 @@ import { ChatBubble } from './ChatBubble';
|
||||
import { Line } from '../../UI/Grid';
|
||||
import { ChatMarkdownText } from './ChatMarkdownText';
|
||||
import GDevelopThemeContext from '../../UI/Theme/GDevelopThemeContext';
|
||||
import { getFunctionCallToFunctionCallOutputMap } from '../AiRequestUtils';
|
||||
import { getFunctionCallToFunctionCallOutputMap } from './AiRequestUtils';
|
||||
import { FunctionCallRow } from './FunctionCallRow';
|
||||
import IconButton from '../../UI/IconButton';
|
||||
import Like from '../../UI/CustomSvgIcons/Like';
|
||||
|
@@ -438,6 +438,7 @@ export const useGetConceptMetadata = () => {
|
||||
}).current;
|
||||
const getActionMetadata = useRefWithInit(() => {
|
||||
return memoize((type: string) => {
|
||||
console.log(type);
|
||||
const {
|
||||
extensionShortHeader,
|
||||
eventsBasedBehavior,
|
||||
@@ -446,6 +447,7 @@ export const useGetConceptMetadata = () => {
|
||||
} = findEventsFunctionInExtensions(extensionShortHeadersByName, type);
|
||||
|
||||
if (extensionShortHeader && eventsFunction) {
|
||||
console.log(eventsFunction);
|
||||
return {
|
||||
kind: 'Action',
|
||||
name: eventsFunction.fullName,
|
||||
|
@@ -29,7 +29,7 @@ import { getHelpLink } from '../../Utils/HelpLink';
|
||||
import Window from '../../Utils/Window';
|
||||
import { type EditorFunctionCallResult } from '../../EditorFunctions/EditorFunctionCallRunner';
|
||||
import { type EditorCallbacks } from '../../EditorFunctions';
|
||||
import { getFunctionCallsToProcess } from '../AiRequestUtils';
|
||||
import { getFunctionCallsToProcess } from './AiRequestUtils';
|
||||
import CircularProgress from '../../UI/CircularProgress';
|
||||
import TwoStatesButton from '../../UI/TwoStatesButton';
|
||||
import Help from '../../UI/CustomSvgIcons/Help';
|
||||
@@ -68,7 +68,6 @@ type Props = {
|
||||
|}) => void,
|
||||
onSendMessage: (options: {|
|
||||
userMessage: string,
|
||||
createdSceneNames?: Array<string>,
|
||||
|}) => Promise<void>,
|
||||
onSendFeedback: (
|
||||
aiRequestId: string,
|
||||
@@ -80,11 +79,7 @@ type Props = {
|
||||
hasOpenedProject: boolean,
|
||||
isAutoProcessingFunctionCalls: boolean,
|
||||
setAutoProcessFunctionCalls: boolean => void,
|
||||
onStartOrOpenChat: ({|
|
||||
mode: 'chat' | 'agent',
|
||||
aiRequestId: string | null,
|
||||
|}) => void,
|
||||
initialMode?: 'chat' | 'agent',
|
||||
onStartNewChat: () => void,
|
||||
|
||||
onProcessFunctionCalls: (
|
||||
functionCalls: Array<AiRequestMessageAssistantFunctionCall>,
|
||||
@@ -127,17 +122,12 @@ const getQuotaOrCreditsText = ({
|
||||
<>
|
||||
{increaseQuotaOffering === 'subscribe' ? (
|
||||
<Trans>
|
||||
Get GDevelop premium to get more free requests every day.
|
||||
</Trans>
|
||||
) : quota.period === '30days' ? (
|
||||
<Trans>
|
||||
These are parts of your GDevelop premium membership ({quota.max}{' '}
|
||||
free requests per month).
|
||||
Get GDevelop premium to get more free requests every month.
|
||||
</Trans>
|
||||
) : (
|
||||
<Trans>
|
||||
These are parts of your GDevelop premium membership ({quota.max}{' '}
|
||||
free requests per day).
|
||||
free requests per month).
|
||||
</Trans>
|
||||
)}{' '}
|
||||
<Trans>Free requests do not consume credits on your account.</Trans>
|
||||
@@ -257,8 +247,7 @@ export const AiRequestChat = React.forwardRef<Props, AiRequestChatInterface>(
|
||||
onStartNewAiRequest,
|
||||
onSendMessage,
|
||||
onSendFeedback,
|
||||
onStartOrOpenChat,
|
||||
initialMode,
|
||||
onStartNewChat,
|
||||
quota,
|
||||
increaseQuotaOffering,
|
||||
lastSendError,
|
||||
@@ -277,17 +266,7 @@ export const AiRequestChat = React.forwardRef<Props, AiRequestChatInterface>(
|
||||
// TODO: store the default mode in the user preferences?
|
||||
const [newAiRequestMode, setNewAiRequestMode] = React.useState<
|
||||
'chat' | 'agent'
|
||||
>(initialMode || 'agent');
|
||||
|
||||
// Update the mode when initialMode changes
|
||||
React.useEffect(
|
||||
() => {
|
||||
if (initialMode) {
|
||||
setNewAiRequestMode(initialMode);
|
||||
}
|
||||
},
|
||||
[initialMode]
|
||||
);
|
||||
>('agent');
|
||||
const aiRequestId: string = aiRequest ? aiRequest.id : '';
|
||||
const [
|
||||
userRequestTextPerAiRequestId,
|
||||
@@ -435,7 +414,7 @@ export const AiRequestChat = React.forwardRef<Props, AiRequestChatInterface>(
|
||||
<RobotIcon rotating size={40} />
|
||||
</Line>
|
||||
<Column noMargin alignItems="center">
|
||||
<Text size="bold-title" align="center">
|
||||
<Text size="bold-title">
|
||||
{newAiRequestMode === 'agent' ? (
|
||||
<Trans>What do you want to make?</Trans>
|
||||
) : (
|
||||
@@ -448,7 +427,7 @@ export const AiRequestChat = React.forwardRef<Props, AiRequestChatInterface>(
|
||||
value={newAiRequestMode}
|
||||
leftButton={{
|
||||
icon: <Hammer fontSize="small" />,
|
||||
label: <Trans>Build for me</Trans>,
|
||||
label: <Trans>Build for me (beta)</Trans>,
|
||||
value: 'agent',
|
||||
}}
|
||||
rightButton={{
|
||||
@@ -555,7 +534,6 @@ export const AiRequestChat = React.forwardRef<Props, AiRequestChatInterface>(
|
||||
</>
|
||||
) : null}
|
||||
</ColumnStackLayout>
|
||||
<Spacer />
|
||||
<Column justifyContent="center">
|
||||
{newAiRequestMode === 'agent' ? (
|
||||
<Text size="body-small" color="secondary" align="center" noMargin>
|
||||
@@ -655,15 +633,7 @@ export const AiRequestChat = React.forwardRef<Props, AiRequestChatInterface>(
|
||||
<Text size="body-small" color="secondary" align="center" noMargin>
|
||||
<Trans>
|
||||
This request is for another project.{' '}
|
||||
<Link
|
||||
href="#"
|
||||
onClick={() =>
|
||||
onStartOrOpenChat({
|
||||
mode: aiRequest.mode || 'chat',
|
||||
aiRequestId: null,
|
||||
})
|
||||
}
|
||||
>
|
||||
<Link href="#" onClick={onStartNewChat}>
|
||||
Start a new chat
|
||||
</Link>{' '}
|
||||
to build on a new project.
|
||||
|
@@ -1,229 +0,0 @@
|
||||
// @flow
|
||||
import * as React from 'react';
|
||||
import {
|
||||
getAiRequest,
|
||||
type AiRequest,
|
||||
} from '../Utils/GDevelopServices/Generation';
|
||||
import AuthenticatedUserContext from '../Profile/AuthenticatedUserContext';
|
||||
import { type EditorFunctionCallResult } from '../EditorFunctions/EditorFunctionCallRunner';
|
||||
|
||||
type EditorFunctionCallResultsStorage = {|
|
||||
getEditorFunctionCallResults: (
|
||||
aiRequestId: string
|
||||
) => Array<EditorFunctionCallResult> | null,
|
||||
addEditorFunctionCallResults: (
|
||||
aiRequestId: string,
|
||||
editorFunctionCallResults: EditorFunctionCallResult[]
|
||||
) => void,
|
||||
clearEditorFunctionCallResults: (aiRequestId: string) => void,
|
||||
|};
|
||||
|
||||
const useEditorFunctionCallResultsStorage = (): EditorFunctionCallResultsStorage => {
|
||||
const [
|
||||
editorFunctionCallResultsPerRequest,
|
||||
setEditorFunctionCallResultsPerRequest,
|
||||
] = React.useState<{
|
||||
[aiRequestId: string]: Array<EditorFunctionCallResult>,
|
||||
}>({});
|
||||
|
||||
return {
|
||||
getEditorFunctionCallResults: React.useCallback(
|
||||
(aiRequestId: string): Array<EditorFunctionCallResult> | null =>
|
||||
editorFunctionCallResultsPerRequest[aiRequestId] || null,
|
||||
[editorFunctionCallResultsPerRequest]
|
||||
),
|
||||
addEditorFunctionCallResults: React.useCallback(
|
||||
(
|
||||
aiRequestId: string,
|
||||
editorFunctionCallResults: EditorFunctionCallResult[]
|
||||
) => {
|
||||
setEditorFunctionCallResultsPerRequest(
|
||||
editorFunctionCallResultsPerRequest => {
|
||||
const existingEditorFunctionCallResults = (
|
||||
editorFunctionCallResultsPerRequest[aiRequestId] || []
|
||||
).filter(existingEditorFunctionCallResult => {
|
||||
return !editorFunctionCallResults.some(
|
||||
editorFunctionCallResult => {
|
||||
return (
|
||||
editorFunctionCallResult.call_id ===
|
||||
existingEditorFunctionCallResult.call_id
|
||||
);
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
return {
|
||||
...editorFunctionCallResultsPerRequest,
|
||||
[aiRequestId]: [
|
||||
...existingEditorFunctionCallResults,
|
||||
...editorFunctionCallResults,
|
||||
],
|
||||
};
|
||||
}
|
||||
);
|
||||
},
|
||||
[]
|
||||
),
|
||||
clearEditorFunctionCallResults: React.useCallback((aiRequestId: string) => {
|
||||
setEditorFunctionCallResultsPerRequest(
|
||||
editorFunctionCallResultsPerRequest => ({
|
||||
...editorFunctionCallResultsPerRequest,
|
||||
[aiRequestId]: null,
|
||||
})
|
||||
);
|
||||
}, []),
|
||||
};
|
||||
};
|
||||
|
||||
type AiRequestStorage = {|
|
||||
aiRequests: { [string]: AiRequest },
|
||||
updateAiRequest: (aiRequestId: string, aiRequest: AiRequest) => void,
|
||||
refreshAiRequest: (aiRequestId: string) => Promise<void>,
|
||||
isSendingAiRequest: (aiRequestId: string | null) => boolean,
|
||||
getLastSendError: (aiRequestId: string | null) => ?Error,
|
||||
setSendingAiRequest: (aiRequestId: string | null, isSending: boolean) => void,
|
||||
setLastSendError: (aiRequestId: string | null, lastSendError: ?Error) => void,
|
||||
|};
|
||||
|
||||
type AiRequestSendState = {|
|
||||
isSending: boolean,
|
||||
lastSendError: ?Error,
|
||||
|};
|
||||
|
||||
export const useAiRequestsStorage = (): AiRequestStorage => {
|
||||
const { profile, getAuthorizationHeader } = React.useContext(
|
||||
AuthenticatedUserContext
|
||||
);
|
||||
|
||||
const [aiRequests, setAiRequests] = React.useState<{ [string]: AiRequest }>(
|
||||
{}
|
||||
);
|
||||
|
||||
const updateAiRequest = React.useCallback(
|
||||
(aiRequestId: string, aiRequest: AiRequest) => {
|
||||
setAiRequests(aiRequests => ({
|
||||
...aiRequests,
|
||||
[aiRequestId]: aiRequest,
|
||||
}));
|
||||
},
|
||||
[]
|
||||
);
|
||||
|
||||
const refreshAiRequest = React.useCallback(
|
||||
async (aiRequestId: string) => {
|
||||
if (!profile) return;
|
||||
|
||||
try {
|
||||
const updatedAiRequest = await getAiRequest(getAuthorizationHeader, {
|
||||
userId: profile.id,
|
||||
aiRequestId: aiRequestId,
|
||||
});
|
||||
updateAiRequest(updatedAiRequest.id, updatedAiRequest);
|
||||
} catch (error) {
|
||||
console.error(
|
||||
'Error while background refreshing AI request - ignoring:',
|
||||
error
|
||||
);
|
||||
}
|
||||
},
|
||||
[getAuthorizationHeader, profile, updateAiRequest]
|
||||
);
|
||||
|
||||
const [aiRequestSendStates, setAiRequestSendStates] = React.useState<{
|
||||
[string]: AiRequestSendState,
|
||||
}>({});
|
||||
const isSendingAiRequest = React.useCallback(
|
||||
(aiRequestId: string | null) =>
|
||||
!!aiRequestSendStates[aiRequestId || ''] &&
|
||||
aiRequestSendStates[aiRequestId || ''].isSending,
|
||||
[aiRequestSendStates]
|
||||
);
|
||||
const getLastSendError = React.useCallback(
|
||||
(aiRequestId: string | null) =>
|
||||
(aiRequestSendStates[aiRequestId || ''] &&
|
||||
aiRequestSendStates[aiRequestId || ''].lastSendError) ||
|
||||
null,
|
||||
[aiRequestSendStates]
|
||||
);
|
||||
const setSendingAiRequest = React.useCallback(
|
||||
(aiRequestId: string | null, isSending: boolean) => {
|
||||
const aiRequestIdToSet: string = aiRequestId || '';
|
||||
setAiRequestSendStates(aiRequestSendStates => ({
|
||||
...aiRequestSendStates,
|
||||
[aiRequestIdToSet]: {
|
||||
isSending,
|
||||
lastSendError: null,
|
||||
},
|
||||
}));
|
||||
},
|
||||
[setAiRequestSendStates]
|
||||
);
|
||||
const setLastSendError = React.useCallback(
|
||||
(aiRequestId: string | null, lastSendError: ?Error) => {
|
||||
const aiRequestIdToSet: string = aiRequestId || '';
|
||||
setAiRequestSendStates(aiRequestSendStates => ({
|
||||
...aiRequestSendStates,
|
||||
[aiRequestIdToSet]: {
|
||||
isSending: false,
|
||||
lastSendError,
|
||||
},
|
||||
}));
|
||||
},
|
||||
[setAiRequestSendStates]
|
||||
);
|
||||
|
||||
return {
|
||||
aiRequests,
|
||||
updateAiRequest,
|
||||
refreshAiRequest,
|
||||
isSendingAiRequest,
|
||||
setSendingAiRequest,
|
||||
setLastSendError,
|
||||
getLastSendError,
|
||||
};
|
||||
};
|
||||
|
||||
type AiRequestContextState = {|
|
||||
aiRequestStorage: AiRequestStorage,
|
||||
editorFunctionCallResultsStorage: EditorFunctionCallResultsStorage,
|
||||
|};
|
||||
|
||||
export const AiRequestContext = React.createContext<AiRequestContextState>({
|
||||
aiRequestStorage: {
|
||||
aiRequests: {},
|
||||
updateAiRequest: () => {},
|
||||
refreshAiRequest: async () => {},
|
||||
isSendingAiRequest: () => false,
|
||||
getLastSendError: () => null,
|
||||
setSendingAiRequest: () => {},
|
||||
setLastSendError: () => {},
|
||||
},
|
||||
editorFunctionCallResultsStorage: {
|
||||
getEditorFunctionCallResults: () => [],
|
||||
addEditorFunctionCallResults: () => {},
|
||||
clearEditorFunctionCallResults: () => {},
|
||||
},
|
||||
});
|
||||
|
||||
type AiRequestProviderProps = {|
|
||||
children: React.Node,
|
||||
|};
|
||||
|
||||
export const AiRequestProvider = ({ children }: AiRequestProviderProps) => {
|
||||
const editorFunctionCallResultsStorage = useEditorFunctionCallResultsStorage();
|
||||
const aiRequestStorage = useAiRequestsStorage();
|
||||
|
||||
const state = React.useMemo(
|
||||
() => ({
|
||||
aiRequestStorage,
|
||||
editorFunctionCallResultsStorage,
|
||||
}),
|
||||
[aiRequestStorage, editorFunctionCallResultsStorage]
|
||||
);
|
||||
|
||||
return (
|
||||
<AiRequestContext.Provider value={state}>
|
||||
{children}
|
||||
</AiRequestContext.Provider>
|
||||
);
|
||||
};
|
@@ -36,7 +36,7 @@ import { type EditorCallbacks } from '../EditorFunctions';
|
||||
import {
|
||||
getFunctionCallOutputsFromEditorFunctionCallResults,
|
||||
getFunctionCallsToProcess,
|
||||
} from './AiRequestUtils';
|
||||
} from './AiRequestChat/AiRequestUtils';
|
||||
import { useStableUpToDateRef } from '../Utils/UseStableUpToDateCallback';
|
||||
import { useTriggerAtNextRender } from '../Utils/useTriggerAtNextRender';
|
||||
import { type NewProjectSetup } from '../ProjectCreation/NewProjectSetupDialog';
|
||||
@@ -52,10 +52,66 @@ import {
|
||||
import { useCreateAiProjectDialog } from './UseCreateAiProjectDialog';
|
||||
import { type ExampleShortHeader } from '../Utils/GDevelopServices/Example';
|
||||
import { prepareAiUserContent } from './PrepareAiUserContent';
|
||||
import { AiRequestContext } from './AiRequestContext';
|
||||
|
||||
const gd: libGDevelop = global.gd;
|
||||
|
||||
const useEditorFunctionCallResultsPerRequest = () => {
|
||||
const [
|
||||
editorFunctionCallResultsPerRequest,
|
||||
setEditorFunctionCallResultsPerRequest,
|
||||
] = React.useState<{
|
||||
[aiRequestId: string]: Array<EditorFunctionCallResult>,
|
||||
}>({});
|
||||
|
||||
return {
|
||||
getEditorFunctionCallResults: React.useCallback(
|
||||
(aiRequestId: string): Array<EditorFunctionCallResult> | null =>
|
||||
editorFunctionCallResultsPerRequest[aiRequestId] || null,
|
||||
[editorFunctionCallResultsPerRequest]
|
||||
),
|
||||
addEditorFunctionCallResults: React.useCallback(
|
||||
(
|
||||
aiRequestId: string,
|
||||
editorFunctionCallResults: EditorFunctionCallResult[]
|
||||
) => {
|
||||
setEditorFunctionCallResultsPerRequest(
|
||||
editorFunctionCallResultsPerRequest => {
|
||||
const existingEditorFunctionCallResults = (
|
||||
editorFunctionCallResultsPerRequest[aiRequestId] || []
|
||||
).filter(existingEditorFunctionCallResult => {
|
||||
return !editorFunctionCallResults.some(
|
||||
editorFunctionCallResult => {
|
||||
return (
|
||||
editorFunctionCallResult.call_id ===
|
||||
existingEditorFunctionCallResult.call_id
|
||||
);
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
return {
|
||||
...editorFunctionCallResultsPerRequest,
|
||||
[aiRequestId]: [
|
||||
...existingEditorFunctionCallResults,
|
||||
...editorFunctionCallResults,
|
||||
],
|
||||
};
|
||||
}
|
||||
);
|
||||
},
|
||||
[]
|
||||
),
|
||||
clearEditorFunctionCallResults: React.useCallback((aiRequestId: string) => {
|
||||
setEditorFunctionCallResultsPerRequest(
|
||||
editorFunctionCallResultsPerRequest => ({
|
||||
...editorFunctionCallResultsPerRequest,
|
||||
[aiRequestId]: null,
|
||||
})
|
||||
);
|
||||
}, []),
|
||||
};
|
||||
};
|
||||
|
||||
const useProcessFunctionCalls = ({
|
||||
i18n,
|
||||
project,
|
||||
@@ -73,9 +129,7 @@ const useProcessFunctionCalls = ({
|
||||
resourceManagementProps: ResourceManagementProps,
|
||||
editorCallbacks: EditorCallbacks,
|
||||
selectedAiRequest: ?AiRequest,
|
||||
onSendEditorFunctionCallResults: (
|
||||
options: null | {| createdSceneNames: Array<string> |}
|
||||
) => Promise<void>,
|
||||
onSendEditorFunctionCallResults: () => Promise<void>,
|
||||
getEditorFunctionCallResults: string => Array<EditorFunctionCallResult> | null,
|
||||
addEditorFunctionCallResults: (
|
||||
string,
|
||||
@@ -142,7 +196,7 @@ const useProcessFunctionCalls = ({
|
||||
}))
|
||||
);
|
||||
|
||||
const { results, createdSceneNames } = await processEditorFunctionCalls({
|
||||
const editorFunctionCallResults = await processEditorFunctionCalls({
|
||||
project,
|
||||
editorCallbacks,
|
||||
functionCalls: functionCalls.map(functionCall => ({
|
||||
@@ -162,13 +216,14 @@ const useProcessFunctionCalls = ({
|
||||
searchAndInstallAsset,
|
||||
});
|
||||
|
||||
addEditorFunctionCallResults(selectedAiRequest.id, results);
|
||||
addEditorFunctionCallResults(
|
||||
selectedAiRequest.id,
|
||||
editorFunctionCallResults
|
||||
);
|
||||
|
||||
// We may have processed everything, so try to send the results
|
||||
// to the backend.
|
||||
triggerSendEditorFunctionCallResults({
|
||||
createdSceneNames,
|
||||
});
|
||||
triggerSendEditorFunctionCallResults();
|
||||
},
|
||||
[
|
||||
project,
|
||||
@@ -225,20 +280,52 @@ const useProcessFunctionCalls = ({
|
||||
};
|
||||
};
|
||||
|
||||
export const useSelectedAiRequest = ({
|
||||
initialAiRequestId,
|
||||
}: {|
|
||||
initialAiRequestId: string | null,
|
||||
|}) => {
|
||||
type AiRequestSendState = {|
|
||||
isSending: boolean,
|
||||
lastSendError: ?Error,
|
||||
|};
|
||||
|
||||
export const useAiRequests = () => {
|
||||
const { profile, getAuthorizationHeader } = React.useContext(
|
||||
AuthenticatedUserContext
|
||||
);
|
||||
const { aiRequestStorage } = React.useContext(AiRequestContext);
|
||||
const { aiRequests, updateAiRequest } = aiRequestStorage;
|
||||
|
||||
const [aiRequests, setAiRequests] = React.useState<{ [string]: AiRequest }>(
|
||||
{}
|
||||
);
|
||||
const [selectedAiRequestId, setSelectedAiRequestId] = React.useState<
|
||||
string | null
|
||||
>(initialAiRequestId);
|
||||
>(null);
|
||||
|
||||
const updateAiRequest = React.useCallback(
|
||||
(aiRequestId: string, aiRequest: AiRequest) => {
|
||||
setAiRequests(aiRequests => ({
|
||||
...aiRequests,
|
||||
[aiRequestId]: aiRequest,
|
||||
}));
|
||||
},
|
||||
[]
|
||||
);
|
||||
|
||||
const refreshAiRequest = React.useCallback(
|
||||
async (aiRequestId: string) => {
|
||||
if (!profile) return;
|
||||
|
||||
try {
|
||||
const updatedAiRequest = await getAiRequest(getAuthorizationHeader, {
|
||||
userId: profile.id,
|
||||
aiRequestId: aiRequestId,
|
||||
});
|
||||
updateAiRequest(updatedAiRequest.id, updatedAiRequest);
|
||||
} catch (error) {
|
||||
console.error(
|
||||
'Error while background refreshing AI request - ignoring:',
|
||||
error
|
||||
);
|
||||
}
|
||||
},
|
||||
[getAuthorizationHeader, profile, updateAiRequest]
|
||||
);
|
||||
|
||||
const selectedAiRequest =
|
||||
(selectedAiRequestId && aiRequests[selectedAiRequestId]) || null;
|
||||
@@ -288,10 +375,59 @@ export const useSelectedAiRequest = ({
|
||||
]
|
||||
);
|
||||
|
||||
const [aiRequestSendStates, setAiRequestSendStates] = React.useState<{
|
||||
[string]: AiRequestSendState,
|
||||
}>({});
|
||||
const isSendingAiRequest = React.useCallback(
|
||||
(aiRequestId: string | null) =>
|
||||
!!aiRequestSendStates[aiRequestId || ''] &&
|
||||
aiRequestSendStates[aiRequestId || ''].isSending,
|
||||
[aiRequestSendStates]
|
||||
);
|
||||
const getLastSendError = React.useCallback(
|
||||
(aiRequestId: string | null) =>
|
||||
(aiRequestSendStates[aiRequestId || ''] &&
|
||||
aiRequestSendStates[aiRequestId || ''].lastSendError) ||
|
||||
null,
|
||||
[aiRequestSendStates]
|
||||
);
|
||||
const setSendingAiRequest = React.useCallback(
|
||||
(aiRequestId: string | null, isSending: boolean) => {
|
||||
const aiRequestIdToSet: string = aiRequestId || '';
|
||||
setAiRequestSendStates(aiRequestSendStates => ({
|
||||
...aiRequestSendStates,
|
||||
[aiRequestIdToSet]: {
|
||||
isSending,
|
||||
lastSendError: null,
|
||||
},
|
||||
}));
|
||||
},
|
||||
[setAiRequestSendStates]
|
||||
);
|
||||
const setLastSendError = React.useCallback(
|
||||
(aiRequestId: string | null, lastSendError: ?Error) => {
|
||||
const aiRequestIdToSet: string = aiRequestId || '';
|
||||
setAiRequestSendStates(aiRequestSendStates => ({
|
||||
...aiRequestSendStates,
|
||||
[aiRequestIdToSet]: {
|
||||
isSending: false,
|
||||
lastSendError,
|
||||
},
|
||||
}));
|
||||
},
|
||||
[setAiRequestSendStates]
|
||||
);
|
||||
|
||||
return {
|
||||
selectedAiRequest,
|
||||
selectedAiRequestId,
|
||||
setSelectedAiRequestId,
|
||||
updateAiRequest,
|
||||
refreshAiRequest,
|
||||
isSendingAiRequest,
|
||||
setSendingAiRequest,
|
||||
setLastSendError,
|
||||
getLastSendError,
|
||||
};
|
||||
};
|
||||
|
||||
@@ -302,8 +438,6 @@ const styles = {
|
||||
flexDirection: 'column',
|
||||
alignItems: 'center',
|
||||
minWidth: 0,
|
||||
overflowY: 'scroll',
|
||||
overflowX: 'hidden',
|
||||
},
|
||||
chatContainer: {
|
||||
flex: 1,
|
||||
@@ -349,13 +483,6 @@ type Props = {|
|
||||
changes: SceneEventsOutsideEditorChanges
|
||||
) => void,
|
||||
onExtensionInstalled: (extensionNames: Array<string>) => void,
|
||||
initialMode: 'chat' | 'agent' | null,
|
||||
initialAiRequestId: string | null,
|
||||
onOpenAskAi: ({|
|
||||
mode: 'chat' | 'agent',
|
||||
aiRequestId: string | null,
|
||||
paneIdentifier: 'left' | 'center' | 'right' | null,
|
||||
|}) => void,
|
||||
|};
|
||||
|
||||
export type AskAiEditorInterface = {|
|
||||
@@ -371,10 +498,7 @@ export type AskAiEditorInterface = {|
|
||||
onSceneEventsModifiedOutsideEditor: (
|
||||
changes: SceneEventsOutsideEditorChanges
|
||||
) => void,
|
||||
startOrOpenChat: ({|
|
||||
mode: 'chat' | 'agent',
|
||||
aiRequestId: string | null,
|
||||
|}) => void,
|
||||
startNewChat: () => void,
|
||||
|};
|
||||
|
||||
export type NewAiRequestOptions = {|
|
||||
@@ -400,9 +524,6 @@ export const AskAiEditor = React.memo<Props>(
|
||||
onOpenLayout,
|
||||
onSceneEventsModifiedOutsideEditor,
|
||||
onExtensionInstalled,
|
||||
initialMode,
|
||||
initialAiRequestId,
|
||||
onOpenAskAi,
|
||||
}: Props,
|
||||
ref
|
||||
) => {
|
||||
@@ -417,9 +538,13 @@ export const AskAiEditor = React.memo<Props>(
|
||||
selectedAiRequest,
|
||||
selectedAiRequestId,
|
||||
setSelectedAiRequestId,
|
||||
} = useSelectedAiRequest({
|
||||
initialAiRequestId,
|
||||
});
|
||||
updateAiRequest,
|
||||
refreshAiRequest,
|
||||
setSendingAiRequest,
|
||||
isSendingAiRequest,
|
||||
getLastSendError,
|
||||
setLastSendError,
|
||||
} = useAiRequests();
|
||||
const upToDateSelectedAiRequestId = useStableUpToDateRef(
|
||||
selectedAiRequestId
|
||||
);
|
||||
@@ -430,31 +555,11 @@ export const AskAiEditor = React.memo<Props>(
|
||||
] = React.useState<NewAiRequestOptions | null>(null);
|
||||
|
||||
const [isHistoryOpen, setIsHistoryOpen] = React.useState<boolean>(false);
|
||||
const [newChatMode, setNewChatMode] = React.useState<'chat' | 'agent'>(
|
||||
initialMode || 'agent'
|
||||
);
|
||||
|
||||
// Update newChatMode when mode prop changes
|
||||
React.useEffect(
|
||||
() => {
|
||||
if (initialMode) {
|
||||
setNewChatMode(initialMode);
|
||||
}
|
||||
},
|
||||
[initialMode]
|
||||
);
|
||||
|
||||
const canStartNewChat = !!selectedAiRequestId;
|
||||
const onStartOrOpenChat = React.useCallback(
|
||||
({
|
||||
mode,
|
||||
aiRequestId,
|
||||
}: {|
|
||||
mode: 'chat' | 'agent',
|
||||
aiRequestId: string | null,
|
||||
|}) => {
|
||||
setSelectedAiRequestId(aiRequestId);
|
||||
setNewChatMode(mode);
|
||||
const onStartNewChat = React.useCallback(
|
||||
() => {
|
||||
setSelectedAiRequestId(null);
|
||||
},
|
||||
[setSelectedAiRequestId]
|
||||
);
|
||||
@@ -467,23 +572,11 @@ export const AskAiEditor = React.memo<Props>(
|
||||
setIsHistoryOpen(false);
|
||||
}, []);
|
||||
|
||||
const {
|
||||
aiRequestStorage,
|
||||
editorFunctionCallResultsStorage,
|
||||
} = React.useContext(AiRequestContext);
|
||||
const {
|
||||
getEditorFunctionCallResults,
|
||||
addEditorFunctionCallResults,
|
||||
clearEditorFunctionCallResults,
|
||||
} = editorFunctionCallResultsStorage;
|
||||
const {
|
||||
updateAiRequest,
|
||||
refreshAiRequest,
|
||||
isSendingAiRequest,
|
||||
getLastSendError,
|
||||
setSendingAiRequest,
|
||||
setLastSendError,
|
||||
} = aiRequestStorage;
|
||||
} = useEditorFunctionCallResultsPerRequest();
|
||||
|
||||
const {
|
||||
createAiProject,
|
||||
@@ -495,14 +588,14 @@ export const AskAiEditor = React.memo<Props>(
|
||||
if (setToolbar) {
|
||||
setToolbar(
|
||||
<Toolbar
|
||||
onStartOrOpenChat={onStartOrOpenChat}
|
||||
onStartNewChat={onStartNewChat}
|
||||
canStartNewChat={canStartNewChat}
|
||||
onOpenHistory={onOpenHistory}
|
||||
/>
|
||||
);
|
||||
}
|
||||
},
|
||||
[setToolbar, onStartOrOpenChat, canStartNewChat, onOpenHistory]
|
||||
[setToolbar, onStartNewChat, canStartNewChat, onOpenHistory]
|
||||
);
|
||||
|
||||
React.useEffect(updateToolbar, [updateToolbar]);
|
||||
@@ -515,7 +608,7 @@ export const AskAiEditor = React.memo<Props>(
|
||||
onSceneObjectEdited: noop,
|
||||
onSceneObjectsDeleted: noop,
|
||||
onSceneEventsModifiedOutsideEditor: noop,
|
||||
startOrOpenChat: onStartOrOpenChat,
|
||||
startNewChat: onStartNewChat,
|
||||
}));
|
||||
|
||||
const aiRequestChatRef = React.useRef<AiRequestChatInterface | null>(
|
||||
@@ -709,7 +802,6 @@ export const AskAiEditor = React.memo<Props>(
|
||||
updateAiRequest,
|
||||
createAiProject,
|
||||
newAiRequestOptions,
|
||||
onOpenAskAi,
|
||||
]
|
||||
);
|
||||
|
||||
@@ -728,13 +820,7 @@ export const AskAiEditor = React.memo<Props>(
|
||||
|
||||
// Send the results of the function call outputs, if any, and the user message (if any).
|
||||
const onSendMessage = React.useCallback(
|
||||
async ({
|
||||
userMessage,
|
||||
createdSceneNames,
|
||||
}: {|
|
||||
userMessage: string,
|
||||
createdSceneNames?: Array<string>,
|
||||
|}) => {
|
||||
async ({ userMessage }: {| userMessage: string |}) => {
|
||||
if (
|
||||
!profile ||
|
||||
!selectedAiRequestId ||
|
||||
@@ -846,25 +932,9 @@ export const AskAiEditor = React.memo<Props>(
|
||||
// Ignore limits refresh error.
|
||||
}
|
||||
}
|
||||
|
||||
if (selectedAiRequest && createdSceneNames) {
|
||||
onOpenAskAi({
|
||||
mode: selectedAiRequest.mode || 'agent',
|
||||
aiRequestId: selectedAiRequestId,
|
||||
paneIdentifier: 'right',
|
||||
});
|
||||
createdSceneNames.forEach(sceneName => {
|
||||
onOpenLayout(sceneName, {
|
||||
openEventsEditor: true,
|
||||
openSceneEditor: true,
|
||||
focusWhenOpened: 'scene',
|
||||
});
|
||||
});
|
||||
}
|
||||
},
|
||||
[
|
||||
profile,
|
||||
selectedAiRequest,
|
||||
selectedAiRequestId,
|
||||
isSendingAiRequest,
|
||||
getEditorFunctionCallResults,
|
||||
@@ -880,15 +950,12 @@ export const AskAiEditor = React.memo<Props>(
|
||||
onRefreshLimits,
|
||||
project,
|
||||
hasFunctionsCallsToProcess,
|
||||
onOpenAskAi,
|
||||
onOpenLayout,
|
||||
]
|
||||
);
|
||||
const onSendEditorFunctionCallResults = React.useCallback(
|
||||
(options: null | {| createdSceneNames: Array<string> |}) =>
|
||||
() =>
|
||||
onSendMessage({
|
||||
userMessage: '',
|
||||
createdSceneNames: options ? options.createdSceneNames : [],
|
||||
}),
|
||||
[onSendMessage]
|
||||
);
|
||||
@@ -972,7 +1039,6 @@ export const AskAiEditor = React.memo<Props>(
|
||||
? isAutoProcessingFunctionCalls(selectedAiRequest.id)
|
||||
: false
|
||||
}
|
||||
initialMode={newChatMode}
|
||||
setAutoProcessFunctionCalls={shouldAutoProcess => {
|
||||
if (!selectedAiRequest) return;
|
||||
setAutoProcessFunctionCalls(
|
||||
@@ -982,7 +1048,7 @@ export const AskAiEditor = React.memo<Props>(
|
||||
}}
|
||||
i18n={i18n}
|
||||
editorCallbacks={editorCallbacks}
|
||||
onStartOrOpenChat={onStartOrOpenChat}
|
||||
onStartNewChat={onStartNewChat}
|
||||
/>
|
||||
</div>
|
||||
</Paper>
|
||||
@@ -1031,13 +1097,6 @@ export const renderAskAiEditorContainer = (
|
||||
props.onSceneEventsModifiedOutsideEditor
|
||||
}
|
||||
onExtensionInstalled={props.onExtensionInstalled}
|
||||
initialMode={
|
||||
(props.extraEditorProps && props.extraEditorProps.mode) || null
|
||||
}
|
||||
initialAiRequestId={
|
||||
(props.extraEditorProps && props.extraEditorProps.aiRequestId) || null
|
||||
}
|
||||
onOpenAskAi={props.onOpenAskAi}
|
||||
/>
|
||||
)}
|
||||
</I18n>
|
||||
|
@@ -247,7 +247,7 @@ export const AskAiHistory = ({
|
||||
return (
|
||||
<Drawer
|
||||
open={open}
|
||||
anchor="right"
|
||||
anchor="left"
|
||||
onClose={onClose}
|
||||
PaperProps={{
|
||||
style: styles.drawer,
|
||||
@@ -260,7 +260,6 @@ export const AskAiHistory = ({
|
||||
<ColumnStackLayout expand noMargin>
|
||||
<DrawerTopBar
|
||||
title={<Trans>AI Chat History</Trans>}
|
||||
drawerAnchor="right"
|
||||
id="ai-chat-history-drawer-top-bar"
|
||||
onClose={onClose}
|
||||
/>
|
||||
|
@@ -3,21 +3,18 @@ import { Trans, t } from '@lingui/macro';
|
||||
import * as React from 'react';
|
||||
import { ToolbarGroup } from '../UI/Toolbar';
|
||||
import AddIcon from '../UI/CustomSvgIcons/Add';
|
||||
import ChatBubblesIcon from '../UI/CustomSvgIcons/ChatBubbles';
|
||||
import ResponsiveRaisedButton from '../UI/ResponsiveRaisedButton';
|
||||
import HistoryIcon from '../UI/CustomSvgIcons/History';
|
||||
import RaisedButton from '../UI/RaisedButton';
|
||||
import IconButton from '../UI/IconButton';
|
||||
|
||||
type Props = {|
|
||||
onStartOrOpenChat: ({|
|
||||
mode: 'chat' | 'agent',
|
||||
aiRequestId: string | null,
|
||||
|}) => void,
|
||||
onStartNewChat: () => void,
|
||||
canStartNewChat: boolean,
|
||||
onOpenHistory: () => void,
|
||||
|};
|
||||
|
||||
export const Toolbar = ({
|
||||
onStartOrOpenChat,
|
||||
onStartNewChat,
|
||||
canStartNewChat,
|
||||
onOpenHistory,
|
||||
}: Props) => {
|
||||
@@ -30,24 +27,16 @@ export const Toolbar = ({
|
||||
tooltip={t`View history`}
|
||||
onClick={onOpenHistory}
|
||||
>
|
||||
<ChatBubblesIcon />
|
||||
<HistoryIcon />
|
||||
</IconButton>
|
||||
</ToolbarGroup>
|
||||
<ToolbarGroup lastChild>
|
||||
<ResponsiveRaisedButton
|
||||
<RaisedButton
|
||||
primary
|
||||
onClick={() =>
|
||||
onStartOrOpenChat({
|
||||
mode: 'agent',
|
||||
aiRequestId: null,
|
||||
})
|
||||
}
|
||||
onClick={onStartNewChat}
|
||||
icon={<AddIcon />}
|
||||
label={<Trans>Start a new chat</Trans>}
|
||||
disabled={!canStartNewChat}
|
||||
style={{
|
||||
flexShrink: 0,
|
||||
}}
|
||||
/>
|
||||
</ToolbarGroup>
|
||||
</>
|
||||
|
@@ -9,7 +9,6 @@ import {
|
||||
import {
|
||||
type PrivateAssetPackListingData,
|
||||
type PrivateGameTemplateListingData,
|
||||
type BundleListingData,
|
||||
} from '../Utils/GDevelopServices/Shop';
|
||||
|
||||
export type AssetStorePageState = {|
|
||||
@@ -18,7 +17,6 @@ export type AssetStorePageState = {|
|
||||
openedAssetShortHeader: ?AssetShortHeader,
|
||||
openedPrivateAssetPackListingData: ?PrivateAssetPackListingData,
|
||||
openedPrivateGameTemplateListingData: ?PrivateGameTemplateListingData,
|
||||
openedBundleListingData: ?BundleListingData,
|
||||
selectedFolders: Array<string>,
|
||||
filtersState: FiltersState,
|
||||
pageBreakIndex?: ?number,
|
||||
@@ -55,11 +53,6 @@ export type NavigationState = {|
|
||||
storeSearchText: boolean,
|
||||
clearSearchText: boolean,
|
||||
|}) => void,
|
||||
openBundleInformationPage: ({|
|
||||
bundleListingData: BundleListingData,
|
||||
storeSearchText: boolean,
|
||||
clearSearchText: boolean,
|
||||
|}) => void,
|
||||
openAssetDetailPage: ({|
|
||||
assetShortHeader: AssetShortHeader,
|
||||
storeSearchText: boolean,
|
||||
@@ -83,7 +76,6 @@ export const assetStoreHomePageState: AssetStorePageState = {
|
||||
openedAssetPack: null,
|
||||
openedPrivateAssetPackListingData: null,
|
||||
openedPrivateGameTemplateListingData: null,
|
||||
openedBundleListingData: null,
|
||||
selectedFolders: [],
|
||||
filtersState: noFilter,
|
||||
displayAssets: false,
|
||||
@@ -95,7 +87,6 @@ const searchPageState: AssetStorePageState = {
|
||||
openedAssetPack: null,
|
||||
openedPrivateAssetPackListingData: null,
|
||||
openedPrivateGameTemplateListingData: null,
|
||||
openedBundleListingData: null,
|
||||
selectedFolders: [],
|
||||
filtersState: noFilter,
|
||||
displayAssets: true,
|
||||
@@ -108,7 +99,6 @@ export const isHomePage = (pageState: AssetStorePageState) => {
|
||||
!pageState.openedPrivateAssetPackListingData &&
|
||||
!pageState.openedPrivateGameTemplateListingData &&
|
||||
!pageState.openedAssetPack &&
|
||||
!pageState.openedBundleListingData &&
|
||||
pageState.filtersState === noFilter &&
|
||||
!pageState.displayAssets)
|
||||
);
|
||||
@@ -119,8 +109,7 @@ export const isSearchResultPage = (pageState: AssetStorePageState) => {
|
||||
!isHomePage(pageState) &&
|
||||
!pageState.openedAssetShortHeader &&
|
||||
!pageState.openedPrivateAssetPackListingData &&
|
||||
!pageState.openedPrivateGameTemplateListingData &&
|
||||
!pageState.openedBundleListingData
|
||||
!pageState.openedPrivateGameTemplateListingData
|
||||
);
|
||||
};
|
||||
|
||||
@@ -144,7 +133,6 @@ export const AssetStoreNavigatorContext = React.createContext<NavigationState>({
|
||||
openPackPage: () => {},
|
||||
openPrivateAssetPackInformationPage: () => {},
|
||||
openPrivateGameTemplateInformationPage: () => {},
|
||||
openBundleInformationPage: () => {},
|
||||
openAssetDetailPage: () => {},
|
||||
navigateInsideFolder: string => {},
|
||||
goBackToFolderIndex: number => {},
|
||||
@@ -181,12 +169,7 @@ export const AssetStoreNavigatorStateProvider = (
|
||||
});
|
||||
return newCurrentPage;
|
||||
}
|
||||
if (previousPages.length === 1) {
|
||||
// If we are already on the root page, do nothing.
|
||||
return previousPages[0];
|
||||
}
|
||||
// If there are no previous pages, return the home page state.
|
||||
return assetStoreHomePageState;
|
||||
return previousPages[0];
|
||||
},
|
||||
openHome: () => {
|
||||
setHistory({ previousPages: [assetStoreHomePageState] });
|
||||
@@ -261,7 +244,6 @@ export const AssetStoreNavigatorStateProvider = (
|
||||
openedAssetPack: null,
|
||||
openedPrivateAssetPackListingData: null,
|
||||
openedPrivateGameTemplateListingData: null,
|
||||
openedBundleListingData: null,
|
||||
displayAssets: true,
|
||||
filtersState: {
|
||||
chosenCategory: {
|
||||
@@ -289,7 +271,6 @@ export const AssetStoreNavigatorStateProvider = (
|
||||
openedAssetPack: null,
|
||||
openedPrivateAssetPackListingData: null,
|
||||
openedPrivateGameTemplateListingData: null,
|
||||
openedBundleListingData: null,
|
||||
filtersState: noFilter,
|
||||
displayAssets: false,
|
||||
selectedFolders: [],
|
||||
@@ -334,7 +315,6 @@ export const AssetStoreNavigatorStateProvider = (
|
||||
openedAssetPack: assetPack,
|
||||
openedPrivateAssetPackListingData: null,
|
||||
openedPrivateGameTemplateListingData: null,
|
||||
openedBundleListingData: null,
|
||||
displayAssets: true,
|
||||
filtersState: {
|
||||
chosenCategory: {
|
||||
@@ -393,7 +373,6 @@ export const AssetStoreNavigatorStateProvider = (
|
||||
openedAssetPack: null,
|
||||
openedPrivateAssetPackListingData: privateAssetPackListingData,
|
||||
openedPrivateGameTemplateListingData: null,
|
||||
openedBundleListingData: null,
|
||||
filtersState: noFilter,
|
||||
displayAssets: false,
|
||||
selectedFolders: [],
|
||||
@@ -439,7 +418,6 @@ export const AssetStoreNavigatorStateProvider = (
|
||||
openedAssetPack: null,
|
||||
openedPrivateAssetPackListingData: null,
|
||||
openedPrivateGameTemplateListingData: null,
|
||||
openedBundleListingData: null,
|
||||
filtersState: noFilter,
|
||||
displayAssets: false,
|
||||
selectedFolders: [],
|
||||
@@ -485,53 +463,6 @@ export const AssetStoreNavigatorStateProvider = (
|
||||
openedAssetPack: null,
|
||||
openedPrivateAssetPackListingData: null,
|
||||
openedPrivateGameTemplateListingData: privateGameTemplateListingData,
|
||||
openedBundleListingData: null,
|
||||
filtersState: noFilter,
|
||||
displayAssets: false,
|
||||
selectedFolders: [],
|
||||
},
|
||||
],
|
||||
};
|
||||
});
|
||||
if (clearSearchText) setSearchText('');
|
||||
},
|
||||
openBundleInformationPage: ({
|
||||
bundleListingData,
|
||||
storeSearchText,
|
||||
clearSearchText,
|
||||
}: {|
|
||||
bundleListingData: BundleListingData,
|
||||
storeSearchText: boolean,
|
||||
clearSearchText: boolean,
|
||||
|}) => {
|
||||
setHistory(previousHistory => {
|
||||
const currentPage =
|
||||
previousHistory.previousPages[
|
||||
previousHistory.previousPages.length - 1
|
||||
];
|
||||
const currentPageWithSearchText = {
|
||||
...currentPage,
|
||||
searchText: storeSearchText ? searchText : '',
|
||||
};
|
||||
const previousPagesWithoutCurrentPage = previousHistory.previousPages.slice(
|
||||
0,
|
||||
previousHistory.previousPages.length - 1
|
||||
);
|
||||
const previousPages = [
|
||||
...previousPagesWithoutCurrentPage,
|
||||
currentPageWithSearchText,
|
||||
];
|
||||
return {
|
||||
...previousHistory,
|
||||
previousPages: [
|
||||
...previousPages,
|
||||
{
|
||||
openedAssetShortHeader: null,
|
||||
openedShopCategory: null,
|
||||
openedAssetPack: null,
|
||||
openedPrivateAssetPackListingData: null,
|
||||
openedPrivateGameTemplateListingData: null,
|
||||
openedBundleListingData: bundleListingData,
|
||||
filtersState: noFilter,
|
||||
displayAssets: false,
|
||||
selectedFolders: [],
|
||||
|
@@ -1,5 +1,4 @@
|
||||
// @flow
|
||||
import * as React from 'react';
|
||||
import {
|
||||
type PublicAssetPacks,
|
||||
type PrivateAssetPack,
|
||||
@@ -8,18 +7,7 @@ import {
|
||||
import {
|
||||
type PrivateAssetPackListingData,
|
||||
type PrivateGameTemplateListingData,
|
||||
type BundleListingData,
|
||||
getArchivedBundleListingData,
|
||||
getArchivedPrivateGameTemplateListingData,
|
||||
getArchivedPrivateAssetPackListingData,
|
||||
} from '../Utils/GDevelopServices/Shop';
|
||||
import {
|
||||
PrivateAssetPackTile,
|
||||
PublicAssetPackTile,
|
||||
PrivateGameTemplateTile,
|
||||
BundleTile,
|
||||
} from './ShopTiles';
|
||||
import { mergeArraysPerGroup } from '../Utils/Array';
|
||||
|
||||
/**
|
||||
* A simple slug generator that allows to link to asset packs on
|
||||
@@ -131,366 +119,3 @@ export const getPrivateGameTemplateListingDataFromUserFriendlySlug = ({
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
export const getBundleListingDataFromUserFriendlySlug = ({
|
||||
bundleListingDatas,
|
||||
userFriendlySlug,
|
||||
}: {|
|
||||
bundleListingDatas: Array<BundleListingData>,
|
||||
userFriendlySlug: string,
|
||||
|}): ?BundleListingData => {
|
||||
const bundleId = getIdFromPrivateProductUserFriendlySlug(userFriendlySlug);
|
||||
const bundleListingData = bundleListingDatas.find(
|
||||
bundle => bundleId === bundle.id
|
||||
);
|
||||
if (bundleListingData) return bundleListingData;
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
export const getBundleTiles = ({
|
||||
allBundleListingDatas,
|
||||
displayedBundleListingDatas,
|
||||
onBundleSelection,
|
||||
receivedBundles,
|
||||
openedShopCategory,
|
||||
hasAssetFiltersApplied,
|
||||
}: {|
|
||||
allBundleListingDatas: ?Array<BundleListingData>,
|
||||
displayedBundleListingDatas: ?Array<BundleListingData>,
|
||||
onBundleSelection: ?(BundleListingData) => void,
|
||||
receivedBundles: ?Array<any>,
|
||||
openedShopCategory?: ?string,
|
||||
hasAssetFiltersApplied?: boolean,
|
||||
|}): Array<React.Node> => {
|
||||
if (
|
||||
!allBundleListingDatas ||
|
||||
!displayedBundleListingDatas ||
|
||||
!onBundleSelection ||
|
||||
hasAssetFiltersApplied
|
||||
)
|
||||
return [];
|
||||
|
||||
const bundleTiles: Array<React.Node> = [];
|
||||
const ownedBundleTiles: Array<React.Node> = [];
|
||||
|
||||
displayedBundleListingDatas
|
||||
.filter(
|
||||
bundleListingData =>
|
||||
!openedShopCategory ||
|
||||
bundleListingData.categories.includes(openedShopCategory)
|
||||
)
|
||||
.forEach(bundleListingData => {
|
||||
const isBundleOwned =
|
||||
!!receivedBundles &&
|
||||
!!receivedBundles.find(bundle => bundle.id === bundleListingData.id);
|
||||
const tile = (
|
||||
<BundleTile
|
||||
bundleListingData={bundleListingData}
|
||||
onSelect={() => {
|
||||
onBundleSelection(bundleListingData);
|
||||
}}
|
||||
owned={isBundleOwned}
|
||||
key={bundleListingData.id}
|
||||
/>
|
||||
);
|
||||
if (isBundleOwned) {
|
||||
ownedBundleTiles.push(tile);
|
||||
} else {
|
||||
bundleTiles.push(tile);
|
||||
}
|
||||
});
|
||||
|
||||
// Handle archived bundles that are owned by the user.
|
||||
// These are bundles that are not listed in the shop anymore,
|
||||
// but that the user has already purchased.
|
||||
const archivedOwnedBundleTiles = (receivedBundles || [])
|
||||
.filter(
|
||||
bundle =>
|
||||
!allBundleListingDatas.find(
|
||||
bundleListingData => bundleListingData.id === bundle.id
|
||||
)
|
||||
)
|
||||
.map(bundle => {
|
||||
const archivedBundleListingData = getArchivedBundleListingData({
|
||||
bundle,
|
||||
});
|
||||
return (
|
||||
<BundleTile
|
||||
bundleListingData={archivedBundleListingData}
|
||||
onSelect={() => {
|
||||
onBundleSelection(archivedBundleListingData);
|
||||
}}
|
||||
owned={true}
|
||||
key={bundle.id}
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
||||
return [...ownedBundleTiles, ...archivedOwnedBundleTiles, ...bundleTiles];
|
||||
};
|
||||
|
||||
export const getGameTemplateTiles = ({
|
||||
allPrivateGameTemplateListingDatas,
|
||||
displayedPrivateGameTemplateListingDatas,
|
||||
onPrivateGameTemplateSelection,
|
||||
receivedGameTemplates,
|
||||
openedShopCategory,
|
||||
hasAssetFiltersApplied,
|
||||
hasAssetPackFiltersApplied,
|
||||
onlyShowAssets,
|
||||
}: {|
|
||||
allPrivateGameTemplateListingDatas: ?Array<PrivateGameTemplateListingData>,
|
||||
displayedPrivateGameTemplateListingDatas: ?Array<PrivateGameTemplateListingData>,
|
||||
onPrivateGameTemplateSelection: ?(PrivateGameTemplateListingData) => void,
|
||||
receivedGameTemplates: ?Array<any>,
|
||||
openedShopCategory?: ?string,
|
||||
hasAssetFiltersApplied?: boolean,
|
||||
hasAssetPackFiltersApplied?: boolean,
|
||||
onlyShowAssets?: boolean,
|
||||
|}): Array<React.Node> => {
|
||||
if (
|
||||
!allPrivateGameTemplateListingDatas ||
|
||||
!displayedPrivateGameTemplateListingDatas ||
|
||||
!onPrivateGameTemplateSelection ||
|
||||
hasAssetFiltersApplied ||
|
||||
hasAssetPackFiltersApplied ||
|
||||
onlyShowAssets
|
||||
)
|
||||
return [];
|
||||
|
||||
const gameTemplateTiles: Array<React.Node> = [];
|
||||
const ownedGameTemplateTiles: Array<React.Node> = [];
|
||||
|
||||
const filteredGameTemplates = displayedPrivateGameTemplateListingDatas.filter(
|
||||
privateGameTemplateListingData =>
|
||||
!openedShopCategory || openedShopCategory === 'game-template'
|
||||
);
|
||||
|
||||
filteredGameTemplates.forEach(privateGameTemplateListingData => {
|
||||
const isGameTemplateOwned =
|
||||
!!receivedGameTemplates &&
|
||||
!!receivedGameTemplates.find(
|
||||
pack => pack.id === privateGameTemplateListingData.id
|
||||
);
|
||||
const tile = (
|
||||
<PrivateGameTemplateTile
|
||||
privateGameTemplateListingData={privateGameTemplateListingData}
|
||||
onSelect={() => {
|
||||
onPrivateGameTemplateSelection(privateGameTemplateListingData);
|
||||
}}
|
||||
owned={isGameTemplateOwned}
|
||||
key={privateGameTemplateListingData.id}
|
||||
/>
|
||||
);
|
||||
|
||||
if (isGameTemplateOwned) {
|
||||
ownedGameTemplateTiles.push(tile);
|
||||
} else {
|
||||
gameTemplateTiles.push(tile);
|
||||
}
|
||||
});
|
||||
|
||||
// Handle archived game templates that are owned by the user.
|
||||
// These are game templates that are not listed in the shop anymore,
|
||||
// but that the user has already purchased.
|
||||
const archivedOwnedGameTemplateTiles = (receivedGameTemplates || [])
|
||||
.filter(
|
||||
gameTemplate =>
|
||||
!allPrivateGameTemplateListingDatas.find(
|
||||
privateGameTemplateListingData =>
|
||||
privateGameTemplateListingData.id === gameTemplate.id
|
||||
)
|
||||
)
|
||||
.map(gameTemplate => {
|
||||
const archivedGameTemplateListingData = getArchivedPrivateGameTemplateListingData(
|
||||
{
|
||||
gameTemplate,
|
||||
}
|
||||
);
|
||||
return (
|
||||
<PrivateGameTemplateTile
|
||||
privateGameTemplateListingData={archivedGameTemplateListingData}
|
||||
onSelect={() => {
|
||||
onPrivateGameTemplateSelection(archivedGameTemplateListingData);
|
||||
}}
|
||||
owned={true}
|
||||
key={gameTemplate.id}
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
||||
return [
|
||||
...ownedGameTemplateTiles,
|
||||
...archivedOwnedGameTemplateTiles,
|
||||
...gameTemplateTiles,
|
||||
];
|
||||
};
|
||||
|
||||
export const getPublicAssetPackTiles = ({
|
||||
publicAssetPacks,
|
||||
onPublicAssetPackSelection,
|
||||
openedShopCategory,
|
||||
hasAssetFiltersApplied,
|
||||
}: {|
|
||||
publicAssetPacks: ?(PublicAssetPack[]),
|
||||
onPublicAssetPackSelection: ?(PublicAssetPack) => void,
|
||||
openedShopCategory?: ?string,
|
||||
hasAssetFiltersApplied?: boolean,
|
||||
|}): Array<React.Node> => {
|
||||
if (
|
||||
!publicAssetPacks ||
|
||||
!onPublicAssetPackSelection ||
|
||||
hasAssetFiltersApplied
|
||||
)
|
||||
return [];
|
||||
|
||||
const filteredAssetPacks = publicAssetPacks.filter(
|
||||
assetPack =>
|
||||
!openedShopCategory || assetPack.categories.includes(openedShopCategory)
|
||||
);
|
||||
|
||||
return filteredAssetPacks.map((assetPack, index) => (
|
||||
<PublicAssetPackTile
|
||||
assetPack={assetPack}
|
||||
onSelect={() => onPublicAssetPackSelection(assetPack)}
|
||||
key={`${assetPack.tag}-${index}`}
|
||||
/>
|
||||
));
|
||||
};
|
||||
|
||||
export const getAssetPackTiles = ({
|
||||
allPrivateAssetPackListingDatas,
|
||||
displayedPrivateAssetPackListingDatas,
|
||||
onPrivateAssetPackSelection,
|
||||
publicAssetPackTiles,
|
||||
receivedAssetPacks,
|
||||
openedShopCategory,
|
||||
hasAssetFiltersApplied,
|
||||
}: {|
|
||||
allPrivateAssetPackListingDatas: ?Array<PrivateAssetPackListingData>,
|
||||
displayedPrivateAssetPackListingDatas: ?Array<PrivateAssetPackListingData>,
|
||||
onPrivateAssetPackSelection: ?(PrivateAssetPackListingData) => void,
|
||||
publicAssetPackTiles?: Array<React.Node>,
|
||||
receivedAssetPacks: ?Array<any>,
|
||||
openedShopCategory?: ?string,
|
||||
hasAssetFiltersApplied?: boolean,
|
||||
|}): {|
|
||||
allAssetPackStandAloneTiles: Array<React.Node>,
|
||||
allAssetPackBundleTiles: Array<React.Node>,
|
||||
|} => {
|
||||
if (
|
||||
!allPrivateAssetPackListingDatas ||
|
||||
!displayedPrivateAssetPackListingDatas ||
|
||||
hasAssetFiltersApplied
|
||||
) {
|
||||
return {
|
||||
allAssetPackStandAloneTiles: [],
|
||||
allAssetPackBundleTiles: [],
|
||||
};
|
||||
}
|
||||
|
||||
const privateAssetPackStandAloneTiles: Array<React.Node> = [];
|
||||
const privateOwnedAssetPackStandAloneTiles: Array<React.Node> = [];
|
||||
const privateAssetPackBundleTiles: Array<React.Node> = [];
|
||||
const privateOwnedAssetPackBundleTiles: Array<React.Node> = [];
|
||||
|
||||
const filteredAssetPacks = displayedPrivateAssetPackListingDatas.filter(
|
||||
assetPackListingData =>
|
||||
!openedShopCategory ||
|
||||
assetPackListingData.categories.includes(openedShopCategory)
|
||||
);
|
||||
|
||||
!!onPrivateAssetPackSelection &&
|
||||
filteredAssetPacks.forEach(assetPackListingData => {
|
||||
const isPackOwned =
|
||||
!!receivedAssetPacks &&
|
||||
!!receivedAssetPacks.find(pack => pack.id === assetPackListingData.id);
|
||||
const tile = (
|
||||
<PrivateAssetPackTile
|
||||
assetPackListingData={assetPackListingData}
|
||||
onSelect={() => {
|
||||
onPrivateAssetPackSelection(assetPackListingData);
|
||||
}}
|
||||
owned={isPackOwned}
|
||||
key={assetPackListingData.id}
|
||||
/>
|
||||
);
|
||||
if (
|
||||
assetPackListingData.includedListableProductIds &&
|
||||
!!assetPackListingData.includedListableProductIds.length
|
||||
) {
|
||||
if (isPackOwned) {
|
||||
privateOwnedAssetPackBundleTiles.push(tile);
|
||||
} else {
|
||||
privateAssetPackBundleTiles.push(tile);
|
||||
}
|
||||
} else {
|
||||
if (isPackOwned) {
|
||||
privateOwnedAssetPackStandAloneTiles.push(tile);
|
||||
} else {
|
||||
privateAssetPackStandAloneTiles.push(tile);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Handle archived asset packs that are owned by the user.
|
||||
// These are asset packs that are not listed in the shop anymore,
|
||||
// but that the user has already purchased.
|
||||
const archivedOwnedAssetPackStandAloneTiles: Array<React.Node> = [];
|
||||
const archivedOwnedAssetPackBundleTiles: Array<React.Node> = [];
|
||||
!!onPrivateAssetPackSelection &&
|
||||
(receivedAssetPacks || [])
|
||||
.filter(
|
||||
assetPack =>
|
||||
!allPrivateAssetPackListingDatas.find(
|
||||
privateAssetPackListingData =>
|
||||
privateAssetPackListingData.id === assetPack.id
|
||||
)
|
||||
)
|
||||
.forEach(assetPack => {
|
||||
const archivedAssetPackListingData = getArchivedPrivateAssetPackListingData(
|
||||
{
|
||||
assetPack,
|
||||
}
|
||||
);
|
||||
const tile = (
|
||||
<PrivateAssetPackTile
|
||||
assetPackListingData={archivedAssetPackListingData}
|
||||
onSelect={() => {
|
||||
onPrivateAssetPackSelection(archivedAssetPackListingData);
|
||||
}}
|
||||
owned={true}
|
||||
key={assetPack.id}
|
||||
/>
|
||||
);
|
||||
|
||||
if (
|
||||
archivedAssetPackListingData.includedListableProductIds &&
|
||||
!!archivedAssetPackListingData.includedListableProductIds.length
|
||||
) {
|
||||
archivedOwnedAssetPackBundleTiles.push(tile);
|
||||
} else {
|
||||
archivedOwnedAssetPackStandAloneTiles.push(tile);
|
||||
}
|
||||
});
|
||||
|
||||
const allAssetPackBundleTiles = [
|
||||
...privateOwnedAssetPackBundleTiles,
|
||||
...archivedOwnedAssetPackBundleTiles,
|
||||
...privateAssetPackBundleTiles,
|
||||
];
|
||||
|
||||
const allAssetPackStandAloneTiles = [
|
||||
...privateOwnedAssetPackStandAloneTiles,
|
||||
...archivedOwnedAssetPackStandAloneTiles,
|
||||
...mergeArraysPerGroup(
|
||||
privateAssetPackStandAloneTiles,
|
||||
publicAssetPackTiles || [],
|
||||
2,
|
||||
1
|
||||
),
|
||||
];
|
||||
|
||||
return { allAssetPackStandAloneTiles, allAssetPackBundleTiles };
|
||||
};
|
||||
|
@@ -191,7 +191,7 @@ function AssetSwappingDialog({
|
||||
>
|
||||
<AssetStore
|
||||
ref={assetStore}
|
||||
onlyShowAssets
|
||||
hideGameTemplates
|
||||
assetSwappedObject={object}
|
||||
minimalUI={minimalUI}
|
||||
/>
|
||||
|
@@ -10,7 +10,6 @@ import type {
|
||||
import {
|
||||
type PrivateAssetPackListingData,
|
||||
type PrivateGameTemplateListingData,
|
||||
type BundleListingData,
|
||||
} from '../Utils/GDevelopServices/Shop';
|
||||
import { Line, Column } from '../UI/Grid';
|
||||
import ScrollView, { type ScrollViewInterface } from '../UI/ScrollView';
|
||||
@@ -19,20 +18,19 @@ import {
|
||||
type WindowSizeType,
|
||||
} from '../UI/Responsive/ResponsiveWindowMeasurer';
|
||||
import AuthenticatedUserContext from '../Profile/AuthenticatedUserContext';
|
||||
import { CategoryTile } from './ShopTiles';
|
||||
import { mergeArraysPerGroup } from '../Utils/Array';
|
||||
import {
|
||||
CategoryTile,
|
||||
PrivateAssetPackTile,
|
||||
PublicAssetPackTile,
|
||||
PrivateGameTemplateTile,
|
||||
} from './ShopTiles';
|
||||
import { useDebounce } from '../Utils/UseDebounce';
|
||||
import PromotionsSlideshow from '../Promotions/PromotionsSlideshow';
|
||||
import { ColumnStackLayout } from '../UI/Layout';
|
||||
import { EarnCredits } from '../GameDashboard/Wallet/EarnCredits';
|
||||
import { LARGE_WIDGET_SIZE } from '../MainFrame/EditorContainers/HomePage/CardWidget';
|
||||
import {
|
||||
getBundleTiles,
|
||||
getGameTemplateTiles,
|
||||
getAssetPackTiles,
|
||||
getPublicAssetPackTiles,
|
||||
} from './AssetStoreUtils';
|
||||
import { EarnCredits } from '../MainFrame/EditorContainers/HomePage/GetStartedSection/EarnCredits';
|
||||
|
||||
const cellSpacing = 10;
|
||||
const cellSpacing = 2;
|
||||
|
||||
const getCategoryColumns = (
|
||||
windowSize: WindowSizeType,
|
||||
@@ -44,9 +42,9 @@ const getCategoryColumns = (
|
||||
case 'medium':
|
||||
return 3;
|
||||
case 'large':
|
||||
return 5;
|
||||
return 4;
|
||||
case 'xlarge':
|
||||
return 7;
|
||||
return 6;
|
||||
default:
|
||||
return 3;
|
||||
}
|
||||
@@ -60,11 +58,11 @@ const getShopItemsColumns = (
|
||||
case 'small':
|
||||
return isLandscape ? 3 : 2;
|
||||
case 'medium':
|
||||
return 3;
|
||||
return 2;
|
||||
case 'large':
|
||||
return 4;
|
||||
return 3;
|
||||
case 'xlarge':
|
||||
return 6;
|
||||
return 5;
|
||||
default:
|
||||
return 2;
|
||||
}
|
||||
@@ -120,13 +118,9 @@ export const shopCategories = {
|
||||
},
|
||||
};
|
||||
|
||||
const MAX_COLUMNS = getShopItemsColumns('xlarge', true);
|
||||
const MAX_SECTION_WIDTH = (LARGE_WIDGET_SIZE + 2 * 5) * MAX_COLUMNS; // widget size + 5 padding per side
|
||||
const styles = {
|
||||
grid: {
|
||||
// Avoid tiles taking too much space on large screens.
|
||||
maxWidth: MAX_SECTION_WIDTH,
|
||||
width: `calc(100% + ${cellSpacing}px)`, // This is needed to compensate for the `margin: -5px` added by MUI related to spacing.
|
||||
margin: '0 10px',
|
||||
// Remove the scroll capability of the grid, the scroll view handles it.
|
||||
overflow: 'unset',
|
||||
},
|
||||
@@ -162,14 +156,12 @@ type Props = {|
|
||||
publicAssetPacks: PublicAssetPacks,
|
||||
privateAssetPackListingDatas: Array<PrivateAssetPackListingData>,
|
||||
privateGameTemplateListingDatas: Array<PrivateGameTemplateListingData>,
|
||||
bundleListingDatas: Array<BundleListingData>,
|
||||
onPublicAssetPackSelection: PublicAssetPack => void,
|
||||
onPrivateAssetPackSelection: PrivateAssetPackListingData => void,
|
||||
onPrivateGameTemplateSelection: PrivateGameTemplateListingData => void,
|
||||
onBundleSelection: BundleListingData => void,
|
||||
onCategorySelection: string => void,
|
||||
openedShopCategory: string | null,
|
||||
onlyShowAssets?: boolean,
|
||||
hideGameTemplates?: boolean,
|
||||
displayPromotions?: boolean,
|
||||
onOpenProfile?: () => void,
|
||||
|};
|
||||
@@ -180,14 +172,12 @@ export const AssetsHome = React.forwardRef<Props, AssetsHomeInterface>(
|
||||
publicAssetPacks: { starterPacks },
|
||||
privateAssetPackListingDatas,
|
||||
privateGameTemplateListingDatas,
|
||||
bundleListingDatas,
|
||||
onPublicAssetPackSelection,
|
||||
onPrivateAssetPackSelection,
|
||||
onPrivateGameTemplateSelection,
|
||||
onBundleSelection,
|
||||
onCategorySelection,
|
||||
openedShopCategory,
|
||||
onlyShowAssets,
|
||||
hideGameTemplates,
|
||||
displayPromotions,
|
||||
onOpenProfile,
|
||||
}: Props,
|
||||
@@ -197,7 +187,6 @@ export const AssetsHome = React.forwardRef<Props, AssetsHomeInterface>(
|
||||
const {
|
||||
receivedAssetPacks,
|
||||
receivedGameTemplates,
|
||||
receivedBundles,
|
||||
badges,
|
||||
achievements,
|
||||
} = React.useContext(AuthenticatedUserContext);
|
||||
@@ -226,7 +215,7 @@ export const AssetsHome = React.forwardRef<Props, AssetsHomeInterface>(
|
||||
Object.entries(shopCategories).map(
|
||||
// $FlowExpectedError - Object.entries does not infer well the type of the value.
|
||||
([id, { title, imageSource, imageAlt }]) =>
|
||||
onlyShowAssets && id === 'game-template' ? null : (
|
||||
hideGameTemplates && id === 'game-template' ? null : (
|
||||
<CategoryTile
|
||||
// This id would be more appropriate if it was shop-category-...
|
||||
// but it is kept as is to avoid breaking some guided lessons using this
|
||||
@@ -242,71 +231,124 @@ export const AssetsHome = React.forwardRef<Props, AssetsHomeInterface>(
|
||||
/>
|
||||
)
|
||||
),
|
||||
[onCategorySelection, onlyShowAssets]
|
||||
[onCategorySelection, hideGameTemplates]
|
||||
);
|
||||
|
||||
const openedShopCategoryTitle = openedShopCategory
|
||||
? shopCategories[openedShopCategory].title
|
||||
: null;
|
||||
|
||||
const publicPackTiles: Array<React.Node> = React.useMemo(
|
||||
() =>
|
||||
getPublicAssetPackTiles({
|
||||
publicAssetPacks: starterPacks,
|
||||
onPublicAssetPackSelection,
|
||||
openedShopCategory,
|
||||
}),
|
||||
[starterPacks, onPublicAssetPackSelection, openedShopCategory]
|
||||
);
|
||||
const starterPacksTiles: Array<React.Node> = starterPacks
|
||||
.filter(
|
||||
assetPack =>
|
||||
!openedShopCategory ||
|
||||
assetPack.categories.includes(openedShopCategory)
|
||||
)
|
||||
.map((assetPack, index) => (
|
||||
<PublicAssetPackTile
|
||||
assetPack={assetPack}
|
||||
onSelect={() => onPublicAssetPackSelection(assetPack)}
|
||||
key={`${assetPack.tag}-${index}`}
|
||||
/>
|
||||
));
|
||||
|
||||
const {
|
||||
allAssetPackStandAloneTiles,
|
||||
allAssetPackBundleTiles,
|
||||
} = React.useMemo(
|
||||
() =>
|
||||
getAssetPackTiles({
|
||||
allPrivateAssetPackListingDatas: privateAssetPackListingDatas,
|
||||
displayedPrivateAssetPackListingDatas: privateAssetPackListingDatas,
|
||||
onPrivateAssetPackSelection,
|
||||
publicAssetPackTiles: publicPackTiles,
|
||||
receivedAssetPacks,
|
||||
openedShopCategory,
|
||||
}),
|
||||
const { allStandAloneTiles, allBundleTiles } = React.useMemo(
|
||||
() => {
|
||||
const privateAssetPackStandAloneTiles: Array<React.Node> = [];
|
||||
const privateOwnedAssetPackStandAloneTiles: Array<React.Node> = [];
|
||||
const privateAssetPackBundleTiles: Array<React.Node> = [];
|
||||
const privateOwnedAssetPackBundleTiles: Array<React.Node> = [];
|
||||
|
||||
privateAssetPackListingDatas
|
||||
.filter(
|
||||
assetPackListingData =>
|
||||
!openedShopCategory ||
|
||||
assetPackListingData.categories.includes(openedShopCategory)
|
||||
)
|
||||
.forEach(assetPackListingData => {
|
||||
const isPackOwned =
|
||||
!!receivedAssetPacks &&
|
||||
!!receivedAssetPacks.find(
|
||||
pack => pack.id === assetPackListingData.id
|
||||
);
|
||||
const tile = (
|
||||
<PrivateAssetPackTile
|
||||
assetPackListingData={assetPackListingData}
|
||||
onSelect={() => {
|
||||
onPrivateAssetPackSelection(assetPackListingData);
|
||||
}}
|
||||
owned={isPackOwned}
|
||||
key={assetPackListingData.id}
|
||||
/>
|
||||
);
|
||||
if (
|
||||
assetPackListingData.includedListableProductIds &&
|
||||
!!assetPackListingData.includedListableProductIds.length
|
||||
) {
|
||||
if (isPackOwned) {
|
||||
privateOwnedAssetPackBundleTiles.push(tile);
|
||||
} else {
|
||||
privateAssetPackBundleTiles.push(tile);
|
||||
}
|
||||
} else {
|
||||
if (isPackOwned) {
|
||||
privateOwnedAssetPackStandAloneTiles.push(tile);
|
||||
} else {
|
||||
privateAssetPackStandAloneTiles.push(tile);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const allBundleTiles = [
|
||||
...privateOwnedAssetPackBundleTiles, // Display owned bundles first.
|
||||
...privateAssetPackBundleTiles,
|
||||
];
|
||||
|
||||
const allStandAloneTiles = [
|
||||
...privateOwnedAssetPackStandAloneTiles, // Display owned packs first.
|
||||
...mergeArraysPerGroup(
|
||||
privateAssetPackStandAloneTiles,
|
||||
starterPacksTiles,
|
||||
2,
|
||||
1
|
||||
),
|
||||
];
|
||||
|
||||
return { allStandAloneTiles, allBundleTiles };
|
||||
},
|
||||
[
|
||||
privateAssetPackListingDatas,
|
||||
openedShopCategory,
|
||||
onPrivateAssetPackSelection,
|
||||
publicPackTiles,
|
||||
starterPacksTiles,
|
||||
receivedAssetPacks,
|
||||
]
|
||||
);
|
||||
|
||||
const allBundleTiles = React.useMemo(
|
||||
() =>
|
||||
getBundleTiles({
|
||||
allBundleListingDatas: bundleListingDatas,
|
||||
displayedBundleListingDatas: bundleListingDatas,
|
||||
onBundleSelection,
|
||||
receivedBundles,
|
||||
openedShopCategory,
|
||||
}),
|
||||
[
|
||||
bundleListingDatas,
|
||||
openedShopCategory,
|
||||
onBundleSelection,
|
||||
receivedBundles,
|
||||
]
|
||||
);
|
||||
|
||||
const gameTemplateTiles = React.useMemo(
|
||||
() =>
|
||||
getGameTemplateTiles({
|
||||
allPrivateGameTemplateListingDatas: privateGameTemplateListingDatas,
|
||||
displayedPrivateGameTemplateListingDatas: privateGameTemplateListingDatas,
|
||||
onPrivateGameTemplateSelection,
|
||||
receivedGameTemplates,
|
||||
openedShopCategory,
|
||||
}),
|
||||
() => {
|
||||
// Only show game templates if the category is not set or is set to "game-template".
|
||||
return privateGameTemplateListingDatas
|
||||
.filter(
|
||||
privateGameTemplateListingData =>
|
||||
!openedShopCategory || openedShopCategory === 'game-template'
|
||||
)
|
||||
.map((privateGameTemplateListingData, index) => (
|
||||
<PrivateGameTemplateTile
|
||||
privateGameTemplateListingData={privateGameTemplateListingData}
|
||||
onSelect={() => {
|
||||
onPrivateGameTemplateSelection(privateGameTemplateListingData);
|
||||
}}
|
||||
owned={
|
||||
!!receivedGameTemplates &&
|
||||
!!receivedGameTemplates.find(
|
||||
pack => pack.id === privateGameTemplateListingData.id
|
||||
)
|
||||
}
|
||||
key={privateGameTemplateListingData.id}
|
||||
/>
|
||||
));
|
||||
},
|
||||
[
|
||||
privateGameTemplateListingDatas,
|
||||
openedShopCategory,
|
||||
@@ -316,10 +358,10 @@ export const AssetsHome = React.forwardRef<Props, AssetsHomeInterface>(
|
||||
);
|
||||
|
||||
const {
|
||||
displayedList: displayedAssetPackStandAloneTiles,
|
||||
displayedList: displayedStandAloneTiles,
|
||||
onShowMore: onShowMoreStandAloneTiles,
|
||||
} = useProgressiveReveal({
|
||||
list: allAssetPackStandAloneTiles,
|
||||
list: allStandAloneTiles,
|
||||
numberPerPage: 25,
|
||||
});
|
||||
|
||||
@@ -336,7 +378,7 @@ export const AssetsHome = React.forwardRef<Props, AssetsHomeInterface>(
|
||||
>
|
||||
{openedShopCategory ? null : (
|
||||
<>
|
||||
<Column noMargin>
|
||||
<Column>
|
||||
<Line>
|
||||
<Text size="block-title">
|
||||
<Trans>Explore by category</Trans>
|
||||
@@ -353,8 +395,8 @@ export const AssetsHome = React.forwardRef<Props, AssetsHomeInterface>(
|
||||
</GridList>
|
||||
</>
|
||||
)}
|
||||
{displayPromotions && !openedShopCategory ? (
|
||||
<ColumnStackLayout noMargin>
|
||||
{displayPromotions ? (
|
||||
<ColumnStackLayout>
|
||||
<Text size="block-title">
|
||||
<Trans>Promotions + Earn credits</Trans>
|
||||
</Text>
|
||||
@@ -369,12 +411,12 @@ export const AssetsHome = React.forwardRef<Props, AssetsHomeInterface>(
|
||||
)}
|
||||
</ColumnStackLayout>
|
||||
) : null}
|
||||
{allBundleTiles.length && !onlyShowAssets ? (
|
||||
{allBundleTiles.length ? (
|
||||
<>
|
||||
<Column noMargin>
|
||||
<Column>
|
||||
<Line>
|
||||
<Text size="block-title">
|
||||
<Trans>GDevelop Bundles</Trans>
|
||||
<Trans>Bundles</Trans>
|
||||
</Text>
|
||||
</Line>
|
||||
</Column>
|
||||
@@ -388,36 +430,17 @@ export const AssetsHome = React.forwardRef<Props, AssetsHomeInterface>(
|
||||
</GridList>
|
||||
</>
|
||||
) : null}
|
||||
{allAssetPackBundleTiles.length ? (
|
||||
<>
|
||||
<Column noMargin>
|
||||
<Line>
|
||||
<Text size="block-title">
|
||||
<Trans>Asset pack bundles</Trans>
|
||||
</Text>
|
||||
</Line>
|
||||
</Column>
|
||||
<GridList
|
||||
cols={getShopItemsColumns(windowSize, isLandscape)}
|
||||
style={styles.grid}
|
||||
cellHeight="auto"
|
||||
spacing={cellSpacing}
|
||||
>
|
||||
{allAssetPackBundleTiles}
|
||||
</GridList>
|
||||
</>
|
||||
) : null}
|
||||
{openedShopCategoryTitle && (
|
||||
<Column noMargin>
|
||||
<Column>
|
||||
<Line>
|
||||
<Text size="block-title">{openedShopCategoryTitle}</Text>
|
||||
</Line>
|
||||
</Column>
|
||||
)}
|
||||
{!onlyShowAssets && (
|
||||
{!hideGameTemplates && (
|
||||
<>
|
||||
{!openedShopCategoryTitle && (
|
||||
<Column noMargin>
|
||||
<Column>
|
||||
<Line>
|
||||
<Text size="block-title">
|
||||
<Trans>All game templates</Trans>
|
||||
@@ -436,7 +459,7 @@ export const AssetsHome = React.forwardRef<Props, AssetsHomeInterface>(
|
||||
</>
|
||||
)}
|
||||
{!openedShopCategoryTitle && (
|
||||
<Column noMargin>
|
||||
<Column>
|
||||
<Line>
|
||||
<Text size="block-title">
|
||||
<Trans>All asset packs</Trans>
|
||||
@@ -450,7 +473,7 @@ export const AssetsHome = React.forwardRef<Props, AssetsHomeInterface>(
|
||||
cellHeight="auto"
|
||||
spacing={cellSpacing}
|
||||
>
|
||||
{displayedAssetPackStandAloneTiles}
|
||||
{displayedStandAloneTiles}
|
||||
</GridList>
|
||||
</ScrollView>
|
||||
);
|
||||
|
@@ -13,7 +13,6 @@ import {
|
||||
import {
|
||||
type PrivateAssetPackListingData,
|
||||
type PrivateGameTemplateListingData,
|
||||
type BundleListingData,
|
||||
} from '../Utils/GDevelopServices/Shop';
|
||||
import { NoResultPlaceholder } from './NoResultPlaceholder';
|
||||
import GridList from '@material-ui/core/GridList';
|
||||
@@ -25,7 +24,14 @@ import ScrollView, { type ScrollViewInterface } from '../UI/ScrollView';
|
||||
import PlaceholderLoader from '../UI/PlaceholderLoader';
|
||||
import PlaceholderError from '../UI/PlaceholderError';
|
||||
import AuthenticatedUserContext from '../Profile/AuthenticatedUserContext';
|
||||
import { AssetCardTile, AssetFolderTile } from './ShopTiles';
|
||||
import { mergeArraysPerGroup } from '../Utils/Array';
|
||||
import {
|
||||
AssetCardTile,
|
||||
AssetFolderTile,
|
||||
PrivateAssetPackTile,
|
||||
PrivateGameTemplateTile,
|
||||
PublicAssetPackTile,
|
||||
} from './ShopTiles';
|
||||
import PrivateAssetPackAudioFilesDownloadButton from './PrivateAssets/PrivateAssetPackAudioFilesDownloadButton';
|
||||
import { CorsAwareImage } from '../UI/CorsAwareImage';
|
||||
import { Column, LargeSpacer, Line } from '../UI/Grid';
|
||||
@@ -46,14 +52,6 @@ import HelpIcon from '../UI/HelpIcon';
|
||||
import { OwnedProductLicense } from './ProductLicense/ProductLicenseOptions';
|
||||
import { getUserProductPurchaseUsageType } from './ProductPageHelper';
|
||||
import PublicProfileContext from '../Profile/PublicProfileContext';
|
||||
import { BundleStoreContext } from './Bundles/BundleStoreContext';
|
||||
import {
|
||||
getBundleTiles,
|
||||
getGameTemplateTiles,
|
||||
getAssetPackTiles,
|
||||
getPublicAssetPackTiles,
|
||||
} from './AssetStoreUtils';
|
||||
import { LARGE_WIDGET_SIZE } from '../MainFrame/EditorContainers/HomePage/CardWidget';
|
||||
|
||||
const ASSETS_DISPLAY_LIMIT = 60;
|
||||
|
||||
@@ -81,7 +79,7 @@ const getShopItemsColumns = (
|
||||
case 'medium':
|
||||
return 2;
|
||||
case 'large':
|
||||
return 4;
|
||||
return 3;
|
||||
case 'xlarge':
|
||||
return 5;
|
||||
default:
|
||||
@@ -140,14 +138,10 @@ export const getAssetShortHeadersToDisplay = (
|
||||
return assetShortHeaders;
|
||||
};
|
||||
|
||||
const cellSpacing = 10;
|
||||
const MAX_COLUMNS = getShopItemsColumns('xlarge', true);
|
||||
const MAX_SECTION_WIDTH = (LARGE_WIDGET_SIZE + 2 * 5) * MAX_COLUMNS; // widget size + 5 padding per side
|
||||
const cellSpacing = 8;
|
||||
const styles = {
|
||||
grid: {
|
||||
// Avoid tiles taking too much space on large screens.
|
||||
maxWidth: MAX_SECTION_WIDTH,
|
||||
width: `calc(100% + ${cellSpacing}px)`, // This is needed to compensate for the `margin: -5px` added by MUI related to spacing.
|
||||
margin: '0 2px', // Remove the default margin of the grid but keep the horizontal padding for focus outline.
|
||||
// Remove the scroll capability of the grid, the scroll view handles it.
|
||||
overflow: 'unset',
|
||||
},
|
||||
@@ -235,7 +229,6 @@ type Props = {|
|
||||
assetShortHeaders: ?Array<AssetShortHeader>,
|
||||
privateAssetPackListingDatas?: ?Array<PrivateAssetPackListingData>,
|
||||
privateGameTemplateListingDatas?: ?Array<PrivateGameTemplateListingData>,
|
||||
bundleListingDatas?: ?Array<BundleListingData>,
|
||||
publicAssetPacks?: ?Array<PublicAssetPack>,
|
||||
onOpenDetails: (assetShortHeader: AssetShortHeader) => void,
|
||||
noResultsPlaceHolder?: React.Node,
|
||||
@@ -250,7 +243,6 @@ type Props = {|
|
||||
onPrivateGameTemplateSelection?: (
|
||||
privateGameTemplateListingData: PrivateGameTemplateListingData
|
||||
) => void,
|
||||
onBundleSelection?: (bundleListingData: BundleListingData) => void,
|
||||
onFolderSelection?: (folderTag: string) => void,
|
||||
onGoBackToFolderIndex?: (folderIndex: number) => void,
|
||||
noScroll?: boolean,
|
||||
@@ -259,7 +251,7 @@ type Props = {|
|
||||
// Or it can display arbitrary content, like the list of assets in a pack, or similar assets,
|
||||
// then currentPage is null.
|
||||
currentPage?: AssetStorePageState,
|
||||
onlyShowAssets?: boolean,
|
||||
hideGameTemplates?: boolean,
|
||||
hideDetails?: boolean,
|
||||
|};
|
||||
|
||||
@@ -271,17 +263,15 @@ const AssetsList = React.forwardRef<Props, AssetsListInterface>(
|
||||
noResultsPlaceHolder,
|
||||
privateAssetPackListingDatas,
|
||||
privateGameTemplateListingDatas,
|
||||
bundleListingDatas,
|
||||
publicAssetPacks,
|
||||
onPrivateAssetPackSelection,
|
||||
onPublicAssetPackSelection,
|
||||
onPrivateGameTemplateSelection,
|
||||
onBundleSelection,
|
||||
onFolderSelection,
|
||||
onGoBackToFolderIndex,
|
||||
noScroll,
|
||||
currentPage,
|
||||
onlyShowAssets,
|
||||
hideGameTemplates,
|
||||
hideDetails,
|
||||
}: Props,
|
||||
ref
|
||||
@@ -299,19 +289,11 @@ const AssetsList = React.forwardRef<Props, AssetsListInterface>(
|
||||
const {
|
||||
error: gameTemplateStoreError,
|
||||
fetchGameTemplates,
|
||||
privateGameTemplateListingDatas: allPrivateGameTemplateListingDatas,
|
||||
} = React.useContext(PrivateGameTemplateStoreContext);
|
||||
const {
|
||||
error: bundleStoreError,
|
||||
fetchBundles,
|
||||
bundleListingDatas: allBundleListingDatas,
|
||||
} = React.useContext(BundleStoreContext);
|
||||
const {
|
||||
receivedAssetPacks,
|
||||
receivedGameTemplates,
|
||||
receivedBundles,
|
||||
assetPackPurchases,
|
||||
bundlePurchases,
|
||||
} = React.useContext(AuthenticatedUserContext);
|
||||
const [
|
||||
authorPublicProfile,
|
||||
@@ -361,13 +343,11 @@ const AssetsList = React.forwardRef<Props, AssetsListInterface>(
|
||||
() => {
|
||||
fetchAssetsAndFilters();
|
||||
fetchGameTemplates();
|
||||
fetchBundles();
|
||||
},
|
||||
[fetchAssetsAndFilters, fetchGameTemplates, fetchBundles]
|
||||
[fetchAssetsAndFilters, fetchGameTemplates]
|
||||
);
|
||||
|
||||
const shopError =
|
||||
assetStoreError || gameTemplateStoreError || bundleStoreError;
|
||||
const shopError = assetStoreError || gameTemplateStoreError;
|
||||
|
||||
const hasAssetPackFiltersApplied = React.useMemo(
|
||||
// When a pack is opened, the asset pack filters are not hidden, but not relevant either.
|
||||
@@ -489,6 +469,7 @@ const AssetsList = React.forwardRef<Props, AssetsListInterface>(
|
||||
// Don't show assets if filtering on asset packs.)
|
||||
if (hasAssetPackFiltersApplied && !openedAssetPack) return [];
|
||||
const assetSize = getAssetSize(windowSize);
|
||||
const margin = cellSpacing / 2;
|
||||
|
||||
return getAssetShortHeadersToDisplay(
|
||||
assetShortHeaders,
|
||||
@@ -500,7 +481,7 @@ const AssetsList = React.forwardRef<Props, AssetsListInterface>(
|
||||
onOpenDetails={() => onOpenDetails(assetShortHeader)}
|
||||
size={assetSize}
|
||||
key={assetShortHeader.id}
|
||||
margin={cellSpacing / 2}
|
||||
margin={margin}
|
||||
hideShortDescription={!!hideDetails}
|
||||
/>
|
||||
));
|
||||
@@ -517,76 +498,144 @@ const AssetsList = React.forwardRef<Props, AssetsListInterface>(
|
||||
]
|
||||
);
|
||||
|
||||
const publicPackTiles: Array<React.Node> = React.useMemo(
|
||||
() =>
|
||||
getPublicAssetPackTiles({
|
||||
publicAssetPacks,
|
||||
onPublicAssetPackSelection,
|
||||
hasAssetFiltersApplied,
|
||||
}),
|
||||
const publicPacksTiles: Array<React.Node> = React.useMemo(
|
||||
() => {
|
||||
if (
|
||||
!publicAssetPacks ||
|
||||
!onPublicAssetPackSelection ||
|
||||
// Don't show public packs if filtering on assets.
|
||||
hasAssetFiltersApplied
|
||||
)
|
||||
return [];
|
||||
return publicAssetPacks.map((assetPack, index) => (
|
||||
<PublicAssetPackTile
|
||||
assetPack={assetPack}
|
||||
onSelect={() => onPublicAssetPackSelection(assetPack)}
|
||||
key={`${assetPack.tag}-${index}`}
|
||||
/>
|
||||
));
|
||||
},
|
||||
[publicAssetPacks, onPublicAssetPackSelection, hasAssetFiltersApplied]
|
||||
);
|
||||
|
||||
const {
|
||||
allAssetPackStandAloneTiles,
|
||||
allAssetPackBundleTiles,
|
||||
} = React.useMemo(
|
||||
() =>
|
||||
getAssetPackTiles({
|
||||
allPrivateAssetPackListingDatas,
|
||||
displayedPrivateAssetPackListingDatas: privateAssetPackListingDatas,
|
||||
onPrivateAssetPackSelection,
|
||||
publicAssetPackTiles: publicPackTiles,
|
||||
receivedAssetPacks,
|
||||
hasAssetFiltersApplied,
|
||||
}),
|
||||
const { allStandAlonePackTiles, allBundlePackTiles } = React.useMemo(
|
||||
() => {
|
||||
const privateAssetPackStandAloneTiles: Array<React.Node> = [];
|
||||
const privateOwnedAssetPackStandAloneTiles: Array<React.Node> = [];
|
||||
const privateAssetPackBundleTiles: Array<React.Node> = [];
|
||||
const privateOwnedAssetPackBundleTiles: Array<React.Node> = [];
|
||||
|
||||
if (
|
||||
!privateAssetPackListingDatas ||
|
||||
!receivedAssetPacks ||
|
||||
// Don't show private packs if filtering on assets.
|
||||
hasAssetFiltersApplied
|
||||
) {
|
||||
return {
|
||||
allStandAlonePackTiles: [],
|
||||
allBundlePackTiles: [],
|
||||
};
|
||||
}
|
||||
|
||||
!!onPrivateAssetPackSelection &&
|
||||
privateAssetPackListingDatas.forEach(assetPackListingData => {
|
||||
const isPackOwned =
|
||||
!!receivedAssetPacks &&
|
||||
!!receivedAssetPacks.find(
|
||||
pack => pack.id === assetPackListingData.id
|
||||
);
|
||||
const tile = (
|
||||
<PrivateAssetPackTile
|
||||
assetPackListingData={assetPackListingData}
|
||||
onSelect={() => {
|
||||
onPrivateAssetPackSelection(assetPackListingData);
|
||||
}}
|
||||
owned={isPackOwned}
|
||||
key={assetPackListingData.id}
|
||||
/>
|
||||
);
|
||||
if (
|
||||
assetPackListingData.includedListableProductIds &&
|
||||
!!assetPackListingData.includedListableProductIds.length
|
||||
) {
|
||||
if (isPackOwned) {
|
||||
privateOwnedAssetPackBundleTiles.push(tile);
|
||||
} else {
|
||||
privateAssetPackBundleTiles.push(tile);
|
||||
}
|
||||
} else {
|
||||
if (isPackOwned) {
|
||||
privateOwnedAssetPackStandAloneTiles.push(tile);
|
||||
} else {
|
||||
privateAssetPackStandAloneTiles.push(tile);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const allBundlePackTiles = [
|
||||
...privateOwnedAssetPackBundleTiles, // Display owned bundles first.
|
||||
...privateAssetPackBundleTiles,
|
||||
];
|
||||
|
||||
const allStandAlonePackTiles = [
|
||||
...privateOwnedAssetPackStandAloneTiles, // Display owned packs first.
|
||||
...mergeArraysPerGroup(
|
||||
privateAssetPackStandAloneTiles,
|
||||
publicPacksTiles,
|
||||
2,
|
||||
1
|
||||
),
|
||||
];
|
||||
|
||||
return { allStandAlonePackTiles, allBundlePackTiles };
|
||||
},
|
||||
[
|
||||
allPrivateAssetPackListingDatas,
|
||||
privateAssetPackListingDatas,
|
||||
onPrivateAssetPackSelection,
|
||||
publicPackTiles,
|
||||
publicPacksTiles,
|
||||
receivedAssetPacks,
|
||||
hasAssetFiltersApplied,
|
||||
]
|
||||
);
|
||||
|
||||
const gameTemplateTiles = React.useMemo(
|
||||
() =>
|
||||
getGameTemplateTiles({
|
||||
allPrivateGameTemplateListingDatas,
|
||||
displayedPrivateGameTemplateListingDatas: privateGameTemplateListingDatas,
|
||||
onPrivateGameTemplateSelection,
|
||||
receivedGameTemplates,
|
||||
hasAssetFiltersApplied,
|
||||
hasAssetPackFiltersApplied,
|
||||
onlyShowAssets,
|
||||
}),
|
||||
() => {
|
||||
if (
|
||||
!privateGameTemplateListingDatas ||
|
||||
!onPrivateGameTemplateSelection ||
|
||||
// Don't show private game templates if filtering on assets.
|
||||
hasAssetFiltersApplied ||
|
||||
// Don't show private game templates if filtering on asset packs.
|
||||
hasAssetPackFiltersApplied ||
|
||||
hideGameTemplates
|
||||
)
|
||||
return [];
|
||||
|
||||
return privateGameTemplateListingDatas.map(
|
||||
(privateGameTemplateListingData, index) => (
|
||||
<PrivateGameTemplateTile
|
||||
privateGameTemplateListingData={privateGameTemplateListingData}
|
||||
onSelect={() => {
|
||||
onPrivateGameTemplateSelection(privateGameTemplateListingData);
|
||||
}}
|
||||
owned={
|
||||
!!receivedGameTemplates &&
|
||||
!!receivedGameTemplates.find(
|
||||
pack => pack.id === privateGameTemplateListingData.id
|
||||
)
|
||||
}
|
||||
key={privateGameTemplateListingData.id}
|
||||
/>
|
||||
)
|
||||
);
|
||||
},
|
||||
[
|
||||
allPrivateGameTemplateListingDatas,
|
||||
privateGameTemplateListingDatas,
|
||||
onPrivateGameTemplateSelection,
|
||||
receivedGameTemplates,
|
||||
hasAssetFiltersApplied,
|
||||
hasAssetPackFiltersApplied,
|
||||
onlyShowAssets,
|
||||
]
|
||||
);
|
||||
|
||||
const bundleTiles = React.useMemo(
|
||||
() =>
|
||||
getBundleTiles({
|
||||
allBundleListingDatas: allBundleListingDatas,
|
||||
displayedBundleListingDatas: bundleListingDatas,
|
||||
onBundleSelection,
|
||||
receivedBundles,
|
||||
hasAssetFiltersApplied,
|
||||
}),
|
||||
[
|
||||
allBundleListingDatas,
|
||||
bundleListingDatas,
|
||||
onBundleSelection,
|
||||
receivedBundles,
|
||||
hasAssetFiltersApplied,
|
||||
hideGameTemplates,
|
||||
]
|
||||
);
|
||||
|
||||
@@ -649,27 +698,15 @@ const AssetsList = React.forwardRef<Props, AssetsListInterface>(
|
||||
getUserProductPurchaseUsageType({
|
||||
productId:
|
||||
openedAssetPack && openedAssetPack.id ? openedAssetPack.id : null,
|
||||
receivedProducts: [
|
||||
...(receivedAssetPacks || []),
|
||||
...(receivedBundles || []),
|
||||
],
|
||||
productPurchases: [
|
||||
...(assetPackPurchases || []),
|
||||
...(bundlePurchases || []),
|
||||
],
|
||||
allProductListingDatas: [
|
||||
...(allPrivateAssetPackListingDatas || []),
|
||||
...(bundleListingDatas || []),
|
||||
],
|
||||
receivedProducts: receivedAssetPacks,
|
||||
productPurchases: assetPackPurchases,
|
||||
allProductListingDatas: allPrivateAssetPackListingDatas,
|
||||
}),
|
||||
[
|
||||
assetPackPurchases,
|
||||
bundlePurchases,
|
||||
openedAssetPack,
|
||||
allPrivateAssetPackListingDatas,
|
||||
bundleListingDatas,
|
||||
receivedAssetPacks,
|
||||
receivedBundles,
|
||||
]
|
||||
);
|
||||
|
||||
@@ -703,20 +740,6 @@ const AssetsList = React.forwardRef<Props, AssetsListInterface>(
|
||||
scrollView={scrollView.current}
|
||||
/>
|
||||
)}
|
||||
{!openedAssetPack && bundleTiles.length && pageBreakIndex === 0 ? (
|
||||
<Line>
|
||||
<Column noMargin expand>
|
||||
<GridList
|
||||
cols={getShopItemsColumns(windowSize, isLandscape)}
|
||||
style={styles.grid}
|
||||
cellHeight="auto"
|
||||
spacing={cellSpacing}
|
||||
>
|
||||
{bundleTiles}
|
||||
</GridList>
|
||||
</Column>
|
||||
</Line>
|
||||
) : null}
|
||||
{!openedAssetPack &&
|
||||
gameTemplateTiles.length &&
|
||||
pageBreakIndex === 0 ? (
|
||||
@@ -726,7 +749,7 @@ const AssetsList = React.forwardRef<Props, AssetsListInterface>(
|
||||
cols={getShopItemsColumns(windowSize, isLandscape)}
|
||||
style={styles.grid}
|
||||
cellHeight="auto"
|
||||
spacing={cellSpacing}
|
||||
spacing={cellSpacing / 2}
|
||||
>
|
||||
{gameTemplateTiles}
|
||||
</GridList>
|
||||
@@ -734,7 +757,7 @@ const AssetsList = React.forwardRef<Props, AssetsListInterface>(
|
||||
</Line>
|
||||
) : null}
|
||||
{!openedAssetPack &&
|
||||
allAssetPackBundleTiles.length &&
|
||||
allBundlePackTiles.length &&
|
||||
pageBreakIndex === 0 ? (
|
||||
<Line>
|
||||
<Column noMargin expand>
|
||||
@@ -742,15 +765,15 @@ const AssetsList = React.forwardRef<Props, AssetsListInterface>(
|
||||
cols={getShopItemsColumns(windowSize, isLandscape)}
|
||||
style={styles.grid}
|
||||
cellHeight="auto"
|
||||
spacing={cellSpacing}
|
||||
spacing={cellSpacing / 2}
|
||||
>
|
||||
{allAssetPackBundleTiles}
|
||||
{allBundlePackTiles}
|
||||
</GridList>
|
||||
</Column>
|
||||
</Line>
|
||||
) : null}
|
||||
{!openedAssetPack &&
|
||||
allAssetPackStandAloneTiles.length &&
|
||||
allStandAlonePackTiles.length &&
|
||||
pageBreakIndex === 0 ? (
|
||||
<Line>
|
||||
<Column noMargin expand>
|
||||
@@ -758,9 +781,9 @@ const AssetsList = React.forwardRef<Props, AssetsListInterface>(
|
||||
cols={getShopItemsColumns(windowSize, isLandscape)}
|
||||
style={styles.grid}
|
||||
cellHeight="auto"
|
||||
spacing={cellSpacing}
|
||||
spacing={cellSpacing / 2}
|
||||
>
|
||||
{allAssetPackStandAloneTiles}
|
||||
{allStandAlonePackTiles}
|
||||
</GridList>
|
||||
</Column>
|
||||
</Line>
|
||||
@@ -922,12 +945,12 @@ const AssetsList = React.forwardRef<Props, AssetsListInterface>(
|
||||
</Column>
|
||||
) : null}
|
||||
{openedAssetPack && folderTiles.length ? (
|
||||
<Column justifyContent="center" noMargin>
|
||||
<Column justifyContent="center">
|
||||
<GridList
|
||||
style={styles.grid}
|
||||
cellHeight="auto"
|
||||
cols={getAssetFoldersColumns(windowSize, isLandscape)}
|
||||
spacing={cellSpacing}
|
||||
spacing={cellSpacing / 2}
|
||||
>
|
||||
{folderTiles}
|
||||
</GridList>
|
||||
@@ -952,11 +975,9 @@ const AssetsList = React.forwardRef<Props, AssetsListInterface>(
|
||||
// No assets to show.
|
||||
!assetTiles.length &&
|
||||
// No bundles to show.
|
||||
!bundleTiles.length &&
|
||||
// No asset pack bundles to show.
|
||||
!allAssetPackBundleTiles.length &&
|
||||
!allBundlePackTiles.length &&
|
||||
// No packs to show.
|
||||
!allAssetPackStandAloneTiles.length &&
|
||||
!allStandAlonePackTiles.length &&
|
||||
// no templates to show.
|
||||
!gameTemplateTiles.length &&
|
||||
(!openedAssetPack ||
|
||||
|
@@ -1,689 +0,0 @@
|
||||
// @flow
|
||||
import * as React from 'react';
|
||||
import { I18n } from '@lingui/react';
|
||||
import {
|
||||
type BundleListingData,
|
||||
type PrivateAssetPackListingData,
|
||||
type PrivateGameTemplateListingData,
|
||||
type CourseListingData,
|
||||
} from '../../Utils/GDevelopServices/Shop';
|
||||
import {
|
||||
getBundle,
|
||||
type Bundle,
|
||||
type Course,
|
||||
} from '../../Utils/GDevelopServices/Asset';
|
||||
import Text from '../../UI/Text';
|
||||
import { Trans } from '@lingui/macro';
|
||||
import AlertMessage from '../../UI/AlertMessage';
|
||||
import PlaceholderLoader from '../../UI/PlaceholderLoader';
|
||||
import FlatButton from '../../UI/FlatButton';
|
||||
import {
|
||||
ResponsiveLineStackLayout,
|
||||
LineStackLayout,
|
||||
ColumnStackLayout,
|
||||
} from '../../UI/Layout';
|
||||
import { Column, LargeSpacer, Line, Spacer } from '../../UI/Grid';
|
||||
import {
|
||||
getUserPublicProfile,
|
||||
type UserPublicProfile,
|
||||
} from '../../Utils/GDevelopServices/User';
|
||||
import Link from '../../UI/Link';
|
||||
import ResponsiveMediaGallery from '../../UI/ResponsiveMediaGallery';
|
||||
import {
|
||||
useResponsiveWindowSize,
|
||||
type WindowSizeType,
|
||||
} from '../../UI/Responsive/ResponsiveWindowMeasurer';
|
||||
import { sendBundleBuyClicked } from '../../Utils/Analytics/EventSender';
|
||||
import { MarkdownText } from '../../UI/MarkdownText';
|
||||
import ScrollView from '../../UI/ScrollView';
|
||||
import { shouldUseAppStoreProduct } from '../../Utils/AppStorePurchases';
|
||||
import AuthenticatedUserContext from '../../Profile/AuthenticatedUserContext';
|
||||
import { extractGDevelopApiErrorStatusAndCode } from '../../Utils/GDevelopServices/Errors';
|
||||
import Avatar from '@material-ui/core/Avatar';
|
||||
import GridList from '@material-ui/core/GridList';
|
||||
import { BundleStoreContext } from './BundleStoreContext';
|
||||
import {
|
||||
getBundlesContainingProductTiles,
|
||||
getOtherProductsFromSameAuthorTiles,
|
||||
getProductMediaItems,
|
||||
getProductsIncludedInBundle,
|
||||
getProductsIncludedInBundleTiles,
|
||||
getUserProductPurchaseUsageType,
|
||||
PurchaseProductButtons,
|
||||
} from '../ProductPageHelper';
|
||||
import SecureCheckout from '../SecureCheckout/SecureCheckout';
|
||||
import GDevelopThemeContext from '../../UI/Theme/GDevelopThemeContext';
|
||||
import BundlePurchaseDialog from './BundlePurchaseDialog';
|
||||
import PublicProfileContext from '../../Profile/PublicProfileContext';
|
||||
import { LARGE_WIDGET_SIZE } from '../../MainFrame/EditorContainers/HomePage/CardWidget';
|
||||
import { PrivateGameTemplateStoreContext } from '../PrivateGameTemplates/PrivateGameTemplateStoreContext';
|
||||
import { AssetStoreContext } from '../AssetStoreContext';
|
||||
import CourseStoreContext from '../../Course/CourseStoreContext';
|
||||
import { getCreditsAmountFromId } from '../CreditsPackages/CreditsPackageStoreContext';
|
||||
import Coin from '../../Credits/Icons/Coin';
|
||||
import {
|
||||
getPlanIcon,
|
||||
getPlanInferredNameFromId,
|
||||
} from '../../Profile/Subscription/PlanCard';
|
||||
import RedemptionCodesDialog from '../../RedemptionCode/RedemptionCodesDialog';
|
||||
import { selectMessageByLocale } from '../../Utils/i18n/MessageByLocale';
|
||||
import { formatDurationOfRedemptionCode } from '../../RedemptionCode/Utils';
|
||||
|
||||
const cellSpacing = 10;
|
||||
|
||||
const getTemplateColumns = (
|
||||
windowSize: WindowSizeType,
|
||||
isLandscape: boolean
|
||||
) => {
|
||||
switch (windowSize) {
|
||||
case 'small':
|
||||
return isLandscape ? 4 : 2;
|
||||
case 'medium':
|
||||
return 3;
|
||||
case 'large':
|
||||
return 4;
|
||||
case 'xlarge':
|
||||
return 6;
|
||||
default:
|
||||
return 3;
|
||||
}
|
||||
};
|
||||
const MAX_COLUMNS = getTemplateColumns('xlarge', true);
|
||||
const MAX_SECTION_WIDTH = (LARGE_WIDGET_SIZE + 2 * 5) * MAX_COLUMNS; // widget size + 5 padding per side
|
||||
const styles = {
|
||||
disabledText: { opacity: 0.6 },
|
||||
scrollview: { overflowX: 'hidden' },
|
||||
grid: {
|
||||
// Avoid tiles taking too much space on large screens.
|
||||
maxWidth: MAX_SECTION_WIDTH,
|
||||
overflow: 'hidden',
|
||||
width: `calc(100% + ${cellSpacing}px)`, // This is needed to compensate for the `margin: -5px` added by MUI related to spacing.
|
||||
},
|
||||
leftColumnContainer: {
|
||||
flex: 1,
|
||||
minWidth: 0, // This is needed for the container to take the right size.
|
||||
},
|
||||
rightColumnContainer: {
|
||||
flex: 2,
|
||||
},
|
||||
leftColumnContainerMobile: {
|
||||
flex: 1,
|
||||
minWidth: 0, // This is needed for the container to take the right size.
|
||||
},
|
||||
rightColumnContainerMobile: {
|
||||
flex: 1,
|
||||
},
|
||||
avatar: {
|
||||
width: 20,
|
||||
height: 20,
|
||||
},
|
||||
ownedTag: {
|
||||
padding: '4px 8px',
|
||||
borderRadius: 4,
|
||||
color: 'black',
|
||||
},
|
||||
playIcon: {
|
||||
width: 20,
|
||||
height: 20,
|
||||
},
|
||||
coinIcon: {
|
||||
width: 13,
|
||||
height: 13,
|
||||
position: 'relative',
|
||||
top: -1,
|
||||
},
|
||||
};
|
||||
|
||||
type Props = {|
|
||||
bundleListingData: BundleListingData,
|
||||
bundleListingDatasFromSameCreator?: ?Array<BundleListingData>,
|
||||
receivedCourses: ?Array<Course>,
|
||||
onBundleOpen: BundleListingData => void,
|
||||
onGameTemplateOpen: PrivateGameTemplateListingData => void,
|
||||
onAssetPackOpen: (
|
||||
privateAssetPackListingData: PrivateAssetPackListingData,
|
||||
options?: {|
|
||||
forceProductPage?: boolean,
|
||||
|}
|
||||
) => void,
|
||||
onCourseOpen: CourseListingData => void,
|
||||
simulateAppStoreProduct?: boolean,
|
||||
|};
|
||||
|
||||
const BundleInformationPage = ({
|
||||
bundleListingData,
|
||||
bundleListingDatasFromSameCreator,
|
||||
receivedCourses,
|
||||
onBundleOpen,
|
||||
onGameTemplateOpen,
|
||||
onAssetPackOpen,
|
||||
onCourseOpen,
|
||||
simulateAppStoreProduct,
|
||||
}: Props) => {
|
||||
const { id, name, sellerId } = bundleListingData;
|
||||
const { bundleListingDatas } = React.useContext(BundleStoreContext);
|
||||
const { privateGameTemplateListingDatas } = React.useContext(
|
||||
PrivateGameTemplateStoreContext
|
||||
);
|
||||
const { privateAssetPackListingDatas } = React.useContext(AssetStoreContext);
|
||||
const { listedCourses } = React.useContext(CourseStoreContext);
|
||||
const {
|
||||
receivedBundles,
|
||||
bundlePurchases,
|
||||
receivedGameTemplates,
|
||||
receivedAssetPacks,
|
||||
} = React.useContext(AuthenticatedUserContext);
|
||||
const [bundle, setBundle] = React.useState<?Bundle>(null);
|
||||
const [
|
||||
purchasingBundleListingData,
|
||||
setPurchasingBundleListingData,
|
||||
] = React.useState<?BundleListingData>(null);
|
||||
const [isFetching, setIsFetching] = React.useState<boolean>(false);
|
||||
const { openUserPublicProfile } = React.useContext(PublicProfileContext);
|
||||
const [
|
||||
sellerPublicProfile,
|
||||
setSellerPublicProfile,
|
||||
] = React.useState<?UserPublicProfile>(null);
|
||||
const [errorText, setErrorText] = React.useState<?React.Node>(null);
|
||||
const {
|
||||
windowSize,
|
||||
isLandscape,
|
||||
isMediumScreen,
|
||||
isMobile,
|
||||
} = useResponsiveWindowSize();
|
||||
const gdevelopTheme = React.useContext(GDevelopThemeContext);
|
||||
const [
|
||||
isRedemptionCodesDialogOpen,
|
||||
setIsRedemptionCodesDialogOpen,
|
||||
] = React.useState<boolean>(false);
|
||||
|
||||
const shouldUseOrSimulateAppStoreProduct =
|
||||
shouldUseAppStoreProduct() || simulateAppStoreProduct;
|
||||
|
||||
const userBundlePurchaseUsageType = React.useMemo(
|
||||
() =>
|
||||
getUserProductPurchaseUsageType({
|
||||
productId: bundleListingData ? bundleListingData.id : null,
|
||||
receivedProducts: receivedBundles,
|
||||
productPurchases: bundlePurchases,
|
||||
allProductListingDatas: bundleListingDatas,
|
||||
}),
|
||||
[bundlePurchases, bundleListingData, bundleListingDatas, receivedBundles]
|
||||
);
|
||||
const isAlreadyReceived = !!userBundlePurchaseUsageType;
|
||||
const isOwningAnotherVariant = React.useMemo(
|
||||
() => {
|
||||
if (!bundle || isAlreadyReceived || !receivedBundles) return false;
|
||||
|
||||
// Another bundle older version of that bundle can be owned.
|
||||
// We look at the tag to determine if the bundle is the same.
|
||||
return !!receivedBundles.find(
|
||||
receivedBundle => receivedBundle.tag === bundle.tag
|
||||
);
|
||||
},
|
||||
[bundle, isAlreadyReceived, receivedBundles]
|
||||
);
|
||||
|
||||
const additionalProductThumbnailsIncludedInBundle: string[] = React.useMemo(
|
||||
() => {
|
||||
const productsIncludedInBundle = getProductsIncludedInBundle({
|
||||
productListingDatas: [
|
||||
...(bundleListingDatas || []),
|
||||
...(privateGameTemplateListingDatas || []),
|
||||
...(privateAssetPackListingDatas || []),
|
||||
...(listedCourses || []),
|
||||
],
|
||||
productListingData: bundleListingData,
|
||||
});
|
||||
|
||||
if (!productsIncludedInBundle) return [];
|
||||
|
||||
const additionalThumbnails = productsIncludedInBundle
|
||||
.map(product => (product.thumbnailUrls || []).slice(0, 2))
|
||||
.reduce((acc, thumbnails) => acc.concat(thumbnails), []);
|
||||
return additionalThumbnails;
|
||||
},
|
||||
[
|
||||
bundleListingDatas,
|
||||
privateGameTemplateListingDatas,
|
||||
privateAssetPackListingDatas,
|
||||
listedCourses,
|
||||
bundleListingData,
|
||||
]
|
||||
);
|
||||
|
||||
const productsIncludedInBundleTiles = React.useMemo(
|
||||
() =>
|
||||
getProductsIncludedInBundleTiles({
|
||||
product: bundle,
|
||||
productListingDatas: [
|
||||
...(bundleListingDatas || []),
|
||||
...(privateGameTemplateListingDatas || []),
|
||||
...(privateAssetPackListingDatas || []),
|
||||
...(listedCourses || []),
|
||||
],
|
||||
productListingData: bundleListingData,
|
||||
receivedProducts: [
|
||||
...(receivedBundles || []),
|
||||
...(receivedGameTemplates || []),
|
||||
...(receivedAssetPacks || []),
|
||||
...(receivedCourses || []),
|
||||
],
|
||||
onPrivateAssetPackOpen: product =>
|
||||
onAssetPackOpen(product, { forceProductPage: true }),
|
||||
onPrivateGameTemplateOpen: onGameTemplateOpen,
|
||||
onBundleOpen,
|
||||
onCourseOpen,
|
||||
}),
|
||||
[
|
||||
bundle,
|
||||
bundleListingDatas,
|
||||
privateGameTemplateListingDatas,
|
||||
privateAssetPackListingDatas,
|
||||
listedCourses,
|
||||
receivedBundles,
|
||||
receivedGameTemplates,
|
||||
receivedAssetPacks,
|
||||
receivedCourses,
|
||||
onAssetPackOpen,
|
||||
onGameTemplateOpen,
|
||||
onBundleOpen,
|
||||
onCourseOpen,
|
||||
bundleListingData,
|
||||
]
|
||||
);
|
||||
|
||||
const bundlesContainingBundleTiles = React.useMemo(
|
||||
() =>
|
||||
getBundlesContainingProductTiles({
|
||||
product: bundle,
|
||||
productListingData: bundleListingData,
|
||||
productListingDatas: bundleListingDatas,
|
||||
receivedProducts: receivedBundles,
|
||||
onPrivateAssetPackOpen: product =>
|
||||
onAssetPackOpen(product, { forceProductPage: true }),
|
||||
onPrivateGameTemplateOpen: onGameTemplateOpen,
|
||||
onBundleOpen,
|
||||
}),
|
||||
[
|
||||
bundle,
|
||||
bundleListingData,
|
||||
bundleListingDatas,
|
||||
receivedBundles,
|
||||
onAssetPackOpen,
|
||||
onGameTemplateOpen,
|
||||
onBundleOpen,
|
||||
]
|
||||
);
|
||||
|
||||
const otherBundlesFromTheSameAuthorTiles = React.useMemo(
|
||||
() =>
|
||||
getOtherProductsFromSameAuthorTiles({
|
||||
otherProductListingDatasFromSameCreator: bundleListingDatasFromSameCreator,
|
||||
currentProductListingData: bundleListingData,
|
||||
receivedProducts: receivedBundles,
|
||||
onProductOpen: onBundleOpen,
|
||||
}),
|
||||
[
|
||||
bundleListingDatasFromSameCreator,
|
||||
bundleListingData,
|
||||
receivedBundles,
|
||||
onBundleOpen,
|
||||
]
|
||||
);
|
||||
|
||||
React.useEffect(
|
||||
() => {
|
||||
(async () => {
|
||||
setIsFetching(true);
|
||||
try {
|
||||
const [bundle, profile] = await Promise.all([
|
||||
getBundle(id),
|
||||
getUserPublicProfile(sellerId),
|
||||
]);
|
||||
|
||||
setBundle(bundle);
|
||||
setSellerPublicProfile(profile);
|
||||
} catch (error) {
|
||||
const extractedStatusAndCode = extractGDevelopApiErrorStatusAndCode(
|
||||
error
|
||||
);
|
||||
if (extractedStatusAndCode && extractedStatusAndCode.status === 404) {
|
||||
setErrorText(
|
||||
<Trans>
|
||||
Bundle not found - An error occurred, please try again later.
|
||||
</Trans>
|
||||
);
|
||||
} else {
|
||||
setErrorText(
|
||||
<Trans>An error occurred, please try again later.</Trans>
|
||||
);
|
||||
}
|
||||
} finally {
|
||||
setIsFetching(false);
|
||||
}
|
||||
})();
|
||||
},
|
||||
[id, sellerId]
|
||||
);
|
||||
|
||||
const onClickBuy = React.useCallback(
|
||||
async () => {
|
||||
if (!bundle || isOwningAnotherVariant) return;
|
||||
if (isAlreadyReceived) {
|
||||
onBundleOpen(bundleListingData);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const price = bundleListingData.prices.find(
|
||||
price => price.usageType === 'default'
|
||||
);
|
||||
|
||||
sendBundleBuyClicked({
|
||||
bundleId: bundle.id,
|
||||
bundleName: bundle.name,
|
||||
bundleTag: bundle.tag,
|
||||
currency: price ? price.currency : undefined,
|
||||
usageType: 'default',
|
||||
});
|
||||
|
||||
setPurchasingBundleListingData(bundleListingData);
|
||||
} catch (e) {
|
||||
console.warn('Unable to send event', e);
|
||||
}
|
||||
},
|
||||
[
|
||||
bundle,
|
||||
bundleListingData,
|
||||
isAlreadyReceived,
|
||||
isOwningAnotherVariant,
|
||||
onBundleOpen,
|
||||
]
|
||||
);
|
||||
|
||||
const mediaItems = React.useMemo(
|
||||
() =>
|
||||
getProductMediaItems({
|
||||
product: bundle,
|
||||
productListingData: bundleListingData,
|
||||
shouldSimulateAppStoreProduct: simulateAppStoreProduct,
|
||||
additionalThumbnails: additionalProductThumbnailsIncludedInBundle,
|
||||
}),
|
||||
[
|
||||
bundle,
|
||||
bundleListingData,
|
||||
simulateAppStoreProduct,
|
||||
additionalProductThumbnailsIncludedInBundle,
|
||||
]
|
||||
);
|
||||
|
||||
const includedCreditsAmount = React.useMemo(
|
||||
() =>
|
||||
(bundleListingData.includedListableProducts || [])
|
||||
.filter(product => product.productType === 'CREDIT_PACKAGE')
|
||||
.reduce(
|
||||
(total, product) => total + getCreditsAmountFromId(product.productId),
|
||||
0
|
||||
),
|
||||
[bundleListingData]
|
||||
);
|
||||
|
||||
const includedRedemptionCodes = React.useMemo(
|
||||
() => bundleListingData.includedRedemptionCodes || [],
|
||||
[bundleListingData]
|
||||
);
|
||||
|
||||
return (
|
||||
<I18n>
|
||||
{({ i18n }) => (
|
||||
<>
|
||||
{errorText ? (
|
||||
<Line alignItems="center" justifyContent="center" expand>
|
||||
<AlertMessage kind="error">{errorText}</AlertMessage>
|
||||
</Line>
|
||||
) : isFetching ? (
|
||||
<Column expand alignItems="center" justifyContent="center">
|
||||
<PlaceholderLoader />
|
||||
</Column>
|
||||
) : bundle && sellerPublicProfile ? (
|
||||
<Column noOverflowParent expand noMargin>
|
||||
<ScrollView autoHideScrollbar style={styles.scrollview}>
|
||||
<ResponsiveLineStackLayout
|
||||
noColumnMargin
|
||||
noMargin
|
||||
// Force the columns to wrap on tablets and small screens.
|
||||
forceMobileLayout={isMediumScreen}
|
||||
// Prevent it to wrap when in landscape mode on small screens.
|
||||
noResponsiveLandscape
|
||||
useLargeSpacer
|
||||
>
|
||||
<div
|
||||
style={
|
||||
isMobile
|
||||
? styles.leftColumnContainerMobile
|
||||
: styles.leftColumnContainer
|
||||
}
|
||||
>
|
||||
<ResponsiveMediaGallery
|
||||
mediaItems={mediaItems}
|
||||
altTextTemplate={`Bundle ${name} preview image {mediaIndex}`}
|
||||
horizontalOuterMarginToEatOnMobile={8}
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
style={
|
||||
isMobile
|
||||
? styles.rightColumnContainerMobile
|
||||
: styles.rightColumnContainer
|
||||
}
|
||||
>
|
||||
<ColumnStackLayout noMargin>
|
||||
<LineStackLayout
|
||||
noMargin
|
||||
alignItems="center"
|
||||
justifyContent="space-between"
|
||||
>
|
||||
<Text noMargin size="title">
|
||||
{selectMessageByLocale(i18n, bundle.nameByLocale)}
|
||||
</Text>
|
||||
{isAlreadyReceived && (
|
||||
<div
|
||||
style={{
|
||||
...styles.ownedTag,
|
||||
backgroundColor:
|
||||
gdevelopTheme.statusIndicator.success,
|
||||
}}
|
||||
>
|
||||
<Text color="inherit" noMargin>
|
||||
<Trans>OWNED</Trans>
|
||||
</Text>
|
||||
</div>
|
||||
)}
|
||||
</LineStackLayout>
|
||||
<LineStackLayout noMargin alignItems="center">
|
||||
<Avatar
|
||||
src={sellerPublicProfile.iconUrl}
|
||||
style={styles.avatar}
|
||||
/>
|
||||
<Text displayInlineAsSpan size="sub-title">
|
||||
<Link
|
||||
onClick={() =>
|
||||
openUserPublicProfile({
|
||||
userId: sellerPublicProfile.id,
|
||||
callbacks: {
|
||||
onAssetPackOpen,
|
||||
onGameTemplateOpen,
|
||||
},
|
||||
})
|
||||
}
|
||||
href="#"
|
||||
>
|
||||
{sellerPublicProfile.username || ''}
|
||||
</Link>
|
||||
</Text>
|
||||
</LineStackLayout>
|
||||
<Spacer />
|
||||
{isOwningAnotherVariant ? (
|
||||
<AlertMessage kind="warning">
|
||||
<Trans>
|
||||
You own an older version of this bundle. Browse the
|
||||
store to access it!
|
||||
</Trans>
|
||||
</AlertMessage>
|
||||
) : !isAlreadyReceived ? (
|
||||
<>
|
||||
{!shouldUseOrSimulateAppStoreProduct && (
|
||||
<SecureCheckout />
|
||||
)}
|
||||
{!errorText && (
|
||||
<PurchaseProductButtons
|
||||
i18n={i18n}
|
||||
productListingData={bundleListingData}
|
||||
selectedUsageType="default"
|
||||
onUsageTypeChange={() => {}}
|
||||
simulateAppStoreProduct={simulateAppStoreProduct}
|
||||
isAlreadyReceived={isAlreadyReceived}
|
||||
onClickBuy={onClickBuy}
|
||||
onClickBuyWithCredits={() => {}}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
) : null}
|
||||
<Text size="body2" displayInlineAsSpan>
|
||||
<MarkdownText
|
||||
source={selectMessageByLocale(
|
||||
i18n,
|
||||
bundle.longDescriptionByLocale
|
||||
)}
|
||||
allowParagraphs
|
||||
/>
|
||||
</Text>
|
||||
{includedRedemptionCodes.length > 0 && (
|
||||
<ColumnStackLayout noMargin>
|
||||
{includedRedemptionCodes.map(
|
||||
(includedRedemptionCode, index) => (
|
||||
<LineStackLayout
|
||||
noMargin
|
||||
alignItems="center"
|
||||
key={`${
|
||||
includedRedemptionCode.givenSubscriptionPlanId
|
||||
}-${index}`}
|
||||
>
|
||||
{getPlanIcon({
|
||||
planId:
|
||||
includedRedemptionCode.givenSubscriptionPlanId,
|
||||
logoSize: 20,
|
||||
})}
|
||||
<Text>
|
||||
<Trans>
|
||||
{formatDurationOfRedemptionCode(
|
||||
includedRedemptionCode.durationInDays
|
||||
)}{' '}
|
||||
of
|
||||
{getPlanInferredNameFromId(
|
||||
includedRedemptionCode.givenSubscriptionPlanId
|
||||
)}
|
||||
subscription included
|
||||
</Trans>
|
||||
</Text>
|
||||
</LineStackLayout>
|
||||
)
|
||||
)}
|
||||
{isAlreadyReceived && (
|
||||
<Line noMargin>
|
||||
<FlatButton
|
||||
primary
|
||||
label={<Trans>See my codes</Trans>}
|
||||
onClick={() =>
|
||||
setIsRedemptionCodesDialogOpen(true)
|
||||
}
|
||||
/>
|
||||
</Line>
|
||||
)}
|
||||
</ColumnStackLayout>
|
||||
)}
|
||||
{includedCreditsAmount > 0 && (
|
||||
<LineStackLayout noMargin alignItems="center">
|
||||
<Coin style={styles.coinIcon} />
|
||||
<Text>
|
||||
<Trans>
|
||||
{includedCreditsAmount} credits included
|
||||
</Trans>
|
||||
</Text>
|
||||
</LineStackLayout>
|
||||
)}
|
||||
</ColumnStackLayout>
|
||||
</div>
|
||||
</ResponsiveLineStackLayout>
|
||||
{bundlesContainingBundleTiles &&
|
||||
bundlesContainingBundleTiles.length ? (
|
||||
<>
|
||||
<ColumnStackLayout noMargin>
|
||||
<LargeSpacer />
|
||||
{bundlesContainingBundleTiles}
|
||||
<LargeSpacer />
|
||||
</ColumnStackLayout>
|
||||
</>
|
||||
) : null}
|
||||
{productsIncludedInBundleTiles && (
|
||||
<>
|
||||
<Line>
|
||||
<Text size="block-title">
|
||||
<Trans>Included in this bundle</Trans>
|
||||
</Text>
|
||||
</Line>
|
||||
<Line>
|
||||
<GridList
|
||||
cols={getTemplateColumns(windowSize, isLandscape)}
|
||||
cellHeight="auto"
|
||||
spacing={cellSpacing}
|
||||
style={styles.grid}
|
||||
>
|
||||
{productsIncludedInBundleTiles}
|
||||
</GridList>
|
||||
</Line>
|
||||
</>
|
||||
)}
|
||||
{otherBundlesFromTheSameAuthorTiles &&
|
||||
otherBundlesFromTheSameAuthorTiles.length > 0 && (
|
||||
<>
|
||||
<Line>
|
||||
<Text size="block-title">
|
||||
<Trans>Similar bundles</Trans>
|
||||
</Text>
|
||||
</Line>
|
||||
<Line>
|
||||
<GridList
|
||||
cols={getTemplateColumns(windowSize, isLandscape)}
|
||||
cellHeight="auto"
|
||||
spacing={cellSpacing}
|
||||
style={styles.grid}
|
||||
>
|
||||
{otherBundlesFromTheSameAuthorTiles}
|
||||
</GridList>
|
||||
</Line>
|
||||
</>
|
||||
)}
|
||||
</ScrollView>
|
||||
</Column>
|
||||
) : null}
|
||||
{!!purchasingBundleListingData && (
|
||||
<BundlePurchaseDialog
|
||||
bundleListingData={purchasingBundleListingData}
|
||||
usageType="default"
|
||||
onClose={() => setPurchasingBundleListingData(null)}
|
||||
/>
|
||||
)}
|
||||
{isRedemptionCodesDialogOpen && (
|
||||
<RedemptionCodesDialog
|
||||
onClose={() => setIsRedemptionCodesDialogOpen(false)}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</I18n>
|
||||
);
|
||||
};
|
||||
|
||||
export default BundleInformationPage;
|
@@ -1,532 +0,0 @@
|
||||
// @flow
|
||||
|
||||
import * as React from 'react';
|
||||
import { I18n } from '@lingui/react';
|
||||
import { Trans } from '@lingui/macro';
|
||||
import Divider from '@material-ui/core/Divider';
|
||||
import {
|
||||
ColumnStackLayout,
|
||||
LineStackLayout,
|
||||
ResponsiveLineStackLayout,
|
||||
} from '../../UI/Layout';
|
||||
import Chip from '../../UI/Chip';
|
||||
import Text from '../../UI/Text';
|
||||
import { selectMessageByLocale } from '../../Utils/i18n/MessageByLocale';
|
||||
import { Column, LargeSpacer, Line, Spacer } from '../../UI/Grid';
|
||||
import {
|
||||
useResponsiveWindowSize,
|
||||
type WindowSizeType,
|
||||
} from '../../UI/Responsive/ResponsiveWindowMeasurer';
|
||||
import Paper from '../../UI/Paper';
|
||||
import RaisedButton from '../../UI/RaisedButton';
|
||||
import Skeleton from '@material-ui/lab/Skeleton';
|
||||
import AuthenticatedUserContext from '../../Profile/AuthenticatedUserContext';
|
||||
import {
|
||||
getArchivedBundleListingData,
|
||||
type BundleListingData,
|
||||
type CourseListingData,
|
||||
} from '../../Utils/GDevelopServices/Shop';
|
||||
import {
|
||||
getProductsIncludedInBundle,
|
||||
getUserProductPurchaseUsageType,
|
||||
} from '../ProductPageHelper';
|
||||
import { PrivateGameTemplateStoreContext } from '../PrivateGameTemplates/PrivateGameTemplateStoreContext';
|
||||
import { AssetStoreContext } from '../AssetStoreContext';
|
||||
import { BundleStoreContext } from './BundleStoreContext';
|
||||
import CourseStoreContext from '../../Course/CourseStoreContext';
|
||||
import { renderProductPrice } from '../ProductPriceTag';
|
||||
import {
|
||||
CreditsPackageStoreContext,
|
||||
getCreditsAmountFromId,
|
||||
} from '../CreditsPackages/CreditsPackageStoreContext';
|
||||
import {
|
||||
getPlanIcon,
|
||||
getPlanInferredNameFromId,
|
||||
} from '../../Profile/Subscription/PlanCard';
|
||||
import Store from '../../UI/CustomSvgIcons/Store';
|
||||
import Hammer from '../../UI/CustomSvgIcons/Hammer';
|
||||
import School from '../../UI/CustomSvgIcons/School';
|
||||
import Coin from '../../Credits/Icons/Coin';
|
||||
import Sparkle from '../../UI/CustomSvgIcons/Sparkle';
|
||||
import { renderEstimatedTotalPriceFormatted } from './Utils';
|
||||
import { formatDurationOfRedemptionCode } from '../../RedemptionCode/Utils';
|
||||
|
||||
const highlightColor = '#6CF9F7';
|
||||
|
||||
const mobilePadding = 8;
|
||||
const desktopPadding = 16;
|
||||
|
||||
const styles = {
|
||||
container: { display: 'flex', borderRadius: 8 },
|
||||
leftColumn: {
|
||||
display: 'flex',
|
||||
flex: 1,
|
||||
flexDirection: 'column',
|
||||
gap: 8,
|
||||
justifyContent: 'space-between',
|
||||
},
|
||||
bundlePreviewContainer: {
|
||||
flex: 1,
|
||||
background:
|
||||
'linear-gradient(90deg, rgba(59, 247, 244, 0.1) 0%, rgba(255, 188, 87, 0.1) 100%)',
|
||||
borderLeft: `2px solid ${highlightColor}`,
|
||||
},
|
||||
discountChip: { height: 24, backgroundColor: '#F03F18', color: 'white' },
|
||||
ownedChip: { height: 24, backgroundColor: '#8BE7C4', color: 'black' },
|
||||
imageContainer: {
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
position: 'relative',
|
||||
borderRadius: 8,
|
||||
overflow: 'hidden',
|
||||
},
|
||||
image: { width: '100%', aspectRatio: '16 / 9' },
|
||||
discountedPrice: { textDecoration: 'line-through', opacity: 0.7 },
|
||||
};
|
||||
|
||||
const CourseTile = ({
|
||||
courseListingData,
|
||||
isAlreadyReceived,
|
||||
}: {|
|
||||
courseListingData: CourseListingData,
|
||||
isAlreadyReceived: boolean,
|
||||
|}) => {
|
||||
return (
|
||||
<I18n>
|
||||
{({ i18n }) => (
|
||||
<ColumnStackLayout expand>
|
||||
<div style={styles.imageContainer}>
|
||||
<img
|
||||
src={courseListingData.thumbnailUrls[0]}
|
||||
style={styles.image}
|
||||
alt={courseListingData.name}
|
||||
/>
|
||||
</div>
|
||||
<Line noMargin>
|
||||
<Text noMargin>{courseListingData.name}</Text>
|
||||
</Line>
|
||||
{!isAlreadyReceived && (
|
||||
<Line noMargin>
|
||||
<Text noMargin color="secondary">
|
||||
<span style={styles.discountedPrice}>
|
||||
{renderProductPrice({
|
||||
i18n,
|
||||
productListingData: courseListingData,
|
||||
usageType: 'default',
|
||||
plainText: true,
|
||||
})}
|
||||
</span>
|
||||
</Text>
|
||||
</Line>
|
||||
)}
|
||||
</ColumnStackLayout>
|
||||
)}
|
||||
</I18n>
|
||||
);
|
||||
};
|
||||
|
||||
const BundlePreviewTile = ({
|
||||
bundleListingData,
|
||||
}: {
|
||||
bundleListingData: ?BundleListingData,
|
||||
}) => {
|
||||
const { isMobile } = useResponsiveWindowSize();
|
||||
if (!bundleListingData) return null;
|
||||
|
||||
const includedListableProducts =
|
||||
bundleListingData.includedListableProducts || [];
|
||||
const includedRedemptionCodes =
|
||||
bundleListingData.includedRedemptionCodes || [];
|
||||
const numberOfAssetPacks = includedListableProducts.filter(
|
||||
product => product.productType === 'ASSET_PACK'
|
||||
).length;
|
||||
const numberOfGameTemplates = includedListableProducts.filter(
|
||||
product => product.productType === 'GAME_TEMPLATE'
|
||||
).length;
|
||||
const numberOfCourses = includedListableProducts.filter(
|
||||
product => product.productType === 'COURSE'
|
||||
).length;
|
||||
const totalCredits = includedListableProducts
|
||||
.filter(product => product.productType === 'CREDITS_PACKAGE')
|
||||
.reduce(
|
||||
(total, product) => total + getCreditsAmountFromId(product.productId),
|
||||
0
|
||||
);
|
||||
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
...styles.bundlePreviewContainer,
|
||||
padding: isMobile ? mobilePadding : desktopPadding,
|
||||
margin: isMobile ? -mobilePadding : -desktopPadding,
|
||||
marginLeft: isMobile ? -mobilePadding : 0,
|
||||
marginTop: isMobile ? 0 : -desktopPadding,
|
||||
}}
|
||||
>
|
||||
<ColumnStackLayout expand>
|
||||
<LineStackLayout noMargin alignItems="center">
|
||||
<Sparkle style={{ color: highlightColor }} />
|
||||
<Text noMargin size="block-title">
|
||||
<Trans>This bundle includes:</Trans>
|
||||
</Text>
|
||||
</LineStackLayout>
|
||||
<Spacer />
|
||||
{numberOfAssetPacks > 0 && (
|
||||
<LineStackLayout noMargin alignItems="center">
|
||||
<Store />
|
||||
<Text noMargin>
|
||||
{numberOfAssetPacks === 1 ? (
|
||||
<Trans>{numberOfAssetPacks} Asset Pack</Trans>
|
||||
) : (
|
||||
<Trans>{numberOfAssetPacks} Asset Packs</Trans>
|
||||
)}
|
||||
</Text>
|
||||
</LineStackLayout>
|
||||
)}
|
||||
{numberOfGameTemplates > 0 && (
|
||||
<LineStackLayout noMargin alignItems="center">
|
||||
<Hammer />
|
||||
<Text noMargin>
|
||||
{numberOfGameTemplates === 1 ? (
|
||||
<Trans>{numberOfGameTemplates} Game Template</Trans>
|
||||
) : (
|
||||
<Trans>{numberOfGameTemplates} Game Templates</Trans>
|
||||
)}
|
||||
</Text>
|
||||
</LineStackLayout>
|
||||
)}
|
||||
{numberOfCourses > 0 && (
|
||||
<LineStackLayout noMargin alignItems="center">
|
||||
<School />
|
||||
<Text noMargin>
|
||||
{numberOfCourses === 1 ? (
|
||||
<Trans>{numberOfCourses} Course</Trans>
|
||||
) : (
|
||||
<Trans>{numberOfCourses} Courses</Trans>
|
||||
)}
|
||||
</Text>
|
||||
</LineStackLayout>
|
||||
)}
|
||||
{totalCredits > 0 && (
|
||||
<LineStackLayout noMargin alignItems="center">
|
||||
<Coin />
|
||||
<Text noMargin>
|
||||
<Trans>{totalCredits} Credits</Trans>
|
||||
</Text>
|
||||
</LineStackLayout>
|
||||
)}
|
||||
{includedRedemptionCodes.length > 0 &&
|
||||
includedRedemptionCodes.map((redemptionCode, index) => (
|
||||
<Line
|
||||
noMargin
|
||||
alignItems="center"
|
||||
key={`${redemptionCode.givenSubscriptionPlanId}-${index}`}
|
||||
>
|
||||
{getPlanIcon({
|
||||
planId: redemptionCode.givenSubscriptionPlanId,
|
||||
logoSize: 15,
|
||||
})}
|
||||
<Text>
|
||||
<Trans>
|
||||
{formatDurationOfRedemptionCode(
|
||||
redemptionCode.durationInDays
|
||||
)}{' '}
|
||||
of
|
||||
{getPlanInferredNameFromId(
|
||||
redemptionCode.givenSubscriptionPlanId
|
||||
)}
|
||||
subscription
|
||||
</Trans>
|
||||
</Text>
|
||||
</Line>
|
||||
))}
|
||||
</ColumnStackLayout>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const getColumnsFromWindowSize = (windowSize: WindowSizeType) => {
|
||||
if (windowSize === 'xlarge') return 6;
|
||||
if (windowSize === 'large') return 4;
|
||||
if (windowSize === 'medium') return 3;
|
||||
return 2;
|
||||
};
|
||||
|
||||
type Props = {|
|
||||
onDisplayBundle: (bundleListingData: BundleListingData) => void,
|
||||
|};
|
||||
|
||||
const BundlePreviewBanner = ({ onDisplayBundle }: Props) => {
|
||||
const { isMobile, isLandscape, windowSize } = useResponsiveWindowSize();
|
||||
const numberOfTilesToDisplay = getColumnsFromWindowSize(windowSize) - 1; // Reserve one tile for the bundle preview.
|
||||
const { privateGameTemplateListingDatas } = React.useContext(
|
||||
PrivateGameTemplateStoreContext
|
||||
);
|
||||
const { creditsPackageListingDatas } = React.useContext(
|
||||
CreditsPackageStoreContext
|
||||
);
|
||||
const { bundleListingDatas } = React.useContext(BundleStoreContext);
|
||||
const { privateAssetPackListingDatas } = React.useContext(AssetStoreContext);
|
||||
const { listedCourses } = React.useContext(CourseStoreContext);
|
||||
const authenticatedUser = React.useContext(AuthenticatedUserContext);
|
||||
const { bundlePurchases, receivedBundles } = authenticatedUser;
|
||||
|
||||
// For the moment, we either display:
|
||||
// - the first bundle in the list if none are owned.
|
||||
// - the first owned bundle (as a listing data if still listed, or as an archived listing data otherwise)
|
||||
// TODO: improve that logic when we'll have more bundles.
|
||||
const bundleListingData: BundleListingData | null = React.useMemo(
|
||||
() => {
|
||||
if (!bundleListingDatas || !receivedBundles) return null;
|
||||
if (receivedBundles.length === 0) {
|
||||
return bundleListingDatas[0]; // Display the first bundle if none are owned.
|
||||
}
|
||||
const receivedBundle = receivedBundles[0];
|
||||
const bundleListingData = bundleListingDatas.find(
|
||||
bundleListingData => bundleListingData.id === receivedBundle.id
|
||||
);
|
||||
if (bundleListingData) {
|
||||
return bundleListingData; // Display the first owned bundle that is still listed.
|
||||
}
|
||||
// If this bundle is not listed anymore, get an archived listing data for that bundle.
|
||||
return getArchivedBundleListingData({
|
||||
bundle: receivedBundle,
|
||||
});
|
||||
},
|
||||
[bundleListingDatas, receivedBundles]
|
||||
);
|
||||
|
||||
const userBundlePurchaseUsageType = React.useMemo(
|
||||
() => {
|
||||
if (!bundleListingData) return null;
|
||||
return getUserProductPurchaseUsageType({
|
||||
productId: bundleListingData.id,
|
||||
receivedProducts: receivedBundles,
|
||||
productPurchases: bundlePurchases,
|
||||
allProductListingDatas: bundleListingDatas,
|
||||
});
|
||||
},
|
||||
[bundlePurchases, bundleListingData, bundleListingDatas, receivedBundles]
|
||||
);
|
||||
const isAlreadyReceived = !!userBundlePurchaseUsageType;
|
||||
|
||||
const productListingDatasIncludedInBundle = React.useMemo(
|
||||
() =>
|
||||
bundleListingData
|
||||
? getProductsIncludedInBundle({
|
||||
productListingDatas: [
|
||||
...(bundleListingDatas || []),
|
||||
...(privateGameTemplateListingDatas || []),
|
||||
...(privateAssetPackListingDatas || []),
|
||||
...(listedCourses || []),
|
||||
...(creditsPackageListingDatas || []),
|
||||
],
|
||||
productListingData: bundleListingData,
|
||||
})
|
||||
: null,
|
||||
[
|
||||
bundleListingData,
|
||||
bundleListingDatas,
|
||||
privateGameTemplateListingDatas,
|
||||
privateAssetPackListingDatas,
|
||||
listedCourses,
|
||||
creditsPackageListingDatas,
|
||||
]
|
||||
);
|
||||
|
||||
const redemptionCodesIncludedInBundle = React.useMemo(
|
||||
() =>
|
||||
bundleListingData
|
||||
? bundleListingData.includedRedemptionCodes || []
|
||||
: null,
|
||||
[bundleListingData]
|
||||
);
|
||||
|
||||
const courseTiles = React.useMemo(
|
||||
() => {
|
||||
if (isMobile && !isLandscape) {
|
||||
return null; // Don't display course tiles on mobile, they take too much space.
|
||||
}
|
||||
if (!productListingDatasIncludedInBundle) {
|
||||
return new Array(numberOfTilesToDisplay).fill(0).map((_, index) => (
|
||||
<React.Fragment key={`skeleton-${index}`}>
|
||||
{index > 0 &&
|
||||
(isMobile && !isLandscape ? (
|
||||
<Column noMargin>
|
||||
<Divider orientation="horizontal" />
|
||||
</Column>
|
||||
) : (
|
||||
<Line noMargin>
|
||||
<Divider orientation="vertical" />
|
||||
</Line>
|
||||
))}
|
||||
{index > 0 && <Spacer />}
|
||||
<Column expand>
|
||||
<Skeleton height={140} />
|
||||
<Skeleton height={20} />
|
||||
<Skeleton height={20} />
|
||||
<LargeSpacer />
|
||||
</Column>
|
||||
</React.Fragment>
|
||||
));
|
||||
}
|
||||
|
||||
const coursesIncludedInBundle = productListingDatasIncludedInBundle.filter(
|
||||
productListingData => productListingData.productType === 'COURSE'
|
||||
);
|
||||
|
||||
return new Array(numberOfTilesToDisplay).fill(0).map((_, index) => {
|
||||
const courseListingData: ?CourseListingData =
|
||||
// $FlowFixMe
|
||||
coursesIncludedInBundle[index];
|
||||
if (!courseListingData) {
|
||||
return <div style={{ flex: 1 }} key={`empty-tile-${index}`} />;
|
||||
}
|
||||
|
||||
return (
|
||||
<React.Fragment key={`course-${courseListingData.id}`}>
|
||||
{index > 0 && (
|
||||
<Line noMargin>
|
||||
<Divider orientation="vertical" />
|
||||
</Line>
|
||||
)}
|
||||
{index > 0 && <Spacer />}
|
||||
<CourseTile
|
||||
courseListingData={courseListingData}
|
||||
isAlreadyReceived={isAlreadyReceived}
|
||||
/>
|
||||
</React.Fragment>
|
||||
);
|
||||
});
|
||||
},
|
||||
[
|
||||
isMobile,
|
||||
isLandscape,
|
||||
numberOfTilesToDisplay,
|
||||
productListingDatasIncludedInBundle,
|
||||
isAlreadyReceived,
|
||||
]
|
||||
);
|
||||
|
||||
return (
|
||||
<I18n>
|
||||
{({ i18n }) => (
|
||||
<Paper
|
||||
background="medium"
|
||||
variant="outlined"
|
||||
style={{
|
||||
...styles.container,
|
||||
padding: isMobile ? mobilePadding : desktopPadding,
|
||||
}}
|
||||
>
|
||||
<Column expand noMargin>
|
||||
<ResponsiveLineStackLayout
|
||||
noResponsiveLandscape
|
||||
noMargin
|
||||
noColumnMargin
|
||||
noOverflowParent
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
...styles.leftColumn,
|
||||
width: isMobile && !isLandscape ? '100%' : undefined,
|
||||
}}
|
||||
>
|
||||
<ColumnStackLayout noMargin>
|
||||
<Line noMargin>
|
||||
{bundleListingData ? (
|
||||
<Chip
|
||||
label={
|
||||
isAlreadyReceived ? (
|
||||
<Trans>Owned</Trans>
|
||||
) : (
|
||||
<Trans>Discount</Trans>
|
||||
)
|
||||
}
|
||||
style={
|
||||
isAlreadyReceived
|
||||
? styles.ownedChip
|
||||
: styles.discountChip
|
||||
}
|
||||
/>
|
||||
) : (
|
||||
<Skeleton variant="rect" height={20} />
|
||||
)}
|
||||
</Line>
|
||||
<Spacer />
|
||||
{bundleListingData ? (
|
||||
<Text noMargin size="block-title">
|
||||
{bundleListingData.nameByLocale
|
||||
? selectMessageByLocale(
|
||||
i18n,
|
||||
bundleListingData.nameByLocale
|
||||
)
|
||||
: bundleListingData.name}
|
||||
</Text>
|
||||
) : (
|
||||
<Skeleton height={30} />
|
||||
)}
|
||||
{bundleListingData ? (
|
||||
<Text noMargin>
|
||||
{bundleListingData.descriptionByLocale
|
||||
? selectMessageByLocale(
|
||||
i18n,
|
||||
bundleListingData.descriptionByLocale
|
||||
)
|
||||
: bundleListingData.description}
|
||||
</Text>
|
||||
) : (
|
||||
<Skeleton height={30} />
|
||||
)}
|
||||
</ColumnStackLayout>
|
||||
{bundleListingData ? (
|
||||
<ColumnStackLayout noMargin>
|
||||
{!isAlreadyReceived && (
|
||||
<Text noMargin color="secondary">
|
||||
<span style={styles.discountedPrice}>
|
||||
{renderEstimatedTotalPriceFormatted({
|
||||
i18n,
|
||||
bundleListingData,
|
||||
productListingDatasIncludedInBundle,
|
||||
redemptionCodesIncludedInBundle,
|
||||
})}
|
||||
</span>
|
||||
</Text>
|
||||
)}
|
||||
<RaisedButton
|
||||
primary
|
||||
label={
|
||||
isAlreadyReceived ? (
|
||||
<Trans>Browse bundle</Trans>
|
||||
) : (
|
||||
<Trans>
|
||||
Buy for{' '}
|
||||
{renderProductPrice({
|
||||
i18n,
|
||||
productListingData: bundleListingData,
|
||||
usageType: 'default',
|
||||
plainText: true,
|
||||
})}
|
||||
</Trans>
|
||||
)
|
||||
}
|
||||
onClick={() => onDisplayBundle(bundleListingData)}
|
||||
/>
|
||||
</ColumnStackLayout>
|
||||
) : (
|
||||
<ColumnStackLayout noMargin>
|
||||
<Skeleton variant="rect" height={40} />
|
||||
</ColumnStackLayout>
|
||||
)}
|
||||
</div>
|
||||
{courseTiles}
|
||||
<BundlePreviewTile bundleListingData={bundleListingData} />
|
||||
</ResponsiveLineStackLayout>
|
||||
</Column>
|
||||
</Paper>
|
||||
)}
|
||||
</I18n>
|
||||
);
|
||||
};
|
||||
|
||||
export default BundlePreviewBanner;
|
@@ -1,373 +0,0 @@
|
||||
// @flow
|
||||
import * as React from 'react';
|
||||
import { t, Trans } from '@lingui/macro';
|
||||
import { type BundleListingData } from '../../Utils/GDevelopServices/Shop';
|
||||
import Dialog, { DialogPrimaryButton } from '../../UI/Dialog';
|
||||
import AuthenticatedUserContext from '../../Profile/AuthenticatedUserContext';
|
||||
import CreateProfile from '../../Profile/CreateProfile';
|
||||
import Text from '../../UI/Text';
|
||||
import { useInterval } from '../../Utils/UseInterval';
|
||||
import { getPurchaseCheckoutUrl } from '../../Utils/GDevelopServices/Shop';
|
||||
import Window from '../../Utils/Window';
|
||||
import { Line, Spacer } from '../../UI/Grid';
|
||||
import CircularProgress from '../../UI/CircularProgress';
|
||||
import BackgroundText from '../../UI/BackgroundText';
|
||||
import Mark from '../../UI/CustomSvgIcons/Mark';
|
||||
import FlatButton from '../../UI/FlatButton';
|
||||
import { ColumnStackLayout, LineStackLayout } from '../../UI/Layout';
|
||||
import useAlertDialog from '../../UI/Alert/useAlertDialog';
|
||||
import {
|
||||
shouldUseAppStoreProduct,
|
||||
purchaseAppStoreProduct,
|
||||
} from '../../Utils/AppStorePurchases';
|
||||
import { extractGDevelopApiErrorStatusAndCode } from '../../Utils/GDevelopServices/Errors';
|
||||
import PasswordPromptDialog from '../PasswordPromptDialog';
|
||||
|
||||
type Props = {|
|
||||
bundleListingData: BundleListingData,
|
||||
usageType: string,
|
||||
onClose: () => void,
|
||||
simulateAppStoreProduct?: boolean,
|
||||
|};
|
||||
|
||||
const BundlePurchaseDialog = ({
|
||||
bundleListingData,
|
||||
usageType,
|
||||
onClose,
|
||||
simulateAppStoreProduct,
|
||||
}: Props) => {
|
||||
const {
|
||||
profile,
|
||||
onOpenLoginDialog,
|
||||
onOpenCreateAccountDialog,
|
||||
receivedBundles,
|
||||
onPurchaseSuccessful,
|
||||
onRefreshBundlePurchases,
|
||||
bundlePurchases,
|
||||
} = React.useContext(AuthenticatedUserContext);
|
||||
const [isPurchasing, setIsPurchasing] = React.useState(false);
|
||||
const [
|
||||
isCheckingPurchasesAfterLogin,
|
||||
setIsCheckingPurchasesAfterLogin,
|
||||
] = React.useState(!receivedBundles);
|
||||
const [purchaseSuccessful, setPurchaseSuccessful] = React.useState(false);
|
||||
const [
|
||||
displayPasswordPrompt,
|
||||
setDisplayPasswordPrompt,
|
||||
] = React.useState<boolean>(false);
|
||||
const [password, setPassword] = React.useState<string>('');
|
||||
const { showAlert } = useAlertDialog();
|
||||
|
||||
const shouldUseOrSimulateAppStoreProduct =
|
||||
shouldUseAppStoreProduct() || simulateAppStoreProduct;
|
||||
|
||||
const onStartPurchase = async () => {
|
||||
if (!profile) return;
|
||||
setDisplayPasswordPrompt(false);
|
||||
|
||||
// Purchase with the App Store.
|
||||
if (shouldUseOrSimulateAppStoreProduct) {
|
||||
try {
|
||||
setIsPurchasing(true);
|
||||
await purchaseAppStoreProduct(bundleListingData.appStoreProductId);
|
||||
} finally {
|
||||
setIsPurchasing(false);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
const price = bundleListingData.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 bundle. Please try again later.`,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// Purchase with web.
|
||||
try {
|
||||
setIsPurchasing(true);
|
||||
const checkoutUrl = getPurchaseCheckoutUrl({
|
||||
productId: bundleListingData.id,
|
||||
priceName: price.name,
|
||||
userId: profile.id,
|
||||
userEmail: profile.email,
|
||||
...(password ? { password } : undefined),
|
||||
});
|
||||
Window.openExternalURL(checkoutUrl);
|
||||
} catch (error) {
|
||||
const extractedStatusAndCode = extractGDevelopApiErrorStatusAndCode(
|
||||
error
|
||||
);
|
||||
if (
|
||||
extractedStatusAndCode &&
|
||||
extractedStatusAndCode.status === 403 &&
|
||||
extractedStatusAndCode.code === 'auth/wrong-password'
|
||||
) {
|
||||
await showAlert({
|
||||
title: t`Operation not allowed`,
|
||||
message: t`The password you entered is incorrect. Please try again.`,
|
||||
});
|
||||
} else {
|
||||
console.error('Unable to get the checkout URL', error);
|
||||
await showAlert({
|
||||
title: t`An error happened`,
|
||||
message: t`Unable to get the checkout URL. Please try again later.`,
|
||||
});
|
||||
}
|
||||
setIsPurchasing(false);
|
||||
} finally {
|
||||
setPassword('');
|
||||
}
|
||||
};
|
||||
|
||||
const onWillPurchase = () => {
|
||||
// Password is required in dev environment only so that one cannot freely purchase bundles.
|
||||
if (Window.isDev()) setDisplayPasswordPrompt(true);
|
||||
else onStartPurchase();
|
||||
};
|
||||
|
||||
React.useEffect(
|
||||
() => {
|
||||
onWillPurchase();
|
||||
},
|
||||
// Launch the start process directly when the dialog is opened, to avoid an extra click.
|
||||
// eslint-disable-next-line
|
||||
[]
|
||||
);
|
||||
|
||||
// This effect will be triggered when the bundle purchases change,
|
||||
// to check if the user has just bought the product.
|
||||
React.useEffect(
|
||||
() => {
|
||||
const checkIfPurchaseIsDone = async () => {
|
||||
if (
|
||||
isPurchasing &&
|
||||
bundlePurchases &&
|
||||
bundlePurchases.find(
|
||||
userPurchase => userPurchase.productId === bundleListingData.id
|
||||
)
|
||||
) {
|
||||
// We found the purchase, the user has bought the bundle.
|
||||
// We do not close the dialog yet, as we need to trigger a refresh of the products received.
|
||||
await onPurchaseSuccessful();
|
||||
}
|
||||
};
|
||||
checkIfPurchaseIsDone();
|
||||
},
|
||||
[
|
||||
isPurchasing,
|
||||
bundlePurchases,
|
||||
bundleListingData,
|
||||
onPurchaseSuccessful,
|
||||
onRefreshBundlePurchases,
|
||||
]
|
||||
);
|
||||
|
||||
useInterval(
|
||||
() => {
|
||||
onRefreshBundlePurchases();
|
||||
},
|
||||
isPurchasing ? 3900 : null
|
||||
);
|
||||
|
||||
// Listen to the received bundle, to know when a user has just logged in and the received bundles have been loaded.
|
||||
// In this case, start a timeout to remove the loader and give some time for the store to refresh.
|
||||
React.useEffect(
|
||||
() => {
|
||||
let timeoutId;
|
||||
(async () => {
|
||||
if (receivedBundles) {
|
||||
timeoutId = setTimeout(
|
||||
() => setIsCheckingPurchasesAfterLogin(false),
|
||||
3000
|
||||
);
|
||||
}
|
||||
})();
|
||||
return () => {
|
||||
clearTimeout(timeoutId);
|
||||
};
|
||||
},
|
||||
[receivedBundles]
|
||||
);
|
||||
|
||||
// If the user has received this particular bundle, either:
|
||||
// - they just logged in, and already have it, so we close the dialog.
|
||||
// - they just bought it, we display the success message.
|
||||
React.useEffect(
|
||||
() => {
|
||||
if (receivedBundles) {
|
||||
const receivedBundle = receivedBundles.find(
|
||||
bundle => bundle.id === bundleListingData.id
|
||||
);
|
||||
if (receivedBundle) {
|
||||
if (isPurchasing) {
|
||||
setIsPurchasing(false);
|
||||
setPurchaseSuccessful(true);
|
||||
} else if (!purchaseSuccessful) {
|
||||
onClose();
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
[
|
||||
receivedBundles,
|
||||
bundleListingData,
|
||||
isPurchasing,
|
||||
onClose,
|
||||
isCheckingPurchasesAfterLogin,
|
||||
purchaseSuccessful,
|
||||
]
|
||||
);
|
||||
|
||||
const dialogContents = !profile
|
||||
? {
|
||||
subtitle: <Trans>Log-in to purchase this item</Trans>,
|
||||
content: (
|
||||
<CreateProfile
|
||||
onOpenLoginDialog={onOpenLoginDialog}
|
||||
onOpenCreateAccountDialog={onOpenCreateAccountDialog}
|
||||
message={
|
||||
<Trans>
|
||||
Bundles and their content will be linked to your user account
|
||||
and available for all your projects. Log-in or sign-up to
|
||||
purchase this bundle. (or restore your existing purchase).
|
||||
</Trans>
|
||||
}
|
||||
justifyContent="center"
|
||||
/>
|
||||
),
|
||||
}
|
||||
: purchaseSuccessful
|
||||
? {
|
||||
subtitle: <Trans>Your purchase has been processed!</Trans>,
|
||||
content: (
|
||||
<Line justifyContent="center" alignItems="center">
|
||||
<Text>
|
||||
<Trans>You can now go back to use your new bundle.</Trans>
|
||||
</Text>
|
||||
</Line>
|
||||
),
|
||||
}
|
||||
: isPurchasing
|
||||
? {
|
||||
subtitle: shouldUseOrSimulateAppStoreProduct ? (
|
||||
<Trans>Complete your purchase with the app store.</Trans>
|
||||
) : (
|
||||
<Trans>Complete your payment on the web browser</Trans>
|
||||
),
|
||||
content: shouldUseOrSimulateAppStoreProduct ? (
|
||||
<>
|
||||
<ColumnStackLayout justifyContent="center" alignItems="center">
|
||||
<CircularProgress size={40} />
|
||||
<Text>
|
||||
<Trans>
|
||||
The purchase will be linked to your account once done.
|
||||
</Trans>
|
||||
</Text>
|
||||
</ColumnStackLayout>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Line justifyContent="center" alignItems="center">
|
||||
<CircularProgress size={20} />
|
||||
<Spacer />
|
||||
<Text>
|
||||
<Trans>Waiting for the purchase confirmation...</Trans>
|
||||
</Text>
|
||||
</Line>
|
||||
<Spacer />
|
||||
<Line justifyContent="center">
|
||||
<BackgroundText>
|
||||
<Trans>
|
||||
Once you're done, come back to GDevelop and the bundle will be
|
||||
added to your account automatically.
|
||||
</Trans>
|
||||
</BackgroundText>
|
||||
</Line>
|
||||
</>
|
||||
),
|
||||
}
|
||||
: isCheckingPurchasesAfterLogin
|
||||
? {
|
||||
subtitle: <Trans>Loading your profile...</Trans>,
|
||||
content: (
|
||||
<Line justifyContent="center" alignItems="center">
|
||||
<CircularProgress size={20} />
|
||||
</Line>
|
||||
),
|
||||
}
|
||||
: {
|
||||
subtitle: (
|
||||
<Trans>
|
||||
The bundle {bundleListingData.name} will be linked to your account{' '}
|
||||
{profile.email}.
|
||||
</Trans>
|
||||
),
|
||||
content: shouldUseOrSimulateAppStoreProduct ? null : (
|
||||
<Line justifyContent="center" alignItems="center">
|
||||
<Text>
|
||||
<Trans>
|
||||
A new secure window will open to complete the purchase.
|
||||
</Trans>
|
||||
</Text>
|
||||
</Line>
|
||||
),
|
||||
};
|
||||
|
||||
const allowPurchase =
|
||||
profile &&
|
||||
!isPurchasing &&
|
||||
!purchaseSuccessful &&
|
||||
!isCheckingPurchasesAfterLogin;
|
||||
const dialogActions = [
|
||||
<FlatButton
|
||||
key="cancel"
|
||||
label={purchaseSuccessful ? <Trans>Close</Trans> : <Trans>Cancel</Trans>}
|
||||
onClick={onClose}
|
||||
/>,
|
||||
allowPurchase ? (
|
||||
<DialogPrimaryButton
|
||||
key="continue"
|
||||
primary
|
||||
label={<Trans>Continue</Trans>}
|
||||
onClick={onWillPurchase}
|
||||
/>
|
||||
) : null,
|
||||
];
|
||||
|
||||
return (
|
||||
<>
|
||||
<Dialog
|
||||
title={<Trans>{bundleListingData.name}</Trans>}
|
||||
maxWidth="sm"
|
||||
open
|
||||
onRequestClose={onClose}
|
||||
actions={dialogActions}
|
||||
onApply={purchaseSuccessful ? onClose : onWillPurchase}
|
||||
cannotBeDismissed // Prevent the user from continuing by clicking outside.
|
||||
flexColumnBody
|
||||
>
|
||||
<LineStackLayout justifyContent="center" alignItems="center">
|
||||
{purchaseSuccessful && <Mark />}
|
||||
<Text size="sub-title">{dialogContents.subtitle}</Text>
|
||||
</LineStackLayout>
|
||||
{dialogContents.content}
|
||||
</Dialog>
|
||||
{displayPasswordPrompt && (
|
||||
<PasswordPromptDialog
|
||||
onApply={onStartPurchase}
|
||||
onClose={() => setDisplayPasswordPrompt(false)}
|
||||
passwordValue={password}
|
||||
setPasswordValue={setPassword}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default BundlePurchaseDialog;
|
@@ -1,265 +0,0 @@
|
||||
// @flow
|
||||
import * as React from 'react';
|
||||
import { type FiltersState } from '../../UI/Search/FiltersChooser';
|
||||
import { type Filters } from '../../Utils/GDevelopServices/Filters';
|
||||
import { useSearchItem } from '../../UI/Search/UseSearchItem';
|
||||
import {
|
||||
listListedBundles,
|
||||
type BundleListingData,
|
||||
} from '../../Utils/GDevelopServices/Shop';
|
||||
import { capitalize } from 'lodash';
|
||||
import { AssetStoreNavigatorContext } from '../AssetStoreNavigator';
|
||||
import { getBundleListingDataFromUserFriendlySlug } from '../AssetStoreUtils';
|
||||
import useAlertDialog from '../../UI/Alert/useAlertDialog';
|
||||
import { t } from '@lingui/macro';
|
||||
import { sendBundleInformationOpened } from '../../Utils/Analytics/EventSender';
|
||||
import { BUNDLES_FETCH_TIMEOUT } from '../../Utils/GlobalFetchTimeouts';
|
||||
import AuthenticatedUserContext from '../../Profile/AuthenticatedUserContext';
|
||||
|
||||
const getBundleListingDataSearchTerms = (bundle: BundleListingData) =>
|
||||
bundle.name + '\n' + bundle.description + '\n' + bundle.categories.join('\n');
|
||||
|
||||
type BundleStoreState = {|
|
||||
bundleFilters: ?Filters,
|
||||
fetchBundles: () => void,
|
||||
bundleListingDatas: ?Array<BundleListingData>,
|
||||
error: ?Error,
|
||||
shop: {
|
||||
bundleListingDatasSearchResults: ?Array<BundleListingData>,
|
||||
filtersState: FiltersState,
|
||||
setInitialBundleUserFriendlySlug: string => void,
|
||||
},
|
||||
|};
|
||||
|
||||
export const initialBundleStoreState: BundleStoreState = {
|
||||
bundleFilters: null,
|
||||
fetchBundles: () => {},
|
||||
bundleListingDatas: null,
|
||||
error: null,
|
||||
shop: {
|
||||
bundleListingDatasSearchResults: null,
|
||||
filtersState: {
|
||||
chosenFilters: new Set(),
|
||||
addFilter: () => {},
|
||||
removeFilter: () => {},
|
||||
chosenCategory: null,
|
||||
setChosenCategory: () => {},
|
||||
},
|
||||
setInitialBundleUserFriendlySlug: (
|
||||
initialBundleUserFriendlySlug: string
|
||||
) => {},
|
||||
},
|
||||
};
|
||||
|
||||
export const BundleStoreContext = React.createContext<BundleStoreState>(
|
||||
initialBundleStoreState
|
||||
);
|
||||
|
||||
type BundleStoreStateProviderProps = {|
|
||||
children: React.Node,
|
||||
|};
|
||||
|
||||
export const BundleStoreStateProvider = ({
|
||||
children,
|
||||
}: BundleStoreStateProviderProps) => {
|
||||
const shopNavigationState = React.useContext(AssetStoreNavigatorContext);
|
||||
const {
|
||||
searchText: shopSearchText,
|
||||
setSearchText: setShopSearchText,
|
||||
} = shopNavigationState;
|
||||
const { limits } = React.useContext(AuthenticatedUserContext);
|
||||
|
||||
const [bundleFilters, setBundleFilters] = React.useState<?Filters>(null);
|
||||
const [error, setError] = React.useState<?Error>(null);
|
||||
const [
|
||||
bundleListingDatas,
|
||||
setBundleListingDatas,
|
||||
] = React.useState<?Array<BundleListingData>>(null);
|
||||
const [
|
||||
initialBundleUserFriendlySlug,
|
||||
setInitialBundleUserFriendlySlug,
|
||||
] = React.useState<?string>(null);
|
||||
const initialBundleOpened = React.useRef<boolean>(false);
|
||||
|
||||
const isLoading = React.useRef<boolean>(false);
|
||||
const { showAlert } = useAlertDialog();
|
||||
|
||||
const hidePremiumProducts =
|
||||
!!limits &&
|
||||
!!limits.capabilities.classrooms &&
|
||||
limits.capabilities.classrooms.hidePremiumProducts;
|
||||
|
||||
const fetchBundles = React.useCallback(
|
||||
() => {
|
||||
// If the bundles are already loaded, don't load them again.
|
||||
if (isLoading.current || bundleListingDatas) return;
|
||||
|
||||
(async () => {
|
||||
setError(null);
|
||||
isLoading.current = true;
|
||||
|
||||
try {
|
||||
const fetchedBundleListingDatas = await listListedBundles();
|
||||
|
||||
console.info(
|
||||
`Loaded ${
|
||||
fetchedBundleListingDatas ? fetchedBundleListingDatas.length : 0
|
||||
} bundles from the store.`
|
||||
);
|
||||
|
||||
setBundleListingDatas(fetchedBundleListingDatas);
|
||||
const defaultTags = fetchedBundleListingDatas.reduce(
|
||||
(allCategories, bundleListingData) => {
|
||||
return allCategories.concat(
|
||||
bundleListingData.categories.map(category =>
|
||||
capitalize(category)
|
||||
)
|
||||
);
|
||||
},
|
||||
[]
|
||||
);
|
||||
const uniqueDefaultTags = Array.from(new Set(defaultTags));
|
||||
const bundleFilters: Filters = {
|
||||
allTags: [],
|
||||
defaultTags: uniqueDefaultTags,
|
||||
tagsTree: [],
|
||||
};
|
||||
setBundleFilters(bundleFilters);
|
||||
} catch (error) {
|
||||
console.error(`Unable to load the bundles from the store:`, error);
|
||||
setError(error);
|
||||
}
|
||||
|
||||
isLoading.current = false;
|
||||
})();
|
||||
},
|
||||
[bundleListingDatas]
|
||||
);
|
||||
|
||||
// When the bundles are loaded,
|
||||
// open the bundle with the slug that was asked to be initially loaded.
|
||||
React.useEffect(
|
||||
() => {
|
||||
if (!initialBundleUserFriendlySlug || initialBundleOpened.current) {
|
||||
// If there is no initial bundle or
|
||||
// if the bundle was already opened, don't re-open it again even
|
||||
// if the effect run again.
|
||||
return;
|
||||
}
|
||||
|
||||
if (bundleListingDatas && initialBundleUserFriendlySlug) {
|
||||
initialBundleOpened.current = true;
|
||||
|
||||
// Open the information page of a the bundle.
|
||||
const bundleListingData = getBundleListingDataFromUserFriendlySlug({
|
||||
bundleListingDatas,
|
||||
userFriendlySlug: initialBundleUserFriendlySlug,
|
||||
});
|
||||
|
||||
if (bundleListingData) {
|
||||
sendBundleInformationOpened({
|
||||
bundleName: bundleListingData.name,
|
||||
bundleId: bundleListingData.id,
|
||||
source: 'web-link',
|
||||
});
|
||||
shopNavigationState.openBundleInformationPage({
|
||||
bundleListingData,
|
||||
storeSearchText: true,
|
||||
clearSearchText: false,
|
||||
});
|
||||
initialBundleOpened.current = false; // Allow to open the bundle again if the effect run again.
|
||||
setInitialBundleUserFriendlySlug(null);
|
||||
return;
|
||||
}
|
||||
|
||||
showAlert({
|
||||
title: t`Bundle not found`,
|
||||
message: t`The link to the bundle you've followed seems outdated. Why not take a look at the other bundles in the store?`,
|
||||
});
|
||||
}
|
||||
},
|
||||
[
|
||||
bundleListingDatas,
|
||||
shopNavigationState,
|
||||
showAlert,
|
||||
initialBundleUserFriendlySlug,
|
||||
]
|
||||
);
|
||||
|
||||
React.useEffect(
|
||||
() => {
|
||||
if (isLoading.current) return;
|
||||
|
||||
const timeoutId = setTimeout(() => {
|
||||
console.info('Pre-fetching bundles from the store...');
|
||||
fetchBundles();
|
||||
}, BUNDLES_FETCH_TIMEOUT);
|
||||
return () => clearTimeout(timeoutId);
|
||||
},
|
||||
[fetchBundles]
|
||||
);
|
||||
|
||||
const bundleListingDatasById = React.useMemo(
|
||||
() => {
|
||||
if (!bundleListingDatas) {
|
||||
return null;
|
||||
}
|
||||
const bundleListingDatasById = {};
|
||||
if (hidePremiumProducts) return bundleListingDatasById;
|
||||
bundleListingDatas.forEach(bundleListingData => {
|
||||
const id = bundleListingData.id;
|
||||
if (bundleListingDatasById[id]) {
|
||||
console.warn(`Multiple bundles with the same id: ${id}`);
|
||||
}
|
||||
bundleListingDatasById[id] = bundleListingData;
|
||||
});
|
||||
return bundleListingDatasById;
|
||||
},
|
||||
[bundleListingDatas, hidePremiumProducts]
|
||||
);
|
||||
|
||||
const currentPage = shopNavigationState.getCurrentPage();
|
||||
|
||||
const bundleListingDatasSearchResultsForShop: ?Array<BundleListingData> = useSearchItem(
|
||||
bundleListingDatasById,
|
||||
getBundleListingDataSearchTerms,
|
||||
shopSearchText,
|
||||
currentPage.filtersState.chosenCategory,
|
||||
currentPage.filtersState.chosenFilters
|
||||
);
|
||||
|
||||
const BundleStoreState = React.useMemo(
|
||||
() => ({
|
||||
bundleListingDatas: hidePremiumProducts ? [] : bundleListingDatas,
|
||||
error,
|
||||
bundleFilters,
|
||||
fetchBundles,
|
||||
shop: {
|
||||
bundleListingDatasSearchResults: hidePremiumProducts
|
||||
? []
|
||||
: bundleListingDatasSearchResultsForShop,
|
||||
searchText: shopSearchText,
|
||||
setSearchText: setShopSearchText,
|
||||
filtersState: currentPage.filtersState,
|
||||
setInitialBundleUserFriendlySlug,
|
||||
},
|
||||
}),
|
||||
[
|
||||
hidePremiumProducts,
|
||||
bundleListingDatas,
|
||||
error,
|
||||
bundleFilters,
|
||||
fetchBundles,
|
||||
bundleListingDatasSearchResultsForShop,
|
||||
shopSearchText,
|
||||
setShopSearchText,
|
||||
currentPage.filtersState,
|
||||
]
|
||||
);
|
||||
|
||||
return (
|
||||
<BundleStoreContext.Provider value={BundleStoreState}>
|
||||
{children}
|
||||
</BundleStoreContext.Provider>
|
||||
);
|
||||
};
|
@@ -1,142 +0,0 @@
|
||||
// @flow
|
||||
import { type I18n as I18nType } from '@lingui/core';
|
||||
import {
|
||||
type BundleListingData,
|
||||
type PrivateAssetPackListingData,
|
||||
type PrivateGameTemplateListingData,
|
||||
type CourseListingData,
|
||||
type CreditsPackageListingData,
|
||||
type IncludedRedemptionCode,
|
||||
} from '../../Utils/GDevelopServices/Shop';
|
||||
|
||||
export const renderEstimatedTotalPriceFormatted = ({
|
||||
i18n,
|
||||
bundleListingData,
|
||||
productListingDatasIncludedInBundle,
|
||||
redemptionCodesIncludedInBundle,
|
||||
}: {
|
||||
i18n: I18nType,
|
||||
bundleListingData: ?BundleListingData,
|
||||
productListingDatasIncludedInBundle: ?Array<
|
||||
| PrivateAssetPackListingData
|
||||
| PrivateGameTemplateListingData
|
||||
| BundleListingData
|
||||
| CourseListingData
|
||||
| CreditsPackageListingData
|
||||
>,
|
||||
redemptionCodesIncludedInBundle: ?Array<IncludedRedemptionCode>,
|
||||
}): ?string => {
|
||||
let totalPrice = 0;
|
||||
if (
|
||||
!bundleListingData ||
|
||||
!productListingDatasIncludedInBundle ||
|
||||
!redemptionCodesIncludedInBundle
|
||||
)
|
||||
return null;
|
||||
|
||||
const productPrices = bundleListingData.prices;
|
||||
const bundlePrice = productPrices.find(
|
||||
price => price.usageType === 'default'
|
||||
);
|
||||
const currencyCode = bundlePrice ? bundlePrice.currency : 'USD';
|
||||
const currencySymbol = currencyCode === 'USD' ? '$' : '€';
|
||||
|
||||
for (const product of bundleListingData.includedListableProducts || []) {
|
||||
if (product.productType === 'ASSET_PACK') {
|
||||
const listedAssetPack =
|
||||
productListingDatasIncludedInBundle.find(
|
||||
assetPack => assetPack.id === product.productId
|
||||
) || null;
|
||||
if (listedAssetPack) {
|
||||
const price = listedAssetPack.prices.find(
|
||||
price => price.usageType === product.usageType
|
||||
);
|
||||
totalPrice += price ? price.value : 0;
|
||||
}
|
||||
} else if (product.productType === 'GAME_TEMPLATE') {
|
||||
const listedGameTemplate =
|
||||
productListingDatasIncludedInBundle.find(
|
||||
gameTemplate => gameTemplate.id === product.productId
|
||||
) || null;
|
||||
if (listedGameTemplate) {
|
||||
const price = listedGameTemplate.prices.find(
|
||||
price => price.usageType === product.usageType
|
||||
);
|
||||
totalPrice += price ? price.value : 0;
|
||||
}
|
||||
} else if (product.productType === 'COURSE') {
|
||||
const listedCourse = productListingDatasIncludedInBundle.find(
|
||||
course => course.id === product.productId
|
||||
);
|
||||
if (listedCourse) {
|
||||
const price = listedCourse.prices.find(
|
||||
price => price.usageType === product.usageType
|
||||
);
|
||||
totalPrice += price ? price.value : 0;
|
||||
}
|
||||
} else if (product.productType === 'BUNDLE') {
|
||||
const listedBundle = productListingDatasIncludedInBundle.find(
|
||||
bundle => bundle.id === product.productId
|
||||
);
|
||||
if (listedBundle) {
|
||||
const price = listedBundle.prices.find(
|
||||
price => price.usageType === product.usageType
|
||||
);
|
||||
totalPrice += price ? price.value : 0;
|
||||
}
|
||||
} else if (product.productType === 'CREDITS_PACKAGE') {
|
||||
const listedCreditsPackage =
|
||||
productListingDatasIncludedInBundle.find(
|
||||
creditsPackage => creditsPackage.id === product.productId
|
||||
) || null;
|
||||
if (listedCreditsPackage) {
|
||||
const price = listedCreditsPackage.prices.find(
|
||||
price => price.usageType === product.usageType
|
||||
);
|
||||
totalPrice += price ? price.value : 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (redemptionCodesIncludedInBundle.length > 0) {
|
||||
for (const redemptionCode of redemptionCodesIncludedInBundle) {
|
||||
const planId = redemptionCode.givenSubscriptionPlanId;
|
||||
if (planId) {
|
||||
let estimatedAmountInCents = null;
|
||||
if (redemptionCode.estimatedPrices) {
|
||||
const estimatedPrice = redemptionCode.estimatedPrices.find(
|
||||
price => price.currency === currencyCode
|
||||
);
|
||||
if (estimatedPrice) {
|
||||
estimatedAmountInCents = estimatedPrice.value;
|
||||
}
|
||||
}
|
||||
|
||||
// If no estimated price is provided, guess a mostly correct value
|
||||
// for backward compatibility.
|
||||
if (estimatedAmountInCents === null) {
|
||||
const monthlyEstimatedAmountInCents =
|
||||
planId === 'gdevelop_silver'
|
||||
? 599
|
||||
: planId === 'gdevelop_gold'
|
||||
? 1099
|
||||
: planId === 'gdevelop_startup'
|
||||
? 3499
|
||||
: 0;
|
||||
estimatedAmountInCents =
|
||||
monthlyEstimatedAmountInCents *
|
||||
Math.max(1, Math.round(redemptionCode.durationInDays / 30));
|
||||
}
|
||||
|
||||
totalPrice += estimatedAmountInCents || 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return `${currencySymbol} ${i18n
|
||||
.number(totalPrice / 100, {
|
||||
minimumFractionDigits: 2,
|
||||
maximumFractionDigits: 2,
|
||||
})
|
||||
.replace(/\D00$/, '')}`;
|
||||
};
|
@@ -22,18 +22,10 @@ import {
|
||||
isLinkedToStartingPointExampleShortHeader,
|
||||
isStartingPointExampleShortHeader,
|
||||
} from '../../ProjectCreation/EmptyAndStartingPointProjects';
|
||||
import GDevelopThemeContext from '../../UI/Theme/GDevelopThemeContext';
|
||||
import {
|
||||
useResponsiveWindowSize,
|
||||
type WindowSizeType,
|
||||
} from '../../UI/Responsive/ResponsiveWindowMeasurer';
|
||||
import { LARGE_WIDGET_SIZE } from '../../MainFrame/EditorContainers/HomePage/CardWidget';
|
||||
|
||||
const ITEMS_SPACING = 5;
|
||||
const styles = {
|
||||
grid: {
|
||||
textAlign: 'center',
|
||||
width: `calc(100% + ${2 * ITEMS_SPACING}px)`, // This is needed to compensate for the `margin: -5px` added by MUI related to spacing.
|
||||
margin: 0,
|
||||
// Remove the scroll capability of the grid, the scroll view handles it.
|
||||
overflow: 'unset',
|
||||
},
|
||||
@@ -67,13 +59,12 @@ type Props = {|
|
||||
i18n: I18nType,
|
||||
onlyShowGames?: boolean,
|
||||
hideStartingPoints?: boolean,
|
||||
getColumnsFromWindowSize: (
|
||||
windowSize: WindowSizeType,
|
||||
isLandscape: boolean
|
||||
) => number,
|
||||
columnsCount: number,
|
||||
rowToInsert?: {|
|
||||
row: number,
|
||||
element: React.Node,
|
||||
|},
|
||||
hideSearch?: boolean,
|
||||
limitRowsTo?: number,
|
||||
hidePremiumTemplates?: boolean,
|
||||
|};
|
||||
|
||||
const ExampleStore = ({
|
||||
@@ -82,15 +73,10 @@ const ExampleStore = ({
|
||||
i18n,
|
||||
onlyShowGames,
|
||||
hideStartingPoints,
|
||||
getColumnsFromWindowSize,
|
||||
columnsCount,
|
||||
rowToInsert,
|
||||
hideSearch,
|
||||
limitRowsTo,
|
||||
hidePremiumTemplates,
|
||||
}: Props) => {
|
||||
const MAX_COLUMNS = getColumnsFromWindowSize('xlarge', true);
|
||||
const MAX_SECTION_WIDTH = (LARGE_WIDGET_SIZE + 2 * 5) * MAX_COLUMNS; // widget size + 5 padding per side
|
||||
const { windowSize, isLandscape } = useResponsiveWindowSize();
|
||||
|
||||
const { receivedGameTemplates } = React.useContext(AuthenticatedUserContext);
|
||||
const {
|
||||
exampleShortHeaders: allExampleShortHeaders,
|
||||
@@ -109,8 +95,6 @@ const ExampleStore = ({
|
||||
const [localSearchText, setLocalSearchText] = React.useState(
|
||||
exampleStoreSearchText
|
||||
);
|
||||
const gdevelopTheme = React.useContext(GDevelopThemeContext);
|
||||
const columnsCount = getColumnsFromWindowSize(windowSize, isLandscape);
|
||||
|
||||
const shouldAutofocusSearchbar = useShouldAutofocusInput();
|
||||
const searchBarRef = React.useRef<?SearchBarInterface>(null);
|
||||
@@ -159,15 +143,14 @@ const ExampleStore = ({
|
||||
() => {
|
||||
return getExampleAndTemplateTiles({
|
||||
receivedGameTemplates,
|
||||
privateGameTemplateListingDatas:
|
||||
privateGameTemplateListingDatasSearchResults && !hidePremiumTemplates
|
||||
? privateGameTemplateListingDatasSearchResults
|
||||
.map(({ item }) => item)
|
||||
.filter(
|
||||
privateGameTemplateListingData =>
|
||||
!onlyShowGames || gameFilter(privateGameTemplateListingData)
|
||||
)
|
||||
: [],
|
||||
privateGameTemplateListingDatas: privateGameTemplateListingDatasSearchResults
|
||||
? privateGameTemplateListingDatasSearchResults
|
||||
.map(({ item }) => item)
|
||||
.filter(
|
||||
privateGameTemplateListingData =>
|
||||
!onlyShowGames || gameFilter(privateGameTemplateListingData)
|
||||
)
|
||||
: [],
|
||||
exampleShortHeaders: exampleShortHeadersSearchResults
|
||||
? exampleShortHeadersSearchResults
|
||||
.map(({ item }) => item)
|
||||
@@ -200,10 +183,9 @@ const ExampleStore = ({
|
||||
onSelectExampleShortHeader(exampleShortHeader);
|
||||
},
|
||||
i18n,
|
||||
gdevelopTheme,
|
||||
privateGameTemplatesPeriodicity: 1,
|
||||
showOwnedGameTemplatesFirst: true,
|
||||
});
|
||||
}).allGridItems;
|
||||
},
|
||||
[
|
||||
receivedGameTemplates,
|
||||
@@ -212,14 +194,55 @@ const ExampleStore = ({
|
||||
onSelectPrivateGameTemplateListingData,
|
||||
onSelectExampleShortHeader,
|
||||
i18n,
|
||||
gdevelopTheme,
|
||||
onlyShowGames,
|
||||
hideStartingPoints,
|
||||
allExampleShortHeaders,
|
||||
hidePremiumTemplates,
|
||||
]
|
||||
);
|
||||
|
||||
const nodesToDisplay: React.Node[] = React.useMemo(
|
||||
() => {
|
||||
const numberOfTilesToDisplayUntilRowToInsert = rowToInsert
|
||||
? rowToInsert.row * columnsCount
|
||||
: 0;
|
||||
const firstTiles = resultTiles.slice(
|
||||
0,
|
||||
numberOfTilesToDisplayUntilRowToInsert
|
||||
);
|
||||
const lastTiles = resultTiles.slice(
|
||||
numberOfTilesToDisplayUntilRowToInsert
|
||||
);
|
||||
return [
|
||||
firstTiles.length > 0 ? (
|
||||
<GridList
|
||||
cols={columnsCount}
|
||||
style={styles.grid}
|
||||
cellHeight="auto"
|
||||
spacing={2}
|
||||
key="first-tiles"
|
||||
>
|
||||
{firstTiles}
|
||||
</GridList>
|
||||
) : null,
|
||||
rowToInsert ? (
|
||||
<Line key="inserted-row">{rowToInsert.element}</Line>
|
||||
) : null,
|
||||
lastTiles.length > 0 ? (
|
||||
<GridList
|
||||
cols={columnsCount}
|
||||
style={styles.grid}
|
||||
cellHeight="auto"
|
||||
spacing={2}
|
||||
key="last-tiles"
|
||||
>
|
||||
{lastTiles}
|
||||
</GridList>
|
||||
) : null,
|
||||
].filter(Boolean);
|
||||
},
|
||||
[columnsCount, rowToInsert, resultTiles]
|
||||
);
|
||||
|
||||
return (
|
||||
<React.Fragment>
|
||||
<Column expand noMargin>
|
||||
@@ -244,24 +267,11 @@ const ExampleStore = ({
|
||||
No results returned for your search. Try something else!
|
||||
</Trans>
|
||||
</BackgroundText>
|
||||
{rowToInsert && <Line>{rowToInsert.element}</Line>}
|
||||
</Column>
|
||||
) : (
|
||||
<ColumnStackLayout noMargin expand>
|
||||
<GridList
|
||||
cols={columnsCount}
|
||||
style={{
|
||||
...styles.grid,
|
||||
// Avoid tiles taking too much space on large screens.
|
||||
maxWidth: MAX_SECTION_WIDTH,
|
||||
}}
|
||||
cellHeight="auto"
|
||||
spacing={ITEMS_SPACING * 2}
|
||||
>
|
||||
{resultTiles.slice(
|
||||
0,
|
||||
limitRowsTo ? limitRowsTo * columnsCount : Infinity
|
||||
)}
|
||||
</GridList>
|
||||
{nodesToDisplay}
|
||||
</ColumnStackLayout>
|
||||
)}
|
||||
</Column>
|
||||
|
@@ -640,7 +640,7 @@ function NewObjectDialog({
|
||||
}
|
||||
>
|
||||
{currentTab === 'asset-store' && (
|
||||
<AssetStore ref={assetStore} onlyShowAssets />
|
||||
<AssetStore ref={assetStore} hideGameTemplates />
|
||||
)}
|
||||
{currentTab === 'new-object' &&
|
||||
(selectedCustomObjectEnumeratedMetadata &&
|
||||
|
@@ -6,7 +6,6 @@ import {
|
||||
redeemPrivateAssetPack,
|
||||
type PrivateAssetPackListingData,
|
||||
type PrivateGameTemplateListingData,
|
||||
type BundleListingData,
|
||||
getCalloutToGetSubscriptionOrClaimAssetPack,
|
||||
} from '../../Utils/GDevelopServices/Shop';
|
||||
import type { MessageDescriptor } from '../../Utils/i18n/MessageDescriptor.flow';
|
||||
@@ -65,10 +64,8 @@ import Window from '../../Utils/Window';
|
||||
import RaisedButton from '../../UI/RaisedButton';
|
||||
import PrivateAssetPackPurchaseDialog from './PrivateAssetPackPurchaseDialog';
|
||||
import PublicProfileContext from '../../Profile/PublicProfileContext';
|
||||
import { LARGE_WIDGET_SIZE } from '../../MainFrame/EditorContainers/HomePage/CardWidget';
|
||||
import { BundleStoreContext } from '../Bundles/BundleStoreContext';
|
||||
|
||||
const cellSpacing = 10;
|
||||
const cellSpacing = 8;
|
||||
|
||||
const getPackColumns = (windowSize: WindowSizeType, isLandscape: boolean) => {
|
||||
switch (windowSize) {
|
||||
@@ -79,7 +76,7 @@ const getPackColumns = (windowSize: WindowSizeType, isLandscape: boolean) => {
|
||||
case 'large':
|
||||
return 4;
|
||||
case 'xlarge':
|
||||
return 6;
|
||||
return 5;
|
||||
default:
|
||||
return 3;
|
||||
}
|
||||
@@ -111,16 +108,11 @@ const contentTypeToMessageDescriptor: {
|
||||
audio: t`audios`,
|
||||
};
|
||||
|
||||
const MAX_COLUMNS = getPackColumns('xlarge', true);
|
||||
const MAX_SECTION_WIDTH = (LARGE_WIDGET_SIZE + 2 * 5) * MAX_COLUMNS; // widget size + 5 padding per side
|
||||
const styles = {
|
||||
disabledText: { opacity: 0.6 },
|
||||
scrollview: { overflowX: 'hidden' },
|
||||
grid: {
|
||||
// Avoid tiles taking too much space on large screens.
|
||||
maxWidth: MAX_SECTION_WIDTH,
|
||||
overflow: 'hidden',
|
||||
width: `calc(100% + ${cellSpacing}px)`, // This is needed to compensate for the `margin: -5px` added by MUI related to spacing.
|
||||
margin: '0 2px', // Remove the default margin of the grid but keep the horizontal padding for focus outline.
|
||||
},
|
||||
leftColumnContainer: {
|
||||
flex: 3,
|
||||
@@ -166,9 +158,11 @@ type Props = {|
|
||||
|}
|
||||
) => void,
|
||||
onGameTemplateOpen: (
|
||||
privateGameTemplateListingData: PrivateGameTemplateListingData
|
||||
privateGameTemplateListingData: PrivateGameTemplateListingData,
|
||||
options?: {|
|
||||
forceProductPage?: boolean,
|
||||
|}
|
||||
) => void,
|
||||
onBundleOpen: (bundleListingData: BundleListingData) => void,
|
||||
simulateAppStoreProduct?: boolean,
|
||||
|};
|
||||
|
||||
@@ -177,21 +171,17 @@ const PrivateAssetPackInformationPage = ({
|
||||
privateAssetPackListingDatasFromSameCreator,
|
||||
onAssetPackOpen,
|
||||
onGameTemplateOpen,
|
||||
onBundleOpen,
|
||||
simulateAppStoreProduct,
|
||||
}: Props) => {
|
||||
const { isMobile } = useResponsiveWindowSize();
|
||||
const { id, name, sellerId } = privateAssetPackListingData;
|
||||
const { privateAssetPackListingDatas } = React.useContext(AssetStoreContext);
|
||||
const { bundleListingDatas } = React.useContext(BundleStoreContext);
|
||||
const { showAlert } = useAlertDialog();
|
||||
const {
|
||||
receivedAssetPacks,
|
||||
receivedBundles,
|
||||
profile,
|
||||
limits,
|
||||
assetPackPurchases,
|
||||
bundlePurchases,
|
||||
getAuthorizationHeader,
|
||||
onOpenLoginDialog,
|
||||
subscription,
|
||||
@@ -221,9 +211,10 @@ const PrivateAssetPackInformationPage = ({
|
||||
sellerPublicProfile,
|
||||
setSellerPublicProfile,
|
||||
] = React.useState<?UserPublicProfile>(null);
|
||||
const [displayPasswordPrompt, setDisplayPasswordPrompt] = React.useState<
|
||||
'redeem' | 'credits' | null
|
||||
>(null);
|
||||
const [
|
||||
displayPasswordPrompt,
|
||||
setDisplayPasswordPrompt,
|
||||
] = React.useState<boolean>(false);
|
||||
const [password, setPassword] = React.useState<string>('');
|
||||
const [errorText, setErrorText] = React.useState<?React.Node>(null);
|
||||
const { isLandscape, isMediumScreen, windowSize } = useResponsiveWindowSize();
|
||||
@@ -238,27 +229,15 @@ const PrivateAssetPackInformationPage = ({
|
||||
productId: privateAssetPackListingData
|
||||
? privateAssetPackListingData.id
|
||||
: null,
|
||||
receivedProducts: [
|
||||
...(receivedAssetPacks || []),
|
||||
...(receivedBundles || []),
|
||||
],
|
||||
productPurchases: [
|
||||
...(assetPackPurchases || []),
|
||||
...(bundlePurchases || []),
|
||||
],
|
||||
allProductListingDatas: [
|
||||
...(privateAssetPackListingDatas || []),
|
||||
...(bundleListingDatas || []),
|
||||
],
|
||||
receivedProducts: receivedAssetPacks,
|
||||
productPurchases: assetPackPurchases,
|
||||
allProductListingDatas: privateAssetPackListingDatas,
|
||||
}),
|
||||
[
|
||||
assetPackPurchases,
|
||||
bundlePurchases,
|
||||
privateAssetPackListingData,
|
||||
privateAssetPackListingDatas,
|
||||
bundleListingDatas,
|
||||
receivedAssetPacks,
|
||||
receivedBundles,
|
||||
]
|
||||
);
|
||||
const isAlreadyReceived = !!userAssetPackPurchaseUsageType;
|
||||
@@ -267,21 +246,17 @@ const PrivateAssetPackInformationPage = ({
|
||||
() =>
|
||||
getProductsIncludedInBundleTiles({
|
||||
product: assetPack,
|
||||
productListingDatas: [...(privateAssetPackListingDatas || [])],
|
||||
productListingDatas: privateAssetPackListingDatas,
|
||||
productListingData: privateAssetPackListingData,
|
||||
receivedProducts: [...(receivedAssetPacks || [])],
|
||||
onPrivateAssetPackOpen: product =>
|
||||
receivedProducts: receivedAssetPacks,
|
||||
onProductOpen: product =>
|
||||
onAssetPackOpen(product, { forceProductPage: true }),
|
||||
onPrivateGameTemplateOpen: onGameTemplateOpen,
|
||||
onBundleOpen,
|
||||
}),
|
||||
[
|
||||
assetPack,
|
||||
privateAssetPackListingDatas,
|
||||
receivedAssetPacks,
|
||||
onAssetPackOpen,
|
||||
onGameTemplateOpen,
|
||||
onBundleOpen,
|
||||
privateAssetPackListingData,
|
||||
]
|
||||
);
|
||||
@@ -290,30 +265,16 @@ const PrivateAssetPackInformationPage = ({
|
||||
() =>
|
||||
getBundlesContainingProductTiles({
|
||||
product: assetPack,
|
||||
productListingData: privateAssetPackListingData,
|
||||
productListingDatas: [
|
||||
...(privateAssetPackListingDatas || []),
|
||||
...(bundleListingDatas || []),
|
||||
],
|
||||
receivedProducts: [
|
||||
...(receivedAssetPacks || []),
|
||||
...(receivedBundles || []),
|
||||
],
|
||||
onPrivateAssetPackOpen: product =>
|
||||
productListingDatas: privateAssetPackListingDatas,
|
||||
receivedProducts: receivedAssetPacks,
|
||||
onProductOpen: product =>
|
||||
onAssetPackOpen(product, { forceProductPage: true }),
|
||||
onPrivateGameTemplateOpen: onGameTemplateOpen,
|
||||
onBundleOpen,
|
||||
}),
|
||||
[
|
||||
assetPack,
|
||||
privateAssetPackListingData,
|
||||
privateAssetPackListingDatas,
|
||||
bundleListingDatas,
|
||||
receivedAssetPacks,
|
||||
receivedBundles,
|
||||
onAssetPackOpen,
|
||||
onGameTemplateOpen,
|
||||
onBundleOpen,
|
||||
]
|
||||
);
|
||||
|
||||
@@ -336,16 +297,10 @@ const PrivateAssetPackInformationPage = ({
|
||||
|
||||
const onWillRedeemAssetPack = () => {
|
||||
// Password is required in dev environment only so that one cannot freely claim asset packs.
|
||||
if (Window.isDev()) setDisplayPasswordPrompt('redeem');
|
||||
if (Window.isDev()) setDisplayPasswordPrompt(true);
|
||||
else onRedeemAssetPack();
|
||||
};
|
||||
|
||||
const onWillBuyWithCredits = () => {
|
||||
// Password is required in dev environment only so that one cannot freely claim asset packs.
|
||||
if (Window.isDev()) setDisplayPasswordPrompt('credits');
|
||||
else onClickBuyWithCredits();
|
||||
};
|
||||
|
||||
const onRedeemAssetPack = React.useCallback(
|
||||
async () => {
|
||||
if (!profile || isRedeemingProduct) return;
|
||||
@@ -749,7 +704,7 @@ const PrivateAssetPackInformationPage = ({
|
||||
simulateAppStoreProduct={simulateAppStoreProduct}
|
||||
isAlreadyReceived={isAlreadyReceived}
|
||||
onClickBuy={onClickBuy}
|
||||
onClickBuyWithCredits={onWillBuyWithCredits}
|
||||
onClickBuyWithCredits={onClickBuyWithCredits}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
@@ -809,7 +764,7 @@ const PrivateAssetPackInformationPage = ({
|
||||
<GridList
|
||||
cols={getPackColumns(windowSize, isLandscape)}
|
||||
cellHeight="auto"
|
||||
spacing={cellSpacing}
|
||||
spacing={cellSpacing / 2}
|
||||
style={styles.grid}
|
||||
>
|
||||
{packsIncludedInBundleTiles}
|
||||
@@ -829,7 +784,7 @@ const PrivateAssetPackInformationPage = ({
|
||||
<GridList
|
||||
cols={getPackColumns(windowSize, isLandscape)}
|
||||
cellHeight="auto"
|
||||
spacing={cellSpacing}
|
||||
spacing={cellSpacing / 2}
|
||||
style={styles.grid}
|
||||
>
|
||||
{otherPacksFromTheSameAuthorTiles}
|
||||
@@ -842,12 +797,8 @@ const PrivateAssetPackInformationPage = ({
|
||||
) : null}
|
||||
{displayPasswordPrompt && (
|
||||
<PasswordPromptDialog
|
||||
onApply={
|
||||
displayPasswordPrompt === 'redeem'
|
||||
? onWillRedeemAssetPack
|
||||
: onClickBuyWithCredits
|
||||
}
|
||||
onClose={() => setDisplayPasswordPrompt(null)}
|
||||
onApply={onRedeemAssetPack}
|
||||
onClose={() => setDisplayPasswordPrompt(false)}
|
||||
passwordValue={password}
|
||||
setPasswordValue={setPassword}
|
||||
/>
|
||||
|
@@ -5,7 +5,6 @@ import {
|
||||
buyProductWithCredits,
|
||||
type PrivateGameTemplateListingData,
|
||||
type PrivateAssetPackListingData,
|
||||
type BundleListingData,
|
||||
} from '../../Utils/GDevelopServices/Shop';
|
||||
import {
|
||||
getPrivateGameTemplate,
|
||||
@@ -60,10 +59,8 @@ import Play from '../../UI/CustomSvgIcons/Play';
|
||||
import PrivateGameTemplatePurchaseDialog from './PrivateGameTemplatePurchaseDialog';
|
||||
import PasswordPromptDialog from '../PasswordPromptDialog';
|
||||
import PublicProfileContext from '../../Profile/PublicProfileContext';
|
||||
import { LARGE_WIDGET_SIZE } from '../../MainFrame/EditorContainers/HomePage/CardWidget';
|
||||
import { BundleStoreContext } from '../Bundles/BundleStoreContext';
|
||||
|
||||
const cellSpacing = 10;
|
||||
const cellSpacing = 8;
|
||||
|
||||
const getTemplateColumns = (
|
||||
windowSize: WindowSizeType,
|
||||
@@ -77,21 +74,17 @@ const getTemplateColumns = (
|
||||
case 'large':
|
||||
return 4;
|
||||
case 'xlarge':
|
||||
return 6;
|
||||
return 5;
|
||||
default:
|
||||
return 3;
|
||||
}
|
||||
};
|
||||
const MAX_COLUMNS = getTemplateColumns('xlarge', true);
|
||||
const MAX_SECTION_WIDTH = (LARGE_WIDGET_SIZE + 2 * 5) * MAX_COLUMNS; // widget size + 5 padding per side
|
||||
|
||||
const styles = {
|
||||
disabledText: { opacity: 0.6 },
|
||||
scrollview: { overflowX: 'hidden' },
|
||||
grid: {
|
||||
// Avoid tiles taking too much space on large screens.
|
||||
maxWidth: MAX_SECTION_WIDTH,
|
||||
overflow: 'hidden',
|
||||
width: `calc(100% + ${cellSpacing}px)`, // This is needed to compensate for the `margin: -5px` added by MUI related to spacing.
|
||||
margin: '0 2px', // Remove the default margin of the grid but keep the horizontal padding for focus outline.
|
||||
},
|
||||
leftColumnContainer: {
|
||||
flex: 3,
|
||||
@@ -125,16 +118,8 @@ const styles = {
|
||||
type Props = {|
|
||||
privateGameTemplateListingData: PrivateGameTemplateListingData,
|
||||
privateGameTemplateListingDatasFromSameCreator?: ?Array<PrivateGameTemplateListingData>,
|
||||
onAssetPackOpen?: (
|
||||
privateAssetPackListingData: PrivateAssetPackListingData,
|
||||
options?: {|
|
||||
forceProductPage?: boolean,
|
||||
|}
|
||||
) => void,
|
||||
onGameTemplateOpen: (
|
||||
privateGameTemplateListingData: PrivateGameTemplateListingData
|
||||
) => void,
|
||||
onBundleOpen?: (bundleListingData: BundleListingData) => void,
|
||||
onGameTemplateOpen: PrivateGameTemplateListingData => void,
|
||||
onAssetPackOpen?: PrivateAssetPackListingData => void,
|
||||
onCreateWithGameTemplate?: PrivateGameTemplateListingData => void,
|
||||
simulateAppStoreProduct?: boolean,
|
||||
|};
|
||||
@@ -144,7 +129,6 @@ const PrivateGameTemplateInformationPage = ({
|
||||
privateGameTemplateListingDatasFromSameCreator,
|
||||
onGameTemplateOpen,
|
||||
onAssetPackOpen,
|
||||
onBundleOpen,
|
||||
onCreateWithGameTemplate,
|
||||
simulateAppStoreProduct,
|
||||
}: Props) => {
|
||||
@@ -152,14 +136,11 @@ const PrivateGameTemplateInformationPage = ({
|
||||
const { privateGameTemplateListingDatas } = React.useContext(
|
||||
PrivateGameTemplateStoreContext
|
||||
);
|
||||
const { bundleListingDatas } = React.useContext(BundleStoreContext);
|
||||
const {
|
||||
receivedGameTemplates,
|
||||
receivedBundles,
|
||||
profile,
|
||||
limits,
|
||||
gameTemplatePurchases,
|
||||
bundlePurchases,
|
||||
getAuthorizationHeader,
|
||||
onOpenLoginDialog,
|
||||
} = React.useContext(AuthenticatedUserContext);
|
||||
@@ -205,27 +186,15 @@ const PrivateGameTemplateInformationPage = ({
|
||||
productId: privateGameTemplateListingData
|
||||
? privateGameTemplateListingData.id
|
||||
: null,
|
||||
receivedProducts: [
|
||||
...(receivedGameTemplates || []),
|
||||
...(receivedBundles || []),
|
||||
],
|
||||
productPurchases: [
|
||||
...(gameTemplatePurchases || []),
|
||||
...(bundlePurchases || []),
|
||||
],
|
||||
allProductListingDatas: [
|
||||
...(privateGameTemplateListingDatas || []),
|
||||
...(bundleListingDatas || []),
|
||||
],
|
||||
receivedProducts: receivedGameTemplates,
|
||||
productPurchases: gameTemplatePurchases,
|
||||
allProductListingDatas: privateGameTemplateListingDatas,
|
||||
}),
|
||||
[
|
||||
gameTemplatePurchases,
|
||||
bundlePurchases,
|
||||
privateGameTemplateListingData,
|
||||
privateGameTemplateListingDatas,
|
||||
bundleListingDatas,
|
||||
receivedGameTemplates,
|
||||
receivedBundles,
|
||||
]
|
||||
);
|
||||
const isAlreadyReceived = !!userGameTemplatePurchaseUsageType;
|
||||
@@ -234,55 +203,33 @@ const PrivateGameTemplateInformationPage = ({
|
||||
() =>
|
||||
getProductsIncludedInBundleTiles({
|
||||
product: gameTemplate,
|
||||
productListingDatas: [...(privateGameTemplateListingDatas || [])],
|
||||
productListingDatas: privateGameTemplateListingDatas,
|
||||
productListingData: privateGameTemplateListingData,
|
||||
receivedProducts: [...(receivedGameTemplates || [])],
|
||||
onPrivateAssetPackOpen: onAssetPackOpen
|
||||
? product => onAssetPackOpen(product, { forceProductPage: true })
|
||||
: undefined,
|
||||
onPrivateGameTemplateOpen: onGameTemplateOpen,
|
||||
onBundleOpen,
|
||||
receivedProducts: receivedGameTemplates,
|
||||
onProductOpen: onGameTemplateOpen,
|
||||
}),
|
||||
[
|
||||
gameTemplate,
|
||||
privateGameTemplateListingDatas,
|
||||
receivedGameTemplates,
|
||||
onAssetPackOpen,
|
||||
onGameTemplateOpen,
|
||||
onBundleOpen,
|
||||
privateGameTemplateListingData,
|
||||
]
|
||||
);
|
||||
|
||||
const bundlesContainingTemplateTiles = React.useMemo(
|
||||
const bundlesContainingPackTiles = React.useMemo(
|
||||
() =>
|
||||
getBundlesContainingProductTiles({
|
||||
product: gameTemplate,
|
||||
productListingData: privateGameTemplateListingData,
|
||||
productListingDatas: [
|
||||
...(privateGameTemplateListingDatas || []),
|
||||
...(bundleListingDatas || []),
|
||||
],
|
||||
receivedProducts: [
|
||||
...(receivedGameTemplates || []),
|
||||
...(receivedBundles || []),
|
||||
],
|
||||
onPrivateAssetPackOpen: onAssetPackOpen
|
||||
? product => onAssetPackOpen(product, { forceProductPage: true })
|
||||
: undefined,
|
||||
onPrivateGameTemplateOpen: onGameTemplateOpen,
|
||||
onBundleOpen,
|
||||
productListingDatas: privateGameTemplateListingDatas,
|
||||
receivedProducts: receivedGameTemplates,
|
||||
onProductOpen: onGameTemplateOpen,
|
||||
}),
|
||||
[
|
||||
gameTemplate,
|
||||
privateGameTemplateListingData,
|
||||
privateGameTemplateListingDatas,
|
||||
bundleListingDatas,
|
||||
receivedGameTemplates,
|
||||
receivedBundles,
|
||||
onAssetPackOpen,
|
||||
onGameTemplateOpen,
|
||||
onBundleOpen,
|
||||
]
|
||||
);
|
||||
|
||||
@@ -611,7 +558,7 @@ const PrivateGameTemplateInformationPage = ({
|
||||
simulateAppStoreProduct={simulateAppStoreProduct}
|
||||
isAlreadyReceived={isAlreadyReceived}
|
||||
onClickBuy={onClickBuy}
|
||||
onClickBuyWithCredits={onWillBuyWithCredits}
|
||||
onClickBuyWithCredits={onClickBuyWithCredits}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
@@ -637,12 +584,12 @@ const PrivateGameTemplateInformationPage = ({
|
||||
/>
|
||||
</Text>
|
||||
</Column>
|
||||
{bundlesContainingTemplateTiles &&
|
||||
bundlesContainingTemplateTiles.length ? (
|
||||
{bundlesContainingPackTiles &&
|
||||
bundlesContainingPackTiles.length ? (
|
||||
<>
|
||||
<ColumnStackLayout noMargin>
|
||||
<LargeSpacer />
|
||||
{bundlesContainingTemplateTiles}
|
||||
{bundlesContainingPackTiles}
|
||||
<LargeSpacer />
|
||||
</ColumnStackLayout>
|
||||
</>
|
||||
@@ -658,7 +605,7 @@ const PrivateGameTemplateInformationPage = ({
|
||||
<GridList
|
||||
cols={getTemplateColumns(windowSize, isLandscape)}
|
||||
cellHeight="auto"
|
||||
spacing={cellSpacing}
|
||||
spacing={cellSpacing / 2}
|
||||
style={styles.grid}
|
||||
>
|
||||
{templatesIncludedInBundleTiles}
|
||||
@@ -678,7 +625,7 @@ const PrivateGameTemplateInformationPage = ({
|
||||
<GridList
|
||||
cols={getTemplateColumns(windowSize, isLandscape)}
|
||||
cellHeight="auto"
|
||||
spacing={cellSpacing}
|
||||
spacing={cellSpacing / 2}
|
||||
style={styles.grid}
|
||||
>
|
||||
{otherTemplatesFromTheSameAuthorTiles}
|
||||
@@ -691,7 +638,7 @@ const PrivateGameTemplateInformationPage = ({
|
||||
) : null}
|
||||
{displayPasswordPrompt && (
|
||||
<PasswordPromptDialog
|
||||
onApply={onClickBuyWithCredits}
|
||||
onApply={onWillBuyWithCredits}
|
||||
onClose={() => setDisplayPasswordPrompt(false)}
|
||||
passwordValue={password}
|
||||
setPasswordValue={setPassword}
|
||||
|
@@ -4,23 +4,16 @@ import { type I18n as I18nType } from '@lingui/core';
|
||||
import {
|
||||
type PrivateAssetPackListingData,
|
||||
type PrivateGameTemplateListingData,
|
||||
type BundleListingData,
|
||||
type CourseListingData,
|
||||
type CreditsPackageListingData,
|
||||
type Purchase,
|
||||
} from '../Utils/GDevelopServices/Shop';
|
||||
import {
|
||||
type PrivateAssetPack,
|
||||
type PrivateGameTemplate,
|
||||
type Bundle,
|
||||
type Course,
|
||||
} from '../Utils/GDevelopServices/Asset';
|
||||
import {
|
||||
PrivateAssetPackTile,
|
||||
PrivateGameTemplateTile,
|
||||
BundleTile,
|
||||
PromoBundleCard,
|
||||
CourseTile,
|
||||
} from './ShopTiles';
|
||||
import AuthenticatedUserContext from '../Profile/AuthenticatedUserContext';
|
||||
import { shouldUseAppStoreProduct } from '../Utils/AppStorePurchases';
|
||||
@@ -33,14 +26,10 @@ import { renderProductPrice } from './ProductPriceTag';
|
||||
import { Trans } from '@lingui/macro';
|
||||
import FlatButton from '../UI/FlatButton';
|
||||
import { Column } from '../UI/Grid';
|
||||
import { type MediaItem } from '../UI/ResponsiveMediaGallery';
|
||||
|
||||
export const getOtherProductsFromSameAuthorTiles = <
|
||||
T:
|
||||
| PrivateAssetPackListingData
|
||||
| PrivateGameTemplateListingData
|
||||
| BundleListingData,
|
||||
U: PrivateAssetPack | PrivateGameTemplate | Bundle
|
||||
T: PrivateAssetPackListingData | PrivateGameTemplateListingData,
|
||||
U: PrivateAssetPack | PrivateGameTemplate
|
||||
>({
|
||||
otherProductListingDatasFromSameCreator,
|
||||
currentProductListingData,
|
||||
@@ -93,16 +82,6 @@ export const getOtherProductsFromSameAuthorTiles = <
|
||||
/>
|
||||
);
|
||||
}
|
||||
if (productListingDataFromSameCreator.productType === 'BUNDLE') {
|
||||
return (
|
||||
<BundleTile
|
||||
bundleListingData={productListingDataFromSameCreator}
|
||||
key={productListingDataFromSameCreator.id}
|
||||
onSelect={() => onProductOpen(productListingDataFromSameCreator)}
|
||||
owned={isProductOwned}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
console.error(
|
||||
'Unexpected product type:',
|
||||
@@ -113,81 +92,27 @@ export const getOtherProductsFromSameAuthorTiles = <
|
||||
.filter(Boolean);
|
||||
};
|
||||
|
||||
export const getBundlesContainingProduct = <
|
||||
T:
|
||||
| PrivateAssetPackListingData
|
||||
| PrivateGameTemplateListingData
|
||||
| BundleListingData
|
||||
| CourseListingData,
|
||||
U: PrivateAssetPack | PrivateGameTemplate | Bundle | Course
|
||||
>({
|
||||
product,
|
||||
productListingDatas,
|
||||
}: {|
|
||||
product: U,
|
||||
productListingDatas: Array<T>,
|
||||
|}): T[] => {
|
||||
// A bundle can either be:
|
||||
// - an ASSET_PACK or GAME_TEMPLATE that includes other product ids
|
||||
// - a BUNDLE that includes other products
|
||||
const bundlesContainingProduct = productListingDatas
|
||||
.filter(
|
||||
productListingData =>
|
||||
(productListingData.includedListableProductIds &&
|
||||
productListingData.includedListableProductIds.includes(product.id)) ||
|
||||
(productListingData.productType === 'BUNDLE' &&
|
||||
productListingData.includedListableProducts &&
|
||||
productListingData.includedListableProducts.some(
|
||||
includedProduct => includedProduct.productId === product.id
|
||||
))
|
||||
)
|
||||
// Show types 'BUNDLE' first.
|
||||
.sort((a, b) => {
|
||||
if (a.productType === 'BUNDLE' && b.productType !== 'BUNDLE') {
|
||||
return -1;
|
||||
}
|
||||
if (a.productType !== 'BUNDLE' && b.productType === 'BUNDLE') {
|
||||
return 1;
|
||||
}
|
||||
return 0;
|
||||
});
|
||||
|
||||
return bundlesContainingProduct;
|
||||
};
|
||||
|
||||
export const getBundlesContainingProductTiles = <
|
||||
T:
|
||||
| PrivateAssetPackListingData
|
||||
| PrivateGameTemplateListingData
|
||||
| BundleListingData,
|
||||
U: PrivateAssetPack | PrivateGameTemplate | Bundle
|
||||
T: PrivateAssetPackListingData | PrivateGameTemplateListingData,
|
||||
U: PrivateAssetPack | PrivateGameTemplate
|
||||
>({
|
||||
product,
|
||||
productListingData,
|
||||
productListingDatas,
|
||||
receivedProducts,
|
||||
onPrivateAssetPackOpen,
|
||||
onPrivateGameTemplateOpen,
|
||||
onBundleOpen,
|
||||
onProductOpen,
|
||||
}: {|
|
||||
product: ?U,
|
||||
productListingData: T,
|
||||
productListingDatas: ?Array<T>,
|
||||
receivedProducts: ?Array<U>,
|
||||
onPrivateAssetPackOpen?: (
|
||||
assetPackListingData: PrivateAssetPackListingData
|
||||
) => void,
|
||||
onPrivateGameTemplateOpen?: (
|
||||
privateGameTemplateListingData: PrivateGameTemplateListingData
|
||||
) => void,
|
||||
onBundleOpen?: (bundleListingData: BundleListingData) => void,
|
||||
onProductOpen: (product: T) => void,
|
||||
|}): ?Array<React.Node> => {
|
||||
if (!product || !productListingDatas) return null;
|
||||
|
||||
const bundlesContainingProduct = getBundlesContainingProduct({
|
||||
product,
|
||||
productListingDatas,
|
||||
});
|
||||
const bundlesContainingProduct = productListingDatas.filter(
|
||||
productListingData =>
|
||||
productListingData.includedListableProductIds &&
|
||||
productListingData.includedListableProductIds.includes(product.id)
|
||||
);
|
||||
|
||||
if (!bundlesContainingProduct.length) return null;
|
||||
|
||||
@@ -206,157 +131,65 @@ export const getBundlesContainingProductTiles = <
|
||||
)
|
||||
);
|
||||
|
||||
const allProductsWithOwnedStatus = [
|
||||
...ownedBundlesContainingProduct.map(bundleContainingProduct => ({
|
||||
product: bundleContainingProduct,
|
||||
owned: true,
|
||||
})),
|
||||
...notOwnedBundlesContainingProduct.map(bundleContainingProduct => ({
|
||||
product: bundleContainingProduct,
|
||||
owned: false,
|
||||
})),
|
||||
];
|
||||
|
||||
return allProductsWithOwnedStatus.map(
|
||||
({ product: bundleContainingProduct, owned }) => {
|
||||
if (bundleContainingProduct.productType === 'ASSET_PACK') {
|
||||
if (!onPrivateAssetPackOpen) {
|
||||
console.error(
|
||||
'Trying to render a promo ASSET_PACK tile without onPrivateAssetPackOpen handler.'
|
||||
);
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
<PromoBundleCard
|
||||
bundleProductListingData={bundleContainingProduct}
|
||||
includedProductListingData={productListingData}
|
||||
onSelect={() => onPrivateAssetPackOpen(bundleContainingProduct)}
|
||||
owned={owned}
|
||||
key={bundleContainingProduct.id}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (bundleContainingProduct.productType === 'GAME_TEMPLATE') {
|
||||
if (!onPrivateGameTemplateOpen) {
|
||||
console.error(
|
||||
'Trying to render a promo GAME_TEMPLATE tile without onPrivateGameTemplateOpen handler.'
|
||||
);
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
<PromoBundleCard
|
||||
bundleProductListingData={bundleContainingProduct}
|
||||
includedProductListingData={productListingData}
|
||||
onSelect={() => onPrivateGameTemplateOpen(bundleContainingProduct)}
|
||||
owned={owned}
|
||||
key={bundleContainingProduct.id}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (bundleContainingProduct.productType === 'BUNDLE') {
|
||||
if (!onBundleOpen) {
|
||||
console.error(
|
||||
'Trying to render a promo BUNDLE tile without onBundleOpen handler.'
|
||||
);
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
<PromoBundleCard
|
||||
bundleProductListingData={bundleContainingProduct}
|
||||
includedProductListingData={productListingData}
|
||||
onSelect={() => onBundleOpen(bundleContainingProduct)}
|
||||
owned={owned}
|
||||
key={bundleContainingProduct.id}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
console.error(
|
||||
'Unexpected product type for Promo Tile:',
|
||||
bundleContainingProduct.productType
|
||||
const allTiles = ownedBundlesContainingProduct
|
||||
.map(bundleContainingProduct => {
|
||||
return (
|
||||
<PromoBundleCard
|
||||
productListingData={bundleContainingProduct}
|
||||
onSelect={() => onProductOpen(bundleContainingProduct)}
|
||||
owned
|
||||
key={bundleContainingProduct.id}
|
||||
/>
|
||||
);
|
||||
return null;
|
||||
}
|
||||
);
|
||||
})
|
||||
.concat(
|
||||
notOwnedBundlesContainingProduct.map(bundleContainingProduct => {
|
||||
return (
|
||||
<PromoBundleCard
|
||||
productListingData={bundleContainingProduct}
|
||||
onSelect={() => onProductOpen(bundleContainingProduct)}
|
||||
owned={false}
|
||||
key={bundleContainingProduct.id}
|
||||
/>
|
||||
);
|
||||
})
|
||||
);
|
||||
|
||||
return allTiles;
|
||||
};
|
||||
|
||||
export const getProductsIncludedInBundle = <
|
||||
T:
|
||||
| PrivateAssetPackListingData
|
||||
| PrivateGameTemplateListingData
|
||||
| BundleListingData
|
||||
| CourseListingData
|
||||
| CreditsPackageListingData
|
||||
export const getProductsIncludedInBundleTiles = <
|
||||
T: PrivateAssetPackListingData | PrivateGameTemplateListingData,
|
||||
U: PrivateAssetPack | PrivateGameTemplate
|
||||
>({
|
||||
productListingData,
|
||||
productListingDatas,
|
||||
}: {|
|
||||
productListingDatas: Array<T>,
|
||||
productListingData: T,
|
||||
|}): ?(T[]) => {
|
||||
const includedProductIds =
|
||||
productListingData.includedListableProductIds ||
|
||||
(productListingData.productType === 'BUNDLE' &&
|
||||
productListingData.includedListableProducts &&
|
||||
productListingData.includedListableProducts.map(
|
||||
includedProduct => includedProduct.productId
|
||||
));
|
||||
if (!includedProductIds) return null;
|
||||
|
||||
return productListingDatas.filter(productListingData =>
|
||||
includedProductIds.includes(productListingData.id)
|
||||
);
|
||||
};
|
||||
|
||||
export const getProductsIncludedInBundleTiles = ({
|
||||
product,
|
||||
productListingDatas,
|
||||
productListingData,
|
||||
receivedProducts,
|
||||
onPrivateAssetPackOpen,
|
||||
onPrivateGameTemplateOpen,
|
||||
onBundleOpen,
|
||||
onCourseOpen,
|
||||
onProductOpen,
|
||||
}: {|
|
||||
product: ?PrivateAssetPack | PrivateGameTemplate | Bundle | Course,
|
||||
productListingDatas: ?Array<
|
||||
| PrivateAssetPackListingData
|
||||
| PrivateGameTemplateListingData
|
||||
| BundleListingData
|
||||
| CourseListingData
|
||||
>,
|
||||
productListingData:
|
||||
| PrivateAssetPackListingData
|
||||
| PrivateGameTemplateListingData
|
||||
| BundleListingData
|
||||
| CourseListingData,
|
||||
receivedProducts: ?Array<
|
||||
PrivateAssetPack | PrivateGameTemplate | Bundle | Course
|
||||
>,
|
||||
onPrivateAssetPackOpen?: (
|
||||
assetPackListingData: PrivateAssetPackListingData
|
||||
) => void,
|
||||
onPrivateGameTemplateOpen?: (
|
||||
privateGameTemplateListingData: PrivateGameTemplateListingData
|
||||
) => void,
|
||||
onBundleOpen?: (bundleListingData: BundleListingData) => void,
|
||||
onCourseOpen?: (courseListingData: CourseListingData) => void,
|
||||
product: ?U,
|
||||
productListingDatas: ?Array<T>,
|
||||
productListingData: T,
|
||||
receivedProducts: ?Array<U>,
|
||||
onProductOpen: (product: T) => void,
|
||||
|}): ?Array<React.Node> => {
|
||||
if (!product || !productListingDatas) return null;
|
||||
|
||||
const productsIncludedInBundle = getProductsIncludedInBundle({
|
||||
productListingData,
|
||||
productListingDatas,
|
||||
});
|
||||
const includedProductIds = productListingData.includedListableProductIds;
|
||||
if (!includedProductIds) return null;
|
||||
|
||||
if (!productsIncludedInBundle || !productsIncludedInBundle.length) {
|
||||
return null;
|
||||
}
|
||||
return includedProductIds
|
||||
.map(includedProductId => {
|
||||
const includedProductListingData = productListingDatas.find(
|
||||
privateProductListingData =>
|
||||
privateProductListingData.id === includedProductId
|
||||
);
|
||||
if (!includedProductListingData) {
|
||||
console.warn(`Included product ${includedProductId} not found`);
|
||||
return null;
|
||||
}
|
||||
|
||||
return productsIncludedInBundle
|
||||
.map(includedProductListingData => {
|
||||
const isProductOwned =
|
||||
!!receivedProducts &&
|
||||
!!receivedProducts.find(
|
||||
@@ -364,70 +197,22 @@ export const getProductsIncludedInBundleTiles = ({
|
||||
);
|
||||
|
||||
if (includedProductListingData.productType === 'GAME_TEMPLATE') {
|
||||
if (!onPrivateGameTemplateOpen) {
|
||||
console.error(
|
||||
'Trying to render a GAME_TEMPLATE tile without onPrivateGameTemplateOpen handler.'
|
||||
);
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
<PrivateGameTemplateTile
|
||||
privateGameTemplateListingData={includedProductListingData}
|
||||
key={includedProductListingData.id}
|
||||
onSelect={() =>
|
||||
onPrivateGameTemplateOpen(includedProductListingData)
|
||||
}
|
||||
onSelect={() => onProductOpen(includedProductListingData)}
|
||||
owned={isProductOwned}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (includedProductListingData.productType === 'ASSET_PACK') {
|
||||
if (!onPrivateAssetPackOpen) {
|
||||
console.error(
|
||||
'Trying to render an ASSET_PACK tile without onPrivateAssetPackOpen handler.'
|
||||
);
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
<PrivateAssetPackTile
|
||||
assetPackListingData={includedProductListingData}
|
||||
key={includedProductListingData.id}
|
||||
onSelect={() => onPrivateAssetPackOpen(includedProductListingData)}
|
||||
owned={isProductOwned}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (includedProductListingData.productType === 'BUNDLE') {
|
||||
if (!onBundleOpen) {
|
||||
console.error(
|
||||
'Trying to render a BUNDLE tile without onBundleOpen handler.'
|
||||
);
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
<BundleTile
|
||||
bundleListingData={includedProductListingData}
|
||||
key={includedProductListingData.id}
|
||||
onSelect={() => onBundleOpen(includedProductListingData)}
|
||||
owned={isProductOwned}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (includedProductListingData.productType === 'COURSE') {
|
||||
if (!onCourseOpen) {
|
||||
console.error(
|
||||
'Trying to render a COURSE tile without onCourseOpen handler.'
|
||||
);
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
<CourseTile
|
||||
courseListingData={includedProductListingData}
|
||||
key={includedProductListingData.id}
|
||||
onSelect={() => onCourseOpen(includedProductListingData)}
|
||||
onSelect={() => onProductOpen(includedProductListingData)}
|
||||
owned={isProductOwned}
|
||||
/>
|
||||
);
|
||||
@@ -448,11 +233,8 @@ export const getProductsIncludedInBundleTiles = ({
|
||||
// In case the user has both, we consider the product purchase as the
|
||||
// most important one.
|
||||
export const getUserProductPurchaseUsageType = <
|
||||
T:
|
||||
| PrivateAssetPackListingData
|
||||
| PrivateGameTemplateListingData
|
||||
| BundleListingData,
|
||||
U: PrivateAssetPack | PrivateGameTemplate | Bundle
|
||||
T: PrivateAssetPackListingData | PrivateGameTemplateListingData,
|
||||
U: PrivateAssetPack | PrivateGameTemplate
|
||||
>({
|
||||
productId,
|
||||
receivedProducts,
|
||||
@@ -479,47 +261,29 @@ export const getUserProductPurchaseUsageType = <
|
||||
);
|
||||
if (!productPurchase) {
|
||||
// It is possible the user has the product as part of a bundle.
|
||||
const bundlesIncludingProduct = getBundlesContainingProduct({
|
||||
product: currentReceivedProduct,
|
||||
productListingDatas: allProductListingDatas,
|
||||
});
|
||||
if (!bundlesIncludingProduct.length) return null;
|
||||
|
||||
// We look at all the purchases of the bundles that include the product.
|
||||
const receivedProductBundlePurchases = productPurchases.filter(
|
||||
productPurchase =>
|
||||
bundlesIncludingProduct.some(
|
||||
bundleListingData =>
|
||||
bundleListingData.id === productPurchase.productId
|
||||
)
|
||||
const productBundleListingData = allProductListingDatas.find(
|
||||
productListingData =>
|
||||
productListingData.includedListableProductIds &&
|
||||
productListingData.includedListableProductIds.includes(productId)
|
||||
);
|
||||
|
||||
if (!receivedProductBundlePurchases.length) {
|
||||
return null;
|
||||
if (productBundleListingData) {
|
||||
const receivedProductBundlePurchase = productPurchases.find(
|
||||
productPurchase =>
|
||||
productPurchase.productId === productBundleListingData.id
|
||||
);
|
||||
if (receivedProductBundlePurchase) {
|
||||
return receivedProductBundlePurchase.usageType;
|
||||
}
|
||||
}
|
||||
|
||||
// We don't really know which usage type to return, so we look at the first purchase.
|
||||
if (bundlesIncludingProduct[0].productType === 'BUNDLE') {
|
||||
// In a bundle, we look for the usage type of the included product.
|
||||
const includedProduct = (
|
||||
bundlesIncludingProduct[0].includedListableProducts || []
|
||||
).find(includedProduct => includedProduct.productId === productId);
|
||||
return includedProduct ? includedProduct.usageType : null;
|
||||
}
|
||||
|
||||
// Otherwise, we return the usage type of the purchase. (when included in an ASSET_PACK or GAME_TEMPLATE)
|
||||
return receivedProductBundlePurchases[0].usageType;
|
||||
return null;
|
||||
}
|
||||
|
||||
return productPurchase.usageType;
|
||||
};
|
||||
|
||||
export const PurchaseProductButtons = <
|
||||
T:
|
||||
| PrivateAssetPackListingData
|
||||
| PrivateGameTemplateListingData
|
||||
| CourseListingData
|
||||
| BundleListingData
|
||||
T: PrivateAssetPackListingData | PrivateGameTemplateListingData
|
||||
>({
|
||||
productListingData,
|
||||
selectedUsageType,
|
||||
@@ -536,21 +300,18 @@ export const PurchaseProductButtons = <
|
||||
simulateAppStoreProduct?: boolean,
|
||||
i18n: I18nType,
|
||||
isAlreadyReceived: boolean,
|
||||
onClickBuy: () => void | Promise<void>,
|
||||
onClickBuyWithCredits?: () => void | Promise<void>,
|
||||
onClickBuy: () => Promise<void>,
|
||||
onClickBuyWithCredits: () => Promise<void>,
|
||||
|}) => {
|
||||
const { authenticated } = React.useContext(AuthenticatedUserContext);
|
||||
const shouldUseOrSimulateAppStoreProduct =
|
||||
simulateAppStoreProduct || shouldUseAppStoreProduct();
|
||||
const productType = productListingData.productType.toLowerCase();
|
||||
|
||||
let creditPrice =
|
||||
productListingData.productType !== 'BUNDLE'
|
||||
? productListingData.creditPrices.find(
|
||||
price => price.usageType === selectedUsageType
|
||||
)
|
||||
: null;
|
||||
if (!creditPrice && productListingData.productType !== 'BUNDLE') {
|
||||
let creditPrice = productListingData.creditPrices.find(
|
||||
price => price.usageType === selectedUsageType
|
||||
);
|
||||
if (!creditPrice) {
|
||||
// We're probably switching from one product to another, and the usage type is not available.
|
||||
// Let's reset it.
|
||||
onUsageTypeChange(productListingData.prices[0].usageType);
|
||||
@@ -574,7 +335,7 @@ export const PurchaseProductButtons = <
|
||||
plainText: true,
|
||||
});
|
||||
|
||||
return shouldUseOrSimulateAppStoreProduct && creditPrice ? (
|
||||
return shouldUseOrSimulateAppStoreProduct ? (
|
||||
<LineStackLayout>
|
||||
<RaisedButton
|
||||
primary
|
||||
@@ -593,15 +354,13 @@ export const PurchaseProductButtons = <
|
||||
</LineStackLayout>
|
||||
) : (
|
||||
<LineStackLayout>
|
||||
{creditPrice && (
|
||||
<FlatButton
|
||||
primary
|
||||
label={<Trans>Buy for {creditPrice.amount} credits</Trans>}
|
||||
onClick={onClickBuyWithCredits}
|
||||
id={`buy-${productType}-with-credits`}
|
||||
leftIcon={<Coin fontSize="small" />}
|
||||
/>
|
||||
)}
|
||||
<FlatButton
|
||||
primary
|
||||
label={<Trans>Buy for {creditPrice.amount} credits</Trans>}
|
||||
onClick={onClickBuyWithCredits}
|
||||
id={`buy-${productType}-with-credits`}
|
||||
leftIcon={<Coin fontSize="small" />}
|
||||
/>
|
||||
<RaisedButton
|
||||
primary
|
||||
label={<Trans>Buy for {formattedProductPriceText}</Trans>}
|
||||
@@ -645,45 +404,39 @@ export const OpenProductButton = <
|
||||
};
|
||||
|
||||
export const getProductMediaItems = <
|
||||
T:
|
||||
| PrivateAssetPackListingData
|
||||
| PrivateGameTemplateListingData
|
||||
| BundleListingData,
|
||||
U: PrivateAssetPack | PrivateGameTemplate | Bundle
|
||||
T: PrivateAssetPackListingData | PrivateGameTemplateListingData,
|
||||
U: PrivateAssetPack | PrivateGameTemplate
|
||||
>({
|
||||
productListingData,
|
||||
product,
|
||||
additionalThumbnails,
|
||||
shouldSimulateAppStoreProduct,
|
||||
}: {|
|
||||
productListingData: T,
|
||||
product: ?U,
|
||||
additionalThumbnails?: string[],
|
||||
shouldSimulateAppStoreProduct?: boolean,
|
||||
|}): MediaItem[] => {
|
||||
|}) => {
|
||||
if (!product) return [];
|
||||
|
||||
const shouldUseOrSimulateAppStoreProduct =
|
||||
shouldSimulateAppStoreProduct || shouldUseAppStoreProduct();
|
||||
|
||||
// Deduplicate in case we have the same image in the thumbnailUrls and previewImageUrls.
|
||||
const uniqueImageUrls: string[] = [
|
||||
...new Set([
|
||||
(shouldUseOrSimulateAppStoreProduct &&
|
||||
productListingData.appStoreThumbnailUrls &&
|
||||
productListingData.appStoreThumbnailUrls[0]) ||
|
||||
const mediaItems = [
|
||||
{
|
||||
kind: 'image',
|
||||
url:
|
||||
(shouldUseOrSimulateAppStoreProduct &&
|
||||
productListingData.appStoreThumbnailUrls &&
|
||||
productListingData.appStoreThumbnailUrls[0]) ||
|
||||
productListingData.thumbnailUrls[0],
|
||||
...product.previewImageUrls,
|
||||
...(additionalThumbnails || []),
|
||||
]),
|
||||
},
|
||||
...product.previewImageUrls.map(url => ({
|
||||
kind: 'image',
|
||||
url,
|
||||
})),
|
||||
];
|
||||
const uniqueMediaItems: MediaItem[] = uniqueImageUrls.map((url: string) => ({
|
||||
kind: 'image',
|
||||
url,
|
||||
}));
|
||||
|
||||
if (product.previewSoundUrls) {
|
||||
uniqueMediaItems.push(
|
||||
mediaItems.push(
|
||||
...product.previewSoundUrls.map(url => ({
|
||||
kind: 'audio',
|
||||
url,
|
||||
@@ -691,5 +444,5 @@ export const getProductMediaItems = <
|
||||
);
|
||||
}
|
||||
|
||||
return uniqueMediaItems;
|
||||
return mediaItems;
|
||||
};
|
||||
|
@@ -8,8 +8,6 @@ import {
|
||||
type PrivateAssetPackListingData,
|
||||
type PrivateGameTemplateListingData,
|
||||
type CreditsPackageListingData,
|
||||
type CourseListingData,
|
||||
type BundleListingData,
|
||||
} from '../Utils/GDevelopServices/Shop';
|
||||
import {
|
||||
shouldUseAppStoreProduct,
|
||||
@@ -18,25 +16,11 @@ import {
|
||||
import Coin from '../Credits/Icons/Coin';
|
||||
import { LineStackLayout } from '../UI/Layout';
|
||||
import Text from '../UI/Text';
|
||||
import { Column } from '../UI/Grid';
|
||||
import CheckCircle from '../UI/CustomSvgIcons/CheckCircle';
|
||||
import GDevelopThemeContext from '../UI/Theme/GDevelopThemeContext';
|
||||
|
||||
const styles = {
|
||||
icon: {
|
||||
width: 13,
|
||||
height: 13,
|
||||
position: 'relative',
|
||||
top: -1,
|
||||
},
|
||||
creditPriceContainer: {
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: 3,
|
||||
},
|
||||
columnOrSeparator: {
|
||||
marginTop: -3,
|
||||
marginBottom: -1,
|
||||
width: 12,
|
||||
height: 12,
|
||||
},
|
||||
};
|
||||
|
||||
@@ -44,13 +28,10 @@ type FormatProps = {|
|
||||
productListingData:
|
||||
| PrivateAssetPackListingData
|
||||
| PrivateGameTemplateListingData
|
||||
| CreditsPackageListingData
|
||||
| CourseListingData
|
||||
| BundleListingData,
|
||||
| CreditsPackageListingData,
|
||||
i18n: I18nType,
|
||||
usageType?: string,
|
||||
plainText?: boolean,
|
||||
showBothPrices?: 'column' | 'line', // If defined, will show both the credits price and the product price.
|
||||
|};
|
||||
|
||||
export const renderProductPrice = ({
|
||||
@@ -58,13 +39,11 @@ export const renderProductPrice = ({
|
||||
productListingData,
|
||||
usageType,
|
||||
plainText,
|
||||
showBothPrices,
|
||||
}: FormatProps): React.Node => {
|
||||
// For Credits packages & Bundles, on mobile, only show the app store product price.
|
||||
// Only use the app store product if it's a credits package.
|
||||
if (
|
||||
shouldUseAppStoreProduct() &&
|
||||
(productListingData.productType === 'CREDITS_PACKAGE' ||
|
||||
productListingData.productType === 'BUNDLE')
|
||||
productListingData.productType === 'CREDITS_PACKAGE'
|
||||
) {
|
||||
const appStoreProduct = getAppStoreProduct(
|
||||
productListingData.appStoreProductId
|
||||
@@ -72,15 +51,19 @@ export const renderProductPrice = ({
|
||||
return appStoreProduct ? appStoreProduct.price : '';
|
||||
}
|
||||
|
||||
const creditPrices = productListingData.creditPrices || [];
|
||||
const creditPrice = usageType
|
||||
? creditPrices.find(price => price.usageType === usageType)
|
||||
: creditPrices.length > 0
|
||||
? creditPrices[0]
|
||||
: null;
|
||||
// If we're on mobile, only show credits prices for asset packs & game templates.
|
||||
if (
|
||||
shouldUseAppStoreProduct() &&
|
||||
productListingData.productType !== 'CREDITS_PACKAGE'
|
||||
) {
|
||||
const creditPrices = productListingData.creditPrices;
|
||||
if (!creditPrices) return '';
|
||||
const creditPrice = usageType
|
||||
? creditPrices.find(price => price.usageType === usageType)
|
||||
: creditPrices.length > 0
|
||||
? creditPrices[0]
|
||||
: null;
|
||||
|
||||
// If we're on mobile, only show credits prices for other packages.
|
||||
if (shouldUseAppStoreProduct()) {
|
||||
if (!creditPrice) return '';
|
||||
return plainText ? (
|
||||
i18n._(t`${creditPrice.amount} credits`)
|
||||
@@ -113,40 +96,6 @@ export const renderProductPrice = ({
|
||||
|
||||
return plainText ? (
|
||||
formattedPrice
|
||||
) : showBothPrices && creditPrice ? (
|
||||
showBothPrices === 'column' ? (
|
||||
<Column alignItems="flex-end">
|
||||
<div style={styles.creditPriceContainer}>
|
||||
<Coin style={styles.icon} />
|
||||
<Text noMargin size="sub-title" color="inherit">
|
||||
{creditPrice.amount}
|
||||
</Text>
|
||||
</div>
|
||||
<span style={styles.columnOrSeparator}>
|
||||
<Text noMargin color="inherit">
|
||||
<Trans>or</Trans>
|
||||
</Text>
|
||||
</span>
|
||||
<Text noMargin size="sub-title" color="primary">
|
||||
{formattedPrice}
|
||||
</Text>
|
||||
</Column>
|
||||
) : (
|
||||
<LineStackLayout noMargin>
|
||||
<div style={styles.creditPriceContainer}>
|
||||
<Coin style={styles.icon} />
|
||||
<Text noMargin size="sub-title" color="inherit">
|
||||
{creditPrice.amount}
|
||||
</Text>
|
||||
</div>
|
||||
<Text noMargin color="inherit">
|
||||
<Trans>or</Trans>
|
||||
</Text>
|
||||
<Text noMargin size="sub-title" color="primary">
|
||||
{formattedPrice}
|
||||
</Text>
|
||||
</LineStackLayout>
|
||||
)
|
||||
) : (
|
||||
<Text noMargin size="sub-title" color="inherit">
|
||||
{formattedPrice}
|
||||
@@ -158,42 +107,29 @@ type ProductPriceOrOwnedProps = {|
|
||||
productListingData:
|
||||
| PrivateAssetPackListingData
|
||||
| PrivateGameTemplateListingData
|
||||
| CreditsPackageListingData
|
||||
| CourseListingData
|
||||
| BundleListingData,
|
||||
| CreditsPackageListingData,
|
||||
i18n: I18nType,
|
||||
usageType?: string,
|
||||
owned?: boolean,
|
||||
showBothPrices?: 'column' | 'line',
|
||||
|};
|
||||
|
||||
export const OwnedLabel = () => {
|
||||
const gdevelopTheme = React.useContext(GDevelopThemeContext);
|
||||
return (
|
||||
<LineStackLayout noMargin alignItems="center">
|
||||
<CheckCircle
|
||||
style={{
|
||||
color: gdevelopTheme.message.valid,
|
||||
}}
|
||||
/>
|
||||
<Text noMargin size="sub-title" color="inherit">
|
||||
<Trans>Owned</Trans>
|
||||
</Text>
|
||||
</LineStackLayout>
|
||||
);
|
||||
};
|
||||
|
||||
export const getProductPriceOrOwnedLabel = ({
|
||||
i18n,
|
||||
productListingData,
|
||||
usageType,
|
||||
owned,
|
||||
showBothPrices,
|
||||
}: ProductPriceOrOwnedProps): React.Node => {
|
||||
return owned ? (
|
||||
<OwnedLabel />
|
||||
<LineStackLayout noMargin alignItems="center">
|
||||
<Text noMargin size="sub-title">
|
||||
✅
|
||||
</Text>
|
||||
<Text noMargin size="sub-title" color="inherit">
|
||||
<Trans>Owned</Trans>
|
||||
</Text>
|
||||
</LineStackLayout>
|
||||
) : (
|
||||
renderProductPrice({ i18n, productListingData, usageType, showBothPrices })
|
||||
renderProductPrice({ i18n, productListingData, usageType })
|
||||
);
|
||||
};
|
||||
|
||||
@@ -201,9 +137,7 @@ type ProductPriceTagProps = {|
|
||||
productListingData:
|
||||
| PrivateAssetPackListingData
|
||||
| PrivateGameTemplateListingData
|
||||
| CreditsPackageListingData
|
||||
| CourseListingData
|
||||
| BundleListingData,
|
||||
| CreditsPackageListingData,
|
||||
usageType?: string,
|
||||
/**
|
||||
* To be used when the component is over an element for which
|
||||
|
@@ -9,20 +9,18 @@ import {
|
||||
import {
|
||||
type PrivateAssetPackListingData,
|
||||
type PrivateGameTemplateListingData,
|
||||
type BundleListingData,
|
||||
type CourseListingData,
|
||||
} from '../Utils/GDevelopServices/Shop';
|
||||
import type { ExampleShortHeader } from '../Utils/GDevelopServices/Example';
|
||||
import GridListTile from '@material-ui/core/GridListTile';
|
||||
import createStyles from '@material-ui/core/styles/createStyles';
|
||||
import makeStyles from '@material-ui/core/styles/makeStyles';
|
||||
import { shouldValidate } from '../UI/KeyboardShortcuts/InteractionKeys';
|
||||
import { CorsAwareImage } from '../UI/CorsAwareImage';
|
||||
import { textEllipsisStyle } from '../UI/TextEllipsis';
|
||||
import { Column, Line, Spacer } from '../UI/Grid';
|
||||
import Text from '../UI/Text';
|
||||
import { Trans } from '@lingui/macro';
|
||||
import ProductPriceTag, {
|
||||
OwnedLabel,
|
||||
renderProductPrice,
|
||||
} from './ProductPriceTag';
|
||||
import ProductPriceTag, { renderProductPrice } from './ProductPriceTag';
|
||||
import { AssetCard } from './AssetCard';
|
||||
import FolderIcon from '../UI/CustomSvgIcons/Folder';
|
||||
import FlatButton from '../UI/FlatButton';
|
||||
@@ -32,8 +30,6 @@ import { ResponsiveLineStackLayout } from '../UI/Layout';
|
||||
import Skeleton from '@material-ui/lab/Skeleton';
|
||||
import EmptyMessage from '../UI/EmptyMessage';
|
||||
import { useResponsiveWindowSize } from '../UI/Responsive/ResponsiveWindowMeasurer';
|
||||
import { CardWidget } from '../MainFrame/EditorContainers/HomePage/CardWidget';
|
||||
import { selectMessageByLocale } from '../Utils/i18n/MessageByLocale';
|
||||
|
||||
const styles = {
|
||||
priceTagContainer: {
|
||||
@@ -45,7 +41,7 @@ const styles = {
|
||||
previewImage: {
|
||||
width: '100%',
|
||||
display: 'block',
|
||||
objectFit: 'contain',
|
||||
objectFit: 'cover',
|
||||
borderRadius: 8,
|
||||
border: '1px solid lightgrey',
|
||||
boxSizing: 'border-box', // Take border in account for sizing to avoid cumulative layout shift.
|
||||
@@ -85,6 +81,11 @@ const styles = {
|
||||
minWidth: 200,
|
||||
margin: 4,
|
||||
},
|
||||
paper: {
|
||||
margin: 4,
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
},
|
||||
packTitle: {
|
||||
...textEllipsisStyle,
|
||||
overflowWrap: 'break-word',
|
||||
@@ -111,6 +112,20 @@ const styles = {
|
||||
},
|
||||
};
|
||||
|
||||
const useStylesForGridListItem = ({ disabled }: {| disabled?: boolean |}) =>
|
||||
makeStyles(theme =>
|
||||
createStyles({
|
||||
tile: !disabled
|
||||
? {
|
||||
transition: 'transform 0.3s ease-in-out',
|
||||
'&:hover': {
|
||||
transform: 'scale(1.02)',
|
||||
},
|
||||
}
|
||||
: {},
|
||||
})
|
||||
)();
|
||||
|
||||
export const AssetCardTile = ({
|
||||
assetShortHeader,
|
||||
onOpenDetails,
|
||||
@@ -126,21 +141,30 @@ export const AssetCardTile = ({
|
||||
hideShortDescription?: boolean,
|
||||
disabled?: boolean,
|
||||
|}) => {
|
||||
const classesForGridListItem = useStylesForGridListItem({
|
||||
disabled,
|
||||
});
|
||||
|
||||
return (
|
||||
<GridListTile style={{ margin }}>
|
||||
<CardWidget
|
||||
onClick={!disabled ? onOpenDetails : undefined}
|
||||
size="large"
|
||||
disabled={disabled}
|
||||
noBorder
|
||||
>
|
||||
<AssetCard
|
||||
id={`asset-card-${assetShortHeader.name.replace(/\s/g, '-')}`}
|
||||
assetShortHeader={assetShortHeader}
|
||||
size={size}
|
||||
hideShortDescription={hideShortDescription}
|
||||
/>
|
||||
</CardWidget>
|
||||
<GridListTile
|
||||
classes={classesForGridListItem}
|
||||
tabIndex={0}
|
||||
onKeyPress={(event: SyntheticKeyboardEvent<HTMLLIElement>): void => {
|
||||
if (shouldValidate(event) && !disabled) {
|
||||
onOpenDetails();
|
||||
}
|
||||
}}
|
||||
onClick={!disabled ? onOpenDetails : undefined}
|
||||
style={{
|
||||
margin,
|
||||
}}
|
||||
>
|
||||
<AssetCard
|
||||
id={`asset-card-${assetShortHeader.name.replace(/\s/g, '-')}`}
|
||||
assetShortHeader={assetShortHeader}
|
||||
size={size}
|
||||
hideShortDescription={hideShortDescription}
|
||||
/>
|
||||
</GridListTile>
|
||||
);
|
||||
};
|
||||
@@ -157,27 +181,29 @@ export const AssetFolderTile = ({
|
||||
style?: any,
|
||||
disabled?: boolean,
|
||||
|}) => {
|
||||
const classesForGridListItem = useStylesForGridListItem({
|
||||
disabled,
|
||||
});
|
||||
return (
|
||||
<GridListTile style={style}>
|
||||
<CardWidget
|
||||
onClick={!disabled ? onSelect : undefined}
|
||||
size="large"
|
||||
disabled={disabled}
|
||||
noBorder
|
||||
>
|
||||
<Column
|
||||
noMargin
|
||||
id={`asset-folder-${tag.replace(/\s/g, '-')}`}
|
||||
noOverflowParent
|
||||
>
|
||||
<Line alignItems="center">
|
||||
<FolderIcon style={styles.folderIcon} />
|
||||
<Text noMargin style={styles.folderTitle} size="sub-title">
|
||||
{capitalize(tag)}
|
||||
</Text>
|
||||
</Line>
|
||||
</Column>
|
||||
</CardWidget>
|
||||
<GridListTile
|
||||
classes={classesForGridListItem}
|
||||
tabIndex={0}
|
||||
onKeyPress={(event: SyntheticKeyboardEvent<HTMLLIElement>): void => {
|
||||
if (shouldValidate(event) && !disabled) {
|
||||
onSelect();
|
||||
}
|
||||
}}
|
||||
style={style}
|
||||
onClick={!disabled ? onSelect : undefined}
|
||||
>
|
||||
<Column noMargin id={`asset-folder-${tag.replace(/\s/g, '-')}`}>
|
||||
<Line alignItems="center">
|
||||
<FolderIcon style={styles.folderIcon} />
|
||||
<Text noMargin style={styles.folderTitle} size="sub-title">
|
||||
{capitalize(tag)}
|
||||
</Text>
|
||||
</Line>
|
||||
</Column>
|
||||
</GridListTile>
|
||||
);
|
||||
};
|
||||
@@ -194,46 +220,46 @@ export const PublicAssetPackTile = ({
|
||||
style?: any,
|
||||
disabled?: boolean,
|
||||
|}) => {
|
||||
const gdevelopTheme = React.useContext(GDevelopThemeContext);
|
||||
const classesForGridListItem = useStylesForGridListItem({
|
||||
disabled,
|
||||
});
|
||||
return (
|
||||
<GridListTile style={style}>
|
||||
<CardWidget
|
||||
onClick={!disabled ? onSelect : undefined}
|
||||
size="large"
|
||||
disabled={disabled}
|
||||
noBorder
|
||||
<GridListTile
|
||||
classes={classesForGridListItem}
|
||||
tabIndex={0}
|
||||
onKeyPress={(event: SyntheticKeyboardEvent<HTMLLIElement>): void => {
|
||||
if (shouldValidate(event) && !disabled) {
|
||||
onSelect();
|
||||
}
|
||||
}}
|
||||
style={style}
|
||||
onClick={!disabled ? onSelect : undefined}
|
||||
>
|
||||
<div
|
||||
id={`asset-pack-${assetPack.tag.replace(/\s/g, '-')}`}
|
||||
style={styles.paper}
|
||||
>
|
||||
<Column
|
||||
noMargin
|
||||
expand
|
||||
id={`asset-pack-${assetPack.tag.replace(/\s/g, '-')}`}
|
||||
noOverflowParent
|
||||
>
|
||||
<CorsAwareImage
|
||||
key={assetPack.name}
|
||||
style={{
|
||||
...styles.previewImage,
|
||||
background: gdevelopTheme.paper.backgroundColor.light,
|
||||
}}
|
||||
src={assetPack.thumbnailUrl}
|
||||
alt={`Preview image of asset pack ${assetPack.name}`}
|
||||
loading="lazy"
|
||||
/>
|
||||
<Column>
|
||||
<Line justifyContent="space-between" noMargin>
|
||||
<Text style={styles.packTitle} size="body2">
|
||||
{assetPack.name}
|
||||
</Text>
|
||||
<Text style={styles.packTitle} color="primary" size="body2">
|
||||
<Trans>{assetPack.assetsCount} Assets</Trans>
|
||||
{assetPack.userFriendlyPrice
|
||||
? ' - ' + assetPack.userFriendlyPrice
|
||||
: null}
|
||||
</Text>
|
||||
</Line>
|
||||
</Column>
|
||||
<CorsAwareImage
|
||||
key={assetPack.name}
|
||||
style={styles.previewImage}
|
||||
src={assetPack.thumbnailUrl}
|
||||
alt={`Preview image of asset pack ${assetPack.name}`}
|
||||
loading="lazy"
|
||||
/>
|
||||
<Column>
|
||||
<Line justifyContent="space-between" noMargin>
|
||||
<Text style={styles.packTitle} size="body2">
|
||||
{assetPack.name}
|
||||
</Text>
|
||||
<Text style={styles.packTitle} color="primary" size="body2">
|
||||
<Trans>{assetPack.assetsCount} Assets</Trans>
|
||||
{assetPack.userFriendlyPrice
|
||||
? ' - ' + assetPack.userFriendlyPrice
|
||||
: null}
|
||||
</Text>
|
||||
</Line>
|
||||
</Column>
|
||||
</CardWidget>
|
||||
</div>
|
||||
</GridListTile>
|
||||
);
|
||||
};
|
||||
@@ -252,78 +278,73 @@ export const PrivateAssetPackTile = ({
|
||||
owned: boolean,
|
||||
disabled?: boolean,
|
||||
|}) => {
|
||||
const gdevelopTheme = React.useContext(GDevelopThemeContext);
|
||||
const classesForGridListItem = useStylesForGridListItem({
|
||||
disabled,
|
||||
});
|
||||
return (
|
||||
<GridListTile style={style}>
|
||||
<CardWidget
|
||||
onClick={!disabled ? onSelect : undefined}
|
||||
size="large"
|
||||
disabled={disabled}
|
||||
noBorder
|
||||
>
|
||||
<Column noMargin expand noOverflowParent>
|
||||
<div style={styles.thumbnailContainer}>
|
||||
<CorsAwareImage
|
||||
key={assetPackListingData.name}
|
||||
style={{
|
||||
...styles.previewImage,
|
||||
background: gdevelopTheme.paper.backgroundColor.light,
|
||||
}}
|
||||
src={assetPackListingData.thumbnailUrls[0]}
|
||||
alt={`Preview image of asset pack ${assetPackListingData.name}`}
|
||||
loading="lazy"
|
||||
/>
|
||||
{assetPackListingData.redeemConditions && !owned && (
|
||||
<div style={styles.redeemableContainer}>
|
||||
<img
|
||||
src="res/small-diamond.svg"
|
||||
style={styles.redeemableDiamondIcon}
|
||||
alt="diamond"
|
||||
/>
|
||||
<Text color="inherit" noMargin>
|
||||
<Trans>Claim this pack</Trans>
|
||||
</Text>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div style={styles.priceTagContainer}>
|
||||
<ProductPriceTag
|
||||
productListingData={assetPackListingData}
|
||||
withOverlay
|
||||
owned={owned}
|
||||
/>
|
||||
</div>
|
||||
<Column>
|
||||
<Line justifyContent="space-between" noMargin>
|
||||
<Text style={styles.packTitle} size="body2">
|
||||
{assetPackListingData.name}
|
||||
<GridListTile
|
||||
classes={classesForGridListItem}
|
||||
tabIndex={0}
|
||||
onKeyPress={(event: SyntheticKeyboardEvent<HTMLLIElement>): void => {
|
||||
if (shouldValidate(event) && !disabled) {
|
||||
onSelect();
|
||||
}
|
||||
}}
|
||||
style={style}
|
||||
onClick={!disabled ? onSelect : undefined}
|
||||
>
|
||||
<div style={styles.paper}>
|
||||
<div style={styles.thumbnailContainer}>
|
||||
<CorsAwareImage
|
||||
key={assetPackListingData.name}
|
||||
style={styles.previewImage}
|
||||
src={assetPackListingData.thumbnailUrls[0]}
|
||||
alt={`Preview image of asset pack ${assetPackListingData.name}`}
|
||||
loading="lazy"
|
||||
/>
|
||||
{assetPackListingData.redeemConditions && !owned && (
|
||||
<div style={styles.redeemableContainer}>
|
||||
<img
|
||||
src="res/small-diamond.svg"
|
||||
style={styles.redeemableDiamondIcon}
|
||||
alt="diamond"
|
||||
/>
|
||||
<Text color="inherit" noMargin>
|
||||
<Trans>Claim this pack</Trans>
|
||||
</Text>
|
||||
<Text style={styles.packTitle} color="primary" size="body2">
|
||||
{assetPackListingData.description}
|
||||
</Text>
|
||||
</Line>
|
||||
</Column>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div style={styles.priceTagContainer}>
|
||||
<ProductPriceTag
|
||||
productListingData={assetPackListingData}
|
||||
withOverlay
|
||||
owned={owned}
|
||||
/>
|
||||
</div>
|
||||
<Column>
|
||||
<Line justifyContent="space-between" noMargin>
|
||||
<Text style={styles.packTitle} size="body2">
|
||||
{assetPackListingData.name}
|
||||
</Text>
|
||||
<Text style={styles.packTitle} color="primary" size="body2">
|
||||
{assetPackListingData.description}
|
||||
</Text>
|
||||
</Line>
|
||||
</Column>
|
||||
</CardWidget>
|
||||
</div>
|
||||
</GridListTile>
|
||||
);
|
||||
};
|
||||
|
||||
export const PromoBundleCard = ({
|
||||
bundleProductListingData,
|
||||
includedProductListingData,
|
||||
productListingData,
|
||||
onSelect,
|
||||
owned,
|
||||
}: {|
|
||||
bundleProductListingData:
|
||||
productListingData:
|
||||
| PrivateAssetPackListingData
|
||||
| PrivateGameTemplateListingData
|
||||
| BundleListingData,
|
||||
includedProductListingData:
|
||||
| PrivateAssetPackListingData
|
||||
| PrivateGameTemplateListingData
|
||||
| CourseListingData
|
||||
| BundleListingData,
|
||||
| PrivateGameTemplateListingData,
|
||||
onSelect: () => void,
|
||||
owned: boolean,
|
||||
|}) => {
|
||||
@@ -341,44 +362,52 @@ export const PromoBundleCard = ({
|
||||
<ResponsiveLineStackLayout expand noMargin noResponsiveLandscape>
|
||||
<div style={styles.promoImageContainer}>
|
||||
<CorsAwareImage
|
||||
key={bundleProductListingData.name}
|
||||
key={productListingData.name}
|
||||
style={{
|
||||
...styles.previewImage,
|
||||
...styles.promoImage,
|
||||
}}
|
||||
src={bundleProductListingData.thumbnailUrls[0]}
|
||||
alt={`Preview image of bundle ${
|
||||
bundleProductListingData.name
|
||||
}`}
|
||||
src={productListingData.thumbnailUrls[0]}
|
||||
alt={`Preview image of bundle ${productListingData.name}`}
|
||||
loading="lazy"
|
||||
/>
|
||||
</div>
|
||||
<Column expand alignItems="flex-start" justifyContent="center">
|
||||
<Text color="primary" size="section-title">
|
||||
{!owned ? (
|
||||
<Trans>Get {bundleProductListingData.name}!</Trans>
|
||||
<Trans>Get {productListingData.description}!</Trans>
|
||||
) : productListingData.productType === 'ASSET_PACK' ? (
|
||||
<Trans>You already own this pack!</Trans>
|
||||
) : (
|
||||
<Trans>
|
||||
You already own {bundleProductListingData.name}!
|
||||
</Trans>
|
||||
<Trans>You already own this template!</Trans>
|
||||
)}
|
||||
</Text>
|
||||
<Text color="primary" size="body2">
|
||||
{!owned ? (
|
||||
<Trans>
|
||||
{includedProductListingData.name} is included in this
|
||||
bundle for{' '}
|
||||
{renderProductPrice({
|
||||
i18n,
|
||||
productListingData: bundleProductListingData,
|
||||
plainText: true,
|
||||
})}
|
||||
!
|
||||
</Trans>
|
||||
productListingData.productType === 'ASSET_PACK' ? (
|
||||
<Trans>
|
||||
This pack is included in this bundle for{' '}
|
||||
{renderProductPrice({
|
||||
i18n,
|
||||
productListingData,
|
||||
plainText: true,
|
||||
})}
|
||||
!
|
||||
</Trans>
|
||||
) : (
|
||||
<Trans>
|
||||
This template is included in this bundle for{' '}
|
||||
{renderProductPrice({
|
||||
i18n,
|
||||
productListingData,
|
||||
plainText: true,
|
||||
})}
|
||||
!
|
||||
</Trans>
|
||||
)
|
||||
) : (
|
||||
<Trans>
|
||||
{includedProductListingData.name} is included in the
|
||||
bundle {bundleProductListingData.name}.
|
||||
It is included in the bundle {productListingData.name}.
|
||||
</Trans>
|
||||
)}
|
||||
</Text>
|
||||
@@ -426,34 +455,40 @@ export const CategoryTile = ({
|
||||
style?: any,
|
||||
disabled?: boolean,
|
||||
|}) => {
|
||||
const classesForGridListItem = useStylesForGridListItem({
|
||||
disabled,
|
||||
});
|
||||
const gdevelopTheme = React.useContext(GDevelopThemeContext);
|
||||
return (
|
||||
<GridListTile style={style}>
|
||||
<CardWidget
|
||||
onClick={!disabled ? onSelect : undefined}
|
||||
size="large"
|
||||
disabled={disabled}
|
||||
noBorder
|
||||
>
|
||||
<Column noMargin alignItems="center" expand id={id} noOverflowParent>
|
||||
<CorsAwareImage
|
||||
style={{
|
||||
...styles.previewImage,
|
||||
background: gdevelopTheme.palette.primary,
|
||||
}}
|
||||
src={imageSource}
|
||||
alt={imageAlt}
|
||||
// No lazy loading because categories are the first seen tiles in the shop.
|
||||
/>
|
||||
<Column>
|
||||
<Line justifyContent="center" noMargin>
|
||||
<Text style={styles.packTitle} size="sub-title">
|
||||
{title}
|
||||
</Text>
|
||||
</Line>
|
||||
</Column>
|
||||
<GridListTile
|
||||
classes={classesForGridListItem}
|
||||
tabIndex={0}
|
||||
onKeyPress={(event: SyntheticKeyboardEvent<HTMLLIElement>): void => {
|
||||
if (shouldValidate(event) && !disabled) {
|
||||
onSelect();
|
||||
}
|
||||
}}
|
||||
style={style}
|
||||
onClick={!disabled ? onSelect : undefined}
|
||||
>
|
||||
<div id={id} style={styles.paper}>
|
||||
<CorsAwareImage
|
||||
style={{
|
||||
...styles.previewImage,
|
||||
background: gdevelopTheme.palette.primary,
|
||||
}}
|
||||
src={imageSource}
|
||||
alt={imageAlt}
|
||||
// No lazy loading because categories are the first seen tiles in the shop.
|
||||
/>
|
||||
<Column>
|
||||
<Line justifyContent="center" noMargin>
|
||||
<Text style={styles.packTitle} size="sub-title">
|
||||
{title}
|
||||
</Text>
|
||||
</Line>
|
||||
</Column>
|
||||
</CardWidget>
|
||||
</div>
|
||||
</GridListTile>
|
||||
);
|
||||
};
|
||||
@@ -473,179 +508,51 @@ export const PrivateGameTemplateTile = ({
|
||||
disabled?: boolean,
|
||||
|}) => {
|
||||
const { isMobile } = useResponsiveWindowSize();
|
||||
const gdevelopTheme = React.useContext(GDevelopThemeContext);
|
||||
const classesForGridListItem = useStylesForGridListItem({
|
||||
disabled,
|
||||
});
|
||||
return (
|
||||
<GridListTile style={style}>
|
||||
<CardWidget
|
||||
onClick={!disabled ? onSelect : undefined}
|
||||
size="large"
|
||||
disabled={disabled}
|
||||
noBorder
|
||||
>
|
||||
<Column noMargin expand noOverflowParent>
|
||||
<CorsAwareImage
|
||||
key={privateGameTemplateListingData.name}
|
||||
style={{
|
||||
...styles.previewImage,
|
||||
background: gdevelopTheme.paper.backgroundColor.light,
|
||||
}}
|
||||
src={privateGameTemplateListingData.thumbnailUrls[0]}
|
||||
alt={`Preview image of game template ${
|
||||
privateGameTemplateListingData.name
|
||||
}`}
|
||||
loading="lazy"
|
||||
<GridListTile
|
||||
classes={classesForGridListItem}
|
||||
tabIndex={0}
|
||||
onKeyPress={(event: SyntheticKeyboardEvent<HTMLLIElement>): void => {
|
||||
if (shouldValidate(event) && !disabled) {
|
||||
onSelect();
|
||||
}
|
||||
}}
|
||||
style={style}
|
||||
onClick={!disabled ? onSelect : undefined}
|
||||
>
|
||||
<div style={styles.paper}>
|
||||
<CorsAwareImage
|
||||
key={privateGameTemplateListingData.name}
|
||||
style={styles.previewImage}
|
||||
src={privateGameTemplateListingData.thumbnailUrls[0]}
|
||||
alt={`Preview image of game template ${
|
||||
privateGameTemplateListingData.name
|
||||
}`}
|
||||
loading="lazy"
|
||||
/>
|
||||
<div style={styles.priceTagContainer}>
|
||||
<ProductPriceTag
|
||||
productListingData={privateGameTemplateListingData}
|
||||
withOverlay
|
||||
owned={owned}
|
||||
/>
|
||||
<div style={styles.priceTagContainer}>
|
||||
<ProductPriceTag
|
||||
productListingData={privateGameTemplateListingData}
|
||||
withOverlay
|
||||
owned={owned}
|
||||
/>
|
||||
</div>
|
||||
<Column>
|
||||
{isMobile && <Spacer />}
|
||||
<Line justifyContent="flex-start" noMargin>
|
||||
<Text style={styles.packTitle} size="body2" noMargin={isMobile}>
|
||||
{privateGameTemplateListingData.name}
|
||||
</Text>
|
||||
</Line>
|
||||
</Column>
|
||||
</div>
|
||||
<Column>
|
||||
{isMobile && <Spacer />}
|
||||
<Line justifyContent="flex-start" noMargin>
|
||||
<Text style={styles.packTitle} size="body2" noMargin={isMobile}>
|
||||
{privateGameTemplateListingData.name}
|
||||
</Text>
|
||||
</Line>
|
||||
</Column>
|
||||
</CardWidget>
|
||||
</div>
|
||||
</GridListTile>
|
||||
);
|
||||
};
|
||||
|
||||
export const CourseTile = ({
|
||||
courseListingData,
|
||||
onSelect,
|
||||
style,
|
||||
owned,
|
||||
disabled,
|
||||
}: {|
|
||||
courseListingData: CourseListingData,
|
||||
onSelect: () => void,
|
||||
/** Props needed so that GridList component can adjust tile size */
|
||||
style?: any,
|
||||
owned: boolean,
|
||||
disabled?: boolean,
|
||||
|}) => {
|
||||
const { isMobile } = useResponsiveWindowSize();
|
||||
const gdevelopTheme = React.useContext(GDevelopThemeContext);
|
||||
return (
|
||||
<GridListTile style={style}>
|
||||
<CardWidget
|
||||
onClick={!disabled ? onSelect : undefined}
|
||||
size="large"
|
||||
disabled={disabled}
|
||||
noBorder
|
||||
>
|
||||
<Column noMargin expand noOverflowParent>
|
||||
<CorsAwareImage
|
||||
key={courseListingData.name}
|
||||
style={{
|
||||
...styles.previewImage,
|
||||
background: gdevelopTheme.paper.backgroundColor.light,
|
||||
}}
|
||||
src={courseListingData.thumbnailUrls[0]}
|
||||
alt={`Preview image of course ${courseListingData.name}`}
|
||||
loading="lazy"
|
||||
/>
|
||||
<div style={styles.priceTagContainer}>
|
||||
<ProductPriceTag
|
||||
productListingData={courseListingData}
|
||||
withOverlay
|
||||
owned={owned}
|
||||
/>
|
||||
</div>
|
||||
<Column>
|
||||
{isMobile && <Spacer />}
|
||||
<Line justifyContent="flex-start" noMargin>
|
||||
<Text style={styles.packTitle} size="body2" noMargin={isMobile}>
|
||||
{courseListingData.name}
|
||||
</Text>
|
||||
</Line>
|
||||
</Column>
|
||||
</Column>
|
||||
</CardWidget>
|
||||
</GridListTile>
|
||||
);
|
||||
};
|
||||
|
||||
export const BundleTile = ({
|
||||
bundleListingData,
|
||||
onSelect,
|
||||
style,
|
||||
owned,
|
||||
disabled,
|
||||
}: {|
|
||||
bundleListingData: BundleListingData,
|
||||
onSelect: () => void,
|
||||
/** Props needed so that GridList component can adjust tile size */
|
||||
style?: any,
|
||||
owned: boolean,
|
||||
disabled?: boolean,
|
||||
|}) => {
|
||||
const { isMobile } = useResponsiveWindowSize();
|
||||
const gdevelopTheme = React.useContext(GDevelopThemeContext);
|
||||
|
||||
return (
|
||||
<I18n>
|
||||
{({ i18n }) => (
|
||||
<GridListTile style={style}>
|
||||
<CardWidget
|
||||
onClick={!disabled ? onSelect : undefined}
|
||||
size="large"
|
||||
disabled={disabled}
|
||||
noBorder
|
||||
>
|
||||
<Column noMargin expand noOverflowParent>
|
||||
<CorsAwareImage
|
||||
key={bundleListingData.name}
|
||||
style={{
|
||||
...styles.previewImage,
|
||||
background: gdevelopTheme.paper.backgroundColor.light,
|
||||
}}
|
||||
src={bundleListingData.thumbnailUrls[0]}
|
||||
alt={`Preview image of bundle ${bundleListingData.name}`}
|
||||
loading="lazy"
|
||||
/>
|
||||
<div style={styles.priceTagContainer}>
|
||||
{bundleListingData ? (
|
||||
<ProductPriceTag
|
||||
productListingData={bundleListingData}
|
||||
withOverlay
|
||||
owned={owned}
|
||||
/>
|
||||
) : (
|
||||
<OwnedLabel />
|
||||
)}
|
||||
</div>
|
||||
<Column>
|
||||
{isMobile && <Spacer />}
|
||||
<Line justifyContent="flex-start" noMargin>
|
||||
<Text
|
||||
style={styles.packTitle}
|
||||
size="body2"
|
||||
noMargin={isMobile}
|
||||
>
|
||||
{bundleListingData.nameByLocale
|
||||
? selectMessageByLocale(
|
||||
i18n,
|
||||
bundleListingData.nameByLocale
|
||||
)
|
||||
: bundleListingData.name}
|
||||
</Text>
|
||||
</Line>
|
||||
</Column>
|
||||
</Column>
|
||||
</CardWidget>
|
||||
</GridListTile>
|
||||
)}
|
||||
</I18n>
|
||||
);
|
||||
};
|
||||
|
||||
export const ExampleTile = ({
|
||||
exampleShortHeader,
|
||||
onSelect,
|
||||
@@ -665,7 +572,6 @@ export const ExampleTile = ({
|
||||
disabled?: boolean,
|
||||
|}) => {
|
||||
const { isMobile } = useResponsiveWindowSize();
|
||||
const gdevelopTheme = React.useContext(GDevelopThemeContext);
|
||||
const thumbnailImgUrl = React.useMemo(
|
||||
() => {
|
||||
if (!exampleShortHeader) return '';
|
||||
@@ -681,68 +587,66 @@ export const ExampleTile = ({
|
||||
[exampleShortHeader, useQuickCustomizationThumbnail]
|
||||
);
|
||||
|
||||
const classesForGridListItem = useStylesForGridListItem({ disabled });
|
||||
return (
|
||||
<GridListTile style={style}>
|
||||
<CardWidget
|
||||
onClick={!disabled ? onSelect : undefined}
|
||||
size="large"
|
||||
disabled={disabled}
|
||||
noBorder
|
||||
>
|
||||
<Column noMargin expand noOverflowParent>
|
||||
{exampleShortHeader ? (
|
||||
thumbnailImgUrl ? (
|
||||
<CorsAwareImage
|
||||
key={exampleShortHeader.name}
|
||||
style={{
|
||||
...styles.previewImage,
|
||||
background: gdevelopTheme.paper.backgroundColor.light,
|
||||
}}
|
||||
src={thumbnailImgUrl}
|
||||
alt={`Preview image of example ${exampleShortHeader.name}`}
|
||||
loading="lazy"
|
||||
/>
|
||||
) : (
|
||||
<EmptyMessage
|
||||
style={{
|
||||
...styles.previewImage,
|
||||
display: 'flex',
|
||||
}}
|
||||
>
|
||||
{exampleShortHeader.name}
|
||||
</EmptyMessage>
|
||||
)
|
||||
) : (
|
||||
<Skeleton
|
||||
variant="rect"
|
||||
width="100%"
|
||||
height="100%"
|
||||
style={styles.dataLoadingSkeleton}
|
||||
<GridListTile
|
||||
classes={classesForGridListItem}
|
||||
tabIndex={0}
|
||||
onKeyPress={(event: SyntheticKeyboardEvent<HTMLLIElement>): void => {
|
||||
if (shouldValidate(event) && !disabled) {
|
||||
onSelect();
|
||||
}
|
||||
}}
|
||||
style={style}
|
||||
onClick={!disabled ? onSelect : undefined}
|
||||
>
|
||||
<div style={styles.paper}>
|
||||
{exampleShortHeader ? (
|
||||
thumbnailImgUrl ? (
|
||||
<CorsAwareImage
|
||||
key={exampleShortHeader.name}
|
||||
style={styles.previewImage}
|
||||
src={thumbnailImgUrl}
|
||||
alt={`Preview image of example ${exampleShortHeader.name}`}
|
||||
loading="lazy"
|
||||
/>
|
||||
)}
|
||||
<Column>
|
||||
{isMobile && <Spacer />}
|
||||
<Line
|
||||
justifyContent={centerTitle ? 'center' : 'flex-start'}
|
||||
noMargin
|
||||
) : (
|
||||
<EmptyMessage
|
||||
style={{
|
||||
...styles.previewImage,
|
||||
display: 'flex',
|
||||
}}
|
||||
>
|
||||
<Text
|
||||
style={styles.packTitle}
|
||||
size="body2"
|
||||
hidden={!exampleShortHeader}
|
||||
noMargin={isMobile}
|
||||
>
|
||||
{customTitle
|
||||
? customTitle
|
||||
: exampleShortHeader
|
||||
? exampleShortHeader.name
|
||||
: // Use some placeholder text to avoid layout shift while loading content.
|
||||
'Abcdef123'}
|
||||
</Text>
|
||||
</Line>
|
||||
</Column>
|
||||
{exampleShortHeader.name}
|
||||
</EmptyMessage>
|
||||
)
|
||||
) : (
|
||||
<Skeleton
|
||||
variant="rect"
|
||||
width="100%"
|
||||
height="100%"
|
||||
style={styles.dataLoadingSkeleton}
|
||||
/>
|
||||
)}
|
||||
<Column>
|
||||
{isMobile && <Spacer />}
|
||||
<Line justifyContent={centerTitle ? 'center' : 'flex-start'} noMargin>
|
||||
<Text
|
||||
style={styles.packTitle}
|
||||
size="body2"
|
||||
hidden={!exampleShortHeader}
|
||||
noMargin={isMobile}
|
||||
>
|
||||
{customTitle
|
||||
? customTitle
|
||||
: exampleShortHeader
|
||||
? exampleShortHeader.name
|
||||
: // Use some placeholder text to avoid layout shift while loading content.
|
||||
'Abcdef123'}
|
||||
</Text>
|
||||
</Line>
|
||||
</Column>
|
||||
</CardWidget>
|
||||
</div>
|
||||
</GridListTile>
|
||||
);
|
||||
};
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user