mirror of
https://github.com/4ian/GDevelop.git
synced 2025-10-15 10:19:04 +00:00
Compare commits
42 Commits
Author | SHA1 | Date | |
---|---|---|---|
![]() |
f0f3c257fa | ||
![]() |
42fce7d9ce | ||
![]() |
facac37fff | ||
![]() |
1272b601c6 | ||
![]() |
58e35cfaf5 | ||
![]() |
2befc9781b | ||
![]() |
8fb2872c36 | ||
![]() |
98033515c8 | ||
![]() |
25c02cea2e | ||
![]() |
6d5be78fec | ||
![]() |
f6e60085db | ||
![]() |
a61648af70 | ||
![]() |
2b496c6fd3 | ||
![]() |
1e984f0965 | ||
![]() |
37bed36315 | ||
![]() |
5394cc5201 | ||
![]() |
5e3dfb0e9c | ||
![]() |
f6a6c981f8 | ||
![]() |
415c1bfd2f | ||
![]() |
e4a911db25 | ||
![]() |
5fcd67d77b | ||
![]() |
86db08ac3f | ||
![]() |
8d735fc726 | ||
![]() |
1e1f4bb2a3 | ||
![]() |
d8000aca10 | ||
![]() |
a2660ff0dc | ||
![]() |
000d5785cf | ||
![]() |
9fe04712a9 | ||
![]() |
846afd9e0a | ||
![]() |
6125ff0f90 | ||
![]() |
a5428a8843 | ||
![]() |
19be45cda6 | ||
![]() |
889c97cb27 | ||
![]() |
1d83da41a9 | ||
![]() |
a65f2174eb | ||
![]() |
9db493e87e | ||
![]() |
49a3a18b51 | ||
![]() |
0489e7036b | ||
![]() |
794d5a781c | ||
![]() |
c21dfbcc1f | ||
![]() |
cc75db6d09 | ||
![]() |
48d35a50b5 |
@@ -52,10 +52,25 @@ void GD_CORE_API BuiltinExtensionsImplementer::ImplementsKeyboardExtension(
|
||||
.SetHidden();
|
||||
|
||||
extension
|
||||
.AddCondition("KeyFromTextPressed",
|
||||
_("Key pressed"),
|
||||
_("Check if a key is pressed"),
|
||||
_("_PARAM1_ key is pressed"),
|
||||
.AddCondition(
|
||||
"KeyFromTextPressed",
|
||||
_("Key pressed"),
|
||||
_("Check if a key is pressed. This stays true as long as "
|
||||
"the key is held down. To check if a key was pressed during "
|
||||
"the frame, use \"Key just pressed\" instead."),
|
||||
_("_PARAM1_ key is pressed"),
|
||||
"",
|
||||
"res/conditions/keyboard24.png",
|
||||
"res/conditions/keyboard.png")
|
||||
.AddCodeOnlyParameter("currentScene", "")
|
||||
.AddParameter("keyboardKey", _("Key to check"))
|
||||
.MarkAsSimple();
|
||||
|
||||
extension
|
||||
.AddCondition("KeyFromTextJustPressed",
|
||||
_("Key just pressed"),
|
||||
_("Check if a key was just pressed."),
|
||||
_("_PARAM1_ key was just pressed"),
|
||||
"",
|
||||
"res/conditions/keyboard24.png",
|
||||
"res/conditions/keyboard.png")
|
||||
@@ -66,7 +81,7 @@ void GD_CORE_API BuiltinExtensionsImplementer::ImplementsKeyboardExtension(
|
||||
extension
|
||||
.AddCondition("KeyFromTextReleased",
|
||||
_("Key released"),
|
||||
_("Check if a key was just released"),
|
||||
_("Check if a key was just released."),
|
||||
_("_PARAM1_ key is released"),
|
||||
"",
|
||||
"res/conditions/keyboard24.png",
|
||||
|
@@ -298,6 +298,19 @@ class GD_CORE_API BehaviorMetadata : public InstructionOrExpressionContainerMeta
|
||||
return *this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the behavior can be used on objects from event-based objects.
|
||||
*/
|
||||
bool IsRelevantForChildObjects() const { return isRelevantForChildObjects; }
|
||||
|
||||
/**
|
||||
* Set that behavior can't be used on objects from event-based objects.
|
||||
*/
|
||||
BehaviorMetadata &MarkAsIrrelevantForChildObjects() {
|
||||
isRelevantForChildObjects = false;
|
||||
return *this;
|
||||
}
|
||||
|
||||
QuickCustomization::Visibility GetQuickCustomizationVisibility() const {
|
||||
return quickCustomizationVisibility;
|
||||
}
|
||||
@@ -393,6 +406,7 @@ class GD_CORE_API BehaviorMetadata : public InstructionOrExpressionContainerMeta
|
||||
mutable std::vector<gd::String> requiredBehaviors;
|
||||
bool isPrivate = false;
|
||||
bool isHidden = false;
|
||||
bool isRelevantForChildObjects = true;
|
||||
gd::String openFullEditorLabel;
|
||||
QuickCustomization::Visibility quickCustomizationVisibility = QuickCustomization::Visibility::Default;
|
||||
|
||||
|
@@ -277,6 +277,10 @@ class GD_CORE_API MetadataProvider {
|
||||
return &metadata == &badObjectInfo;
|
||||
}
|
||||
|
||||
static bool IsBadEffectMetadata(const gd::EffectMetadata& metadata) {
|
||||
return &metadata == &badEffectMetadata;
|
||||
}
|
||||
|
||||
virtual ~MetadataProvider();
|
||||
|
||||
private:
|
||||
|
@@ -76,6 +76,7 @@ void ObjectAssetSerializer::SerializeTo(
|
||||
|
||||
double width = 0;
|
||||
double height = 0;
|
||||
std::unordered_set<gd::String> alreadyUsedVariantIdentifiers;
|
||||
if (project.HasEventsBasedObject(object.GetType())) {
|
||||
SerializerElement &variantsElement =
|
||||
objectAssetElement.AddChild("variants");
|
||||
@@ -87,7 +88,6 @@ void ObjectAssetSerializer::SerializeTo(
|
||||
height = variant->GetAreaMaxY() - variant->GetAreaMinY();
|
||||
}
|
||||
|
||||
std::unordered_set<gd::String> alreadyUsedVariantIdentifiers;
|
||||
gd::ObjectAssetSerializer::SerializeUsedVariantsTo(
|
||||
project, object, variantsElement, alreadyUsedVariantIdentifiers);
|
||||
}
|
||||
@@ -114,14 +114,24 @@ void ObjectAssetSerializer::SerializeTo(
|
||||
resourceElement.SetAttribute("name", resource.GetName());
|
||||
}
|
||||
|
||||
std::unordered_set<gd::String> usedExtensionNames;
|
||||
usedExtensionNames.insert(extensionName);
|
||||
for (auto &usedVariantIdentifier : alreadyUsedVariantIdentifiers) {
|
||||
usedExtensionNames.insert(PlatformExtension::GetExtensionFromFullObjectType(
|
||||
usedVariantIdentifier));
|
||||
}
|
||||
SerializerElement &requiredExtensionsElement =
|
||||
objectAssetElement.AddChild("requiredExtensions");
|
||||
requiredExtensionsElement.ConsiderAsArrayOf("requiredExtension");
|
||||
if (project.HasEventsFunctionsExtensionNamed(extensionName)) {
|
||||
SerializerElement &requiredExtensionElement =
|
||||
requiredExtensionsElement.AddChild("requiredExtension");
|
||||
requiredExtensionElement.SetAttribute("extensionName", extensionName);
|
||||
requiredExtensionElement.SetAttribute("extensionVersion", "1.0.0");
|
||||
for (auto &usedExtensionName : usedExtensionNames) {
|
||||
if (project.HasEventsFunctionsExtensionNamed(usedExtensionName)) {
|
||||
auto &extension = project.GetEventsFunctionsExtension(usedExtensionName);
|
||||
SerializerElement &requiredExtensionElement =
|
||||
requiredExtensionsElement.AddChild("requiredExtension");
|
||||
requiredExtensionElement.SetAttribute("extensionName", usedExtensionName);
|
||||
requiredExtensionElement.SetAttribute("extensionVersion",
|
||||
extension.GetVersion());
|
||||
}
|
||||
}
|
||||
|
||||
// TODO This can be removed when the asset script no longer require it.
|
||||
|
@@ -248,12 +248,13 @@ gd::String PropertyFunctionGenerator::GetStringifiedExtraInfo(
|
||||
gd::String arrayString;
|
||||
arrayString += "[";
|
||||
bool isFirst = true;
|
||||
for (const gd::String &choice : property.GetExtraInfo()) {
|
||||
for (const auto &choice : property.GetChoices()) {
|
||||
if (!isFirst) {
|
||||
arrayString += ",";
|
||||
}
|
||||
isFirst = false;
|
||||
arrayString += "\"" + choice + "\"";
|
||||
// TODO Handle labels (and search "choice label")
|
||||
arrayString += "\"" + choice.GetValue() + "\"";
|
||||
}
|
||||
arrayString += "]";
|
||||
return arrayString;
|
||||
|
@@ -75,6 +75,17 @@ void ResourceExposer::ExposeProjectResources(
|
||||
// Expose global objects configuration resources
|
||||
auto objectWorker = gd::GetResourceWorkerOnObjects(project, worker);
|
||||
objectWorker.Launch(project.GetObjects());
|
||||
|
||||
// Exposed extension event resources
|
||||
// Note that using resources in extensions is very unlikely and probably not
|
||||
// worth the effort of something smart.
|
||||
auto eventWorker = gd::GetResourceWorkerOnEvents(project, worker);
|
||||
for (std::size_t e = 0; e < project.GetEventsFunctionsExtensionsCount();
|
||||
e++) {
|
||||
auto &eventsFunctionsExtension = project.GetEventsFunctionsExtension(e);
|
||||
gd::ProjectBrowserHelper::ExposeEventsFunctionsExtensionEvents(
|
||||
project, eventsFunctionsExtension, eventWorker);
|
||||
}
|
||||
}
|
||||
|
||||
void ResourceExposer::ExposeLayoutResources(
|
||||
@@ -103,16 +114,6 @@ void ResourceExposer::ExposeLayoutResources(
|
||||
auto eventWorker = gd::GetResourceWorkerOnEvents(project, worker);
|
||||
gd::ProjectBrowserHelper::ExposeLayoutEventsAndDependencies(
|
||||
project, layout, eventWorker);
|
||||
|
||||
// Exposed extension event resources
|
||||
// Note that using resources in extensions is very unlikely and probably not
|
||||
// worth the effort of something smart.
|
||||
for (std::size_t e = 0; e < project.GetEventsFunctionsExtensionsCount();
|
||||
e++) {
|
||||
auto &eventsFunctionsExtension = project.GetEventsFunctionsExtension(e);
|
||||
gd::ProjectBrowserHelper::ExposeEventsFunctionsExtensionEvents(
|
||||
project, eventsFunctionsExtension, eventWorker);
|
||||
}
|
||||
}
|
||||
|
||||
void ResourceExposer::ExposeEffectResources(
|
||||
|
@@ -34,6 +34,20 @@ void PropertyDescriptor::SerializeTo(SerializerElement& element) const {
|
||||
}
|
||||
}
|
||||
|
||||
if (!choices.empty()
|
||||
// Compatibility with GD <= 5.5.239
|
||||
|| !extraInformation.empty()
|
||||
// end of compatibility code
|
||||
) {
|
||||
SerializerElement &choicesElement = element.AddChild("choices");
|
||||
choicesElement.ConsiderAsArrayOf("choice");
|
||||
for (const auto &choice : choices) {
|
||||
auto &choiceElement = choicesElement.AddChild("Choice");
|
||||
choiceElement.SetStringAttribute("value", choice.GetValue());
|
||||
choiceElement.SetStringAttribute("label", choice.GetLabel());
|
||||
}
|
||||
}
|
||||
|
||||
if (hidden) {
|
||||
element.AddChild("hidden").SetBoolValue(hidden);
|
||||
}
|
||||
@@ -80,6 +94,26 @@ void PropertyDescriptor::UnserializeFrom(const SerializerElement& element) {
|
||||
extraInformationElement.GetChild(i).GetStringValue());
|
||||
}
|
||||
|
||||
if (element.HasChild("choices")) {
|
||||
choices.clear();
|
||||
const SerializerElement &choicesElement = element.GetChild("choices");
|
||||
choicesElement.ConsiderAsArrayOf("choice");
|
||||
for (std::size_t i = 0; i < choicesElement.GetChildrenCount(); ++i) {
|
||||
auto &choiceElement = choicesElement.GetChild(i);
|
||||
AddChoice(choiceElement.GetStringAttribute("value"),
|
||||
choiceElement.GetStringAttribute("label"));
|
||||
}
|
||||
}
|
||||
// Compatibility with GD <= 5.5.239
|
||||
else if (type == "Choice") {
|
||||
choices.clear();
|
||||
for (auto &choiceValue : extraInformation) {
|
||||
AddChoice(choiceValue, choiceValue);
|
||||
}
|
||||
extraInformation.clear();
|
||||
}
|
||||
// end of compatibility code
|
||||
|
||||
hidden = element.HasChild("hidden")
|
||||
? element.GetChild("hidden").GetBoolValue()
|
||||
: false;
|
||||
|
@@ -116,6 +116,11 @@ class GD_CORE_API PropertyDescriptor {
|
||||
return *this;
|
||||
}
|
||||
|
||||
PropertyDescriptor& ClearChoices() {
|
||||
choices.clear();
|
||||
return *this;
|
||||
}
|
||||
|
||||
PropertyDescriptor& AddChoice(const gd::String& value,
|
||||
const gd::String& label) {
|
||||
choices.push_back(PropertyDescriptorChoice(value, label));
|
||||
|
@@ -764,129 +764,6 @@ TEST_CASE("ArbitraryResourceWorker", "[common][resources]") {
|
||||
REQUIRE(worker.audios[0] == "res4");
|
||||
}
|
||||
|
||||
SECTION("Can find resource usages in event-based functions") {
|
||||
gd::Project project;
|
||||
gd::Platform platform;
|
||||
SetupProjectWithDummyPlatform(project, platform);
|
||||
|
||||
project.GetResourcesManager().AddResource(
|
||||
"res1", "path/to/file1.png", "image");
|
||||
project.GetResourcesManager().AddResource(
|
||||
"res2", "path/to/file2.png", "image");
|
||||
project.GetResourcesManager().AddResource(
|
||||
"res3", "path/to/file3.png", "image");
|
||||
ArbitraryResourceWorkerTest worker(project.GetResourcesManager());
|
||||
|
||||
auto& extension = project.InsertNewEventsFunctionsExtension("MyEventExtension", 0);
|
||||
auto &function = extension.GetEventsFunctions().InsertNewEventsFunction(
|
||||
"MyFreeFunction", 0);
|
||||
|
||||
gd::StandardEvent standardEvent;
|
||||
gd::Instruction instruction;
|
||||
instruction.SetType("MyExtension::DoSomethingWithResources");
|
||||
instruction.SetParametersCount(3);
|
||||
instruction.SetParameter(0, "res3");
|
||||
instruction.SetParameter(1, "res1");
|
||||
instruction.SetParameter(2, "res4");
|
||||
standardEvent.GetActions().Insert(instruction);
|
||||
function.GetEvents().InsertEvent(standardEvent);
|
||||
|
||||
auto& layout = project.InsertNewLayout("MyScene", 0);
|
||||
|
||||
// MyEventExtension::MyFreeFunction doesn't need to be actually used in
|
||||
// events because the implementation is naive.
|
||||
|
||||
gd::ResourceExposer::ExposeLayoutResources(project, layout, worker);
|
||||
REQUIRE(worker.bitmapFonts.size() == 1);
|
||||
REQUIRE(worker.bitmapFonts[0] == "res3");
|
||||
REQUIRE(worker.images.size() == 1);
|
||||
REQUIRE(worker.images[0] == "res1");
|
||||
REQUIRE(worker.audios.size() == 1);
|
||||
REQUIRE(worker.audios[0] == "res4");
|
||||
}
|
||||
|
||||
SECTION("Can find resource usages in event-based behavior functions") {
|
||||
gd::Project project;
|
||||
gd::Platform platform;
|
||||
SetupProjectWithDummyPlatform(project, platform);
|
||||
|
||||
project.GetResourcesManager().AddResource(
|
||||
"res1", "path/to/file1.png", "image");
|
||||
project.GetResourcesManager().AddResource(
|
||||
"res2", "path/to/file2.png", "image");
|
||||
project.GetResourcesManager().AddResource(
|
||||
"res3", "path/to/file3.png", "image");
|
||||
ArbitraryResourceWorkerTest worker(project.GetResourcesManager());
|
||||
|
||||
auto& extension = project.InsertNewEventsFunctionsExtension("MyEventExtension", 0);
|
||||
auto& behavior = extension.GetEventsBasedBehaviors().InsertNew("MyBehavior", 0);
|
||||
auto& function = behavior.GetEventsFunctions().InsertNewEventsFunction("MyFunction", 0);
|
||||
|
||||
gd::StandardEvent standardEvent;
|
||||
gd::Instruction instruction;
|
||||
instruction.SetType("MyExtension::DoSomethingWithResources");
|
||||
instruction.SetParametersCount(3);
|
||||
instruction.SetParameter(0, "res3");
|
||||
instruction.SetParameter(1, "res1");
|
||||
instruction.SetParameter(2, "res4");
|
||||
standardEvent.GetActions().Insert(instruction);
|
||||
function.GetEvents().InsertEvent(standardEvent);
|
||||
|
||||
auto& layout = project.InsertNewLayout("MyScene", 0);
|
||||
|
||||
// MyEventExtension::MyBehavior::MyFunction doesn't need to be actually used in
|
||||
// events because the implementation is naive.
|
||||
|
||||
gd::ResourceExposer::ExposeLayoutResources(project, layout, worker);
|
||||
REQUIRE(worker.bitmapFonts.size() == 1);
|
||||
REQUIRE(worker.bitmapFonts[0] == "res3");
|
||||
REQUIRE(worker.images.size() == 1);
|
||||
REQUIRE(worker.images[0] == "res1");
|
||||
REQUIRE(worker.audios.size() == 1);
|
||||
REQUIRE(worker.audios[0] == "res4");
|
||||
}
|
||||
|
||||
SECTION("Can find resource usages in event-based object functions") {
|
||||
gd::Project project;
|
||||
gd::Platform platform;
|
||||
SetupProjectWithDummyPlatform(project, platform);
|
||||
|
||||
project.GetResourcesManager().AddResource(
|
||||
"res1", "path/to/file1.png", "image");
|
||||
project.GetResourcesManager().AddResource(
|
||||
"res2", "path/to/file2.png", "image");
|
||||
project.GetResourcesManager().AddResource(
|
||||
"res3", "path/to/file3.png", "image");
|
||||
ArbitraryResourceWorkerTest worker(project.GetResourcesManager());
|
||||
|
||||
auto& extension = project.InsertNewEventsFunctionsExtension("MyEventExtension", 0);
|
||||
auto& object = extension.GetEventsBasedObjects().InsertNew("MyObject", 0);
|
||||
auto& function = object.GetEventsFunctions().InsertNewEventsFunction("MyFunction", 0);
|
||||
|
||||
gd::StandardEvent standardEvent;
|
||||
gd::Instruction instruction;
|
||||
instruction.SetType("MyExtension::DoSomethingWithResources");
|
||||
instruction.SetParametersCount(3);
|
||||
instruction.SetParameter(0, "res3");
|
||||
instruction.SetParameter(1, "res1");
|
||||
instruction.SetParameter(2, "res4");
|
||||
standardEvent.GetActions().Insert(instruction);
|
||||
function.GetEvents().InsertEvent(standardEvent);
|
||||
|
||||
auto& layout = project.InsertNewLayout("MyScene", 0);
|
||||
|
||||
// MyEventExtension::MyObject::MyFunction doesn't need to be actually used in
|
||||
// events because the implementation is naive.
|
||||
|
||||
gd::ResourceExposer::ExposeLayoutResources(project, layout, worker);
|
||||
REQUIRE(worker.bitmapFonts.size() == 1);
|
||||
REQUIRE(worker.bitmapFonts[0] == "res3");
|
||||
REQUIRE(worker.images.size() == 1);
|
||||
REQUIRE(worker.images[0] == "res1");
|
||||
REQUIRE(worker.audios.size() == 1);
|
||||
REQUIRE(worker.audios[0] == "res4");
|
||||
}
|
||||
|
||||
SECTION("Can find resource usages in layer effects") {
|
||||
gd::Project project;
|
||||
gd::Platform platform;
|
||||
|
@@ -139,8 +139,8 @@ TEST_CASE("PropertyFunctionGenerator", "[common]") {
|
||||
.SetLabel("Dot shape")
|
||||
.SetDescription("The shape is used for collision.")
|
||||
.SetGroup("Movement");
|
||||
property.GetExtraInfo().push_back("Dot shape");
|
||||
property.GetExtraInfo().push_back("Bounding disk");
|
||||
property.AddChoice("DotShape", "Dot shape");
|
||||
property.AddChoice("BoundingDisk", "Bounding disk");
|
||||
|
||||
gd::PropertyFunctionGenerator::GenerateBehaviorGetterAndSetter(
|
||||
project, extension, behavior, property, false);
|
||||
@@ -157,7 +157,7 @@ TEST_CASE("PropertyFunctionGenerator", "[common]") {
|
||||
gd::EventsFunction::ExpressionAndCondition);
|
||||
REQUIRE(getter.GetExpressionType().GetName() == "stringWithSelector");
|
||||
REQUIRE(getter.GetExpressionType().GetExtraInfo() ==
|
||||
"[\"Dot shape\",\"Bounding disk\"]");
|
||||
"[\"DotShape\",\"BoundingDisk\"]");
|
||||
}
|
||||
|
||||
SECTION("Can generate functions for a boolean property in a behavior") {
|
||||
@@ -386,8 +386,8 @@ TEST_CASE("PropertyFunctionGenerator", "[common]") {
|
||||
.SetLabel("Dot shape")
|
||||
.SetDescription("The shape is used for collision.")
|
||||
.SetGroup("Movement");
|
||||
property.GetExtraInfo().push_back("Dot shape");
|
||||
property.GetExtraInfo().push_back("Bounding disk");
|
||||
property.AddChoice("DotShape", "Dot shape");
|
||||
property.AddChoice("BoundingDisk", "Bounding disk");
|
||||
|
||||
gd::PropertyFunctionGenerator::GenerateObjectGetterAndSetter(
|
||||
project, extension, object, property);
|
||||
@@ -404,7 +404,7 @@ TEST_CASE("PropertyFunctionGenerator", "[common]") {
|
||||
gd::EventsFunction::ExpressionAndCondition);
|
||||
REQUIRE(getter.GetExpressionType().GetName() == "stringWithSelector");
|
||||
REQUIRE(getter.GetExpressionType().GetExtraInfo() ==
|
||||
"[\"Dot shape\",\"Bounding disk\"]");
|
||||
"[\"DotShape\",\"BoundingDisk\"]");
|
||||
}
|
||||
|
||||
SECTION("Can generate functions for a boolean property in an object") {
|
||||
|
@@ -1908,7 +1908,9 @@ module.exports = {
|
||||
.addEffect('AmbientLight')
|
||||
.setFullName(_('Ambient light'))
|
||||
.setDescription(
|
||||
_('A light that illuminates all objects from every direction.')
|
||||
_(
|
||||
'A light that illuminates all objects from every direction. Often used along with a Directional light (though a Hemisphere light can be used instead of an Ambient light).'
|
||||
)
|
||||
)
|
||||
.markAsNotWorkingForObjects()
|
||||
.markAsOnlyWorkingFor3D()
|
||||
@@ -1929,7 +1931,11 @@ module.exports = {
|
||||
const effect = extension
|
||||
.addEffect('DirectionalLight')
|
||||
.setFullName(_('Directional light'))
|
||||
.setDescription(_('A very far light source like the sun.'))
|
||||
.setDescription(
|
||||
_(
|
||||
"A very far light source like the sun. This is the light to use for casting shadows for 3D objects (other lights won't emit shadows). Often used along with a Hemisphere light."
|
||||
)
|
||||
)
|
||||
.markAsNotWorkingForObjects()
|
||||
.markAsOnlyWorkingFor3D()
|
||||
.addIncludeFile('Extensions/3D/DirectionalLight.js');
|
||||
@@ -2013,7 +2019,7 @@ module.exports = {
|
||||
.setFullName(_('Hemisphere light'))
|
||||
.setDescription(
|
||||
_(
|
||||
'A light that illuminates objects from every direction with a gradient.'
|
||||
'A light that illuminates objects from every direction with a gradient. Often used along with a Directional light.'
|
||||
)
|
||||
)
|
||||
.markAsNotWorkingForObjects()
|
||||
|
@@ -473,7 +473,14 @@ namespace gdjs {
|
||||
this._parentOldMaxY = instanceContainer.getUnrotatedViewportMaxY();
|
||||
}
|
||||
|
||||
doStepPostEvents(instanceContainer: gdjs.RuntimeInstanceContainer) {}
|
||||
doStepPostEvents(instanceContainer: gdjs.RuntimeInstanceContainer) {
|
||||
// Custom objects can be resized during the events step.
|
||||
// The anchor constraints must be applied on child-objects after the parent events.
|
||||
const isChildObject = instanceContainer !== instanceContainer.getScene();
|
||||
if (isChildObject) {
|
||||
this.doStepPreEvents(instanceContainer);
|
||||
}
|
||||
}
|
||||
|
||||
private _convertCoords(
|
||||
instanceContainer: gdjs.RuntimeInstanceContainer,
|
||||
|
@@ -21,7 +21,11 @@ module.exports = {
|
||||
.setExtensionInformation(
|
||||
'Physics2',
|
||||
_('2D Physics Engine'),
|
||||
"The 2D physics engine simulates realistic object physics, with gravity, forces, collisions, joints, etc. It's perfect for 2D games that need to have realistic behaving objects and a gameplay centered around it.",
|
||||
"The 2D physics engine simulates realistic object physics, with gravity, forces, collisions, joints, etc. It's perfect for 2D games that need to have realistic behaving objects and a gameplay centered around it.\n" +
|
||||
'\n' +
|
||||
'Objects like floors or wall objects should usually be set to "Static" as type. Objects that should be moveable are usually "Dynamic" (default). "Kinematic" objects (typically, players or controlled characters) are only moved by their "linear velocity" and "angular velocity" - they can interact with other objects but only these other objects will move.\n' +
|
||||
'\n' +
|
||||
'Forces (and impulses) are expressed in all conditions/expressions/actions of the 2D physics engine in Newtons (N). Typical values for a force are 10-200 N. One meter is 100 pixels by default in the game (check the world scale). Mass is expressed in kilograms (kg).',
|
||||
'Florian Rival, Franco Maciel',
|
||||
'MIT'
|
||||
)
|
||||
@@ -527,6 +531,7 @@ module.exports = {
|
||||
physics2Behavior,
|
||||
sharedData
|
||||
)
|
||||
.markAsIrrelevantForChildObjects()
|
||||
.setIncludeFile('Extensions/Physics2Behavior/physics2runtimebehavior.js')
|
||||
.addIncludeFile('Extensions/Physics2Behavior/Box2D_v2.3.1_min.wasm.js')
|
||||
.addRequiredFile('Extensions/Physics2Behavior/Box2D_v2.3.1_min.wasm.wasm')
|
||||
|
@@ -21,7 +21,11 @@ module.exports = {
|
||||
.setExtensionInformation(
|
||||
'Physics3D',
|
||||
_('3D physics engine'),
|
||||
"The 3D physics engine simulates realistic object physics, with gravity, forces, collisions, joints, etc. It's perfect for almost all 3D games.",
|
||||
"The 3D physics engine simulates realistic object physics, with gravity, forces, collisions, joints, etc. It's perfect for almost all 3D games.\n" +
|
||||
'\n' +
|
||||
'Objects like floors or wall objects should usually be set to "Static" as type. Objects that should be moveable are usually "Dynamic" (default). "Kinematic" objects (typically, players or controlled characters) are only moved by their "linear velocity" and "angular velocity" - they can interact with other objects but only these other objects will move.\n' +
|
||||
'\n' +
|
||||
'Forces (and impulses) are expressed in all conditions/expressions/actions of the 3D physics engine in Newtons (N). Typical values for a force are 10-200 N. One meter is 100 pixels by default in the game (check the world scale). Mass is expressed in kilograms (kg).',
|
||||
'Florian Rival',
|
||||
'MIT'
|
||||
)
|
||||
@@ -675,6 +679,7 @@ module.exports = {
|
||||
behavior,
|
||||
sharedData
|
||||
)
|
||||
.markAsIrrelevantForChildObjects()
|
||||
.addIncludeFile(
|
||||
'Extensions/Physics3DBehavior/Physics3DRuntimeBehavior.js'
|
||||
)
|
||||
@@ -2043,7 +2048,12 @@ module.exports = {
|
||||
'PhysicsCharacter3D',
|
||||
_('3D physics character'),
|
||||
'PhysicsCharacter3D',
|
||||
_('Jump and run on platforms.'),
|
||||
_(
|
||||
'Allow an object to jump and run on platforms that have the 3D physics behavior' +
|
||||
'(and which are generally set to "Static" as type, unless the platform is animated/moved in events).\n' +
|
||||
'\n' +
|
||||
'This behavior is usually used with one or more "mapper" behavior to let the player move it.'
|
||||
),
|
||||
'',
|
||||
'JsPlatform/Extensions/physics_character3d.svg',
|
||||
'PhysicsCharacter3D',
|
||||
@@ -2612,7 +2622,7 @@ module.exports = {
|
||||
'JumpSustainTime',
|
||||
_('Jump sustain time'),
|
||||
_(
|
||||
'the jump sustain time of an object. This is the time during which keeping the jump button held allow the initial jump speed to be maintained.'
|
||||
'the jump sustain time of an object. This is the time during which keeping the jump button held allow the initial jump speed to be maintained'
|
||||
),
|
||||
_('the jump sustain time'),
|
||||
_('Character configuration'),
|
||||
@@ -3300,7 +3310,11 @@ module.exports = {
|
||||
'PhysicsCar3D',
|
||||
_('3D physics car'),
|
||||
'PhysicsCar3D',
|
||||
_('Simulate a realistic car using the 3D physics engine.'),
|
||||
_(
|
||||
"Simulate a realistic car using the 3D physics engine. This is mostly useful for the car controlled by the player (it's usually too complex for other cars in a game).\n" +
|
||||
'\n' +
|
||||
'This behavior is usually used with one or more "mapper" behavior to let the player move it.'
|
||||
),
|
||||
'',
|
||||
'JsPlatform/Extensions/physics_car3d.svg',
|
||||
'PhysicsCar3D',
|
||||
|
@@ -37,7 +37,8 @@ void DeclarePhysicsBehaviorExtension(gd::PlatformExtension& extension) {
|
||||
"res/physics-deprecated32.png",
|
||||
"PhysicsBehavior",
|
||||
std::make_shared<PhysicsBehavior>(),
|
||||
std::make_shared<ScenePhysicsDatas>());
|
||||
std::make_shared<ScenePhysicsDatas>())
|
||||
.MarkAsIrrelevantForChildObjects();
|
||||
|
||||
aut.AddAction("SetStatic",
|
||||
("Make the object static"),
|
||||
|
@@ -18,6 +18,8 @@ KeyboardExtension::KeyboardExtension() {
|
||||
"gdjs.evtTools.input.wasKeyReleased");
|
||||
GetAllConditions()["KeyFromTextPressed"].SetFunctionName(
|
||||
"gdjs.evtTools.input.isKeyPressed");
|
||||
GetAllConditions()["KeyFromTextJustPressed"].SetFunctionName(
|
||||
"gdjs.evtTools.input.wasKeyJustPressed");
|
||||
GetAllConditions()["KeyFromTextReleased"].SetFunctionName(
|
||||
"gdjs.evtTools.input.wasKeyReleased");
|
||||
GetAllConditions()["AnyKeyPressed"].SetFunctionName(
|
||||
|
@@ -5,7 +5,7 @@
|
||||
<name>GDJS_PROJECTNAME</name>
|
||||
<content src="index.html" />
|
||||
<plugin name="cordova-plugin-whitelist" version="1" />
|
||||
<plugin name="cordova-plugin-screen-orientation" version="3.0.2" />
|
||||
<plugin name="cordova-plugin-screen-orientation" version="3.0.4" />
|
||||
<access origin="*" />
|
||||
<allow-intent href="http://*/*" />
|
||||
<allow-intent href="https://*/*" />
|
||||
@@ -67,4 +67,4 @@
|
||||
<!-- Keep cordova-plugin-ionic-webview plugin last as it has a deployment-target to 11, which
|
||||
affects the installation of other plugins.-->
|
||||
<plugin name="cordova-plugin-ionic-webview" version="5.0.1" />
|
||||
</widget>
|
||||
</widget>
|
||||
|
@@ -300,8 +300,13 @@ namespace gdjs {
|
||||
if (profiler) {
|
||||
profiler.end(this.type);
|
||||
}
|
||||
}
|
||||
|
||||
this._instanceContainer._updateObjectsPostEvents();
|
||||
override stepBehaviorsPostEvents(
|
||||
instanceContainer: gdjs.RuntimeInstanceContainer
|
||||
): void {
|
||||
super.stepBehaviorsPostEvents(instanceContainer);
|
||||
this._instanceContainer._stepBehaviorsPostEvents();
|
||||
}
|
||||
|
||||
/**
|
||||
|
@@ -576,9 +576,9 @@ namespace gdjs {
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the objects (update positions, time management...)
|
||||
* Call each behavior stepPostEvents method.
|
||||
*/
|
||||
_updateObjectsPostEvents() {
|
||||
_stepBehaviorsPostEvents() {
|
||||
this._cacheOrClearRemovedInstances();
|
||||
|
||||
// It is *mandatory* to create and iterate on a external list of all objects, as the behaviors
|
||||
|
@@ -153,7 +153,7 @@ namespace gdjs {
|
||||
};
|
||||
|
||||
/**
|
||||
* Return true if the specified key is pressed
|
||||
* Return true if the specified key is pressed (i.e: just pressed or held down).
|
||||
*
|
||||
*/
|
||||
export const isKeyPressed = function (
|
||||
@@ -170,8 +170,22 @@ namespace gdjs {
|
||||
};
|
||||
|
||||
/**
|
||||
* Return true if the specified key was just released
|
||||
*
|
||||
* Return true if the specified key was just pressed (i.e: it started being pressed
|
||||
* during this frame).
|
||||
*/
|
||||
export const wasKeyJustPressed = function (
|
||||
instanceContainer: gdjs.RuntimeInstanceContainer,
|
||||
key: string
|
||||
) {
|
||||
return instanceContainer
|
||||
.getGame()
|
||||
.getInputManager()
|
||||
.wasKeyJustPressed(gdjs.evtTools.input.keysNameToCode[key]);
|
||||
};
|
||||
|
||||
/**
|
||||
* Return true if the specified key was just released (i.e: it stopped being pressed
|
||||
* during this frame).
|
||||
*/
|
||||
export const wasKeyReleased = function (
|
||||
instanceContainer: gdjs.RuntimeInstanceContainer,
|
||||
@@ -187,7 +201,7 @@ namespace gdjs {
|
||||
};
|
||||
|
||||
/**
|
||||
* Return the name of the last key pressed in the game
|
||||
* Return the name of the last key pressed in the game.
|
||||
*/
|
||||
export const lastPressedKey = function (
|
||||
instanceContainer: gdjs.RuntimeInstanceContainer
|
||||
|
@@ -23,12 +23,13 @@ namespace gdjs {
|
||||
* variants and should default to their left variant values
|
||||
* if location is not specified.
|
||||
*/
|
||||
static _DEFAULT_LEFT_VARIANT_KEYS: integer[] = [16, 17, 18, 91];
|
||||
_pressedKeys: Hashtable<boolean>;
|
||||
_releasedKeys: Hashtable<boolean>;
|
||||
_lastPressedKey: float = 0;
|
||||
_pressedMouseButtons: Array<boolean>;
|
||||
_releasedMouseButtons: Array<boolean>;
|
||||
private static _DEFAULT_LEFT_VARIANT_KEYS: integer[] = [16, 17, 18, 91];
|
||||
private _pressedKeys: Hashtable<boolean>;
|
||||
private _justPressedKeys: Hashtable<boolean>;
|
||||
private _releasedKeys: Hashtable<boolean>;
|
||||
private _lastPressedKey: float = 0;
|
||||
private _pressedMouseButtons: Array<boolean>;
|
||||
private _releasedMouseButtons: Array<boolean>;
|
||||
/**
|
||||
* The cursor X position (moved by mouse and touch events).
|
||||
*/
|
||||
@@ -79,6 +80,7 @@ namespace gdjs {
|
||||
|
||||
constructor() {
|
||||
this._pressedKeys = new Hashtable();
|
||||
this._justPressedKeys = new Hashtable();
|
||||
this._releasedKeys = new Hashtable();
|
||||
this._pressedMouseButtons = new Array(5);
|
||||
this._releasedMouseButtons = new Array(5);
|
||||
@@ -124,6 +126,7 @@ namespace gdjs {
|
||||
location
|
||||
);
|
||||
this._pressedKeys.put(locationAwareKeyCode, true);
|
||||
this._justPressedKeys.put(locationAwareKeyCode, true);
|
||||
this._lastPressedKey = locationAwareKeyCode;
|
||||
}
|
||||
|
||||
@@ -140,9 +143,34 @@ namespace gdjs {
|
||||
location
|
||||
);
|
||||
this._pressedKeys.put(locationAwareKeyCode, false);
|
||||
this._justPressedKeys.put(locationAwareKeyCode, false);
|
||||
this._releasedKeys.put(locationAwareKeyCode, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Release all keys that are currently pressed.
|
||||
* Note: if you want to discard pressed keys without considering them as
|
||||
* released, check `clearAllPressedKeys` instead.
|
||||
*/
|
||||
releaseAllPressedKeys(): void {
|
||||
for (const locationAwareKeyCode in this._pressedKeys.items) {
|
||||
this._pressedKeys.put(locationAwareKeyCode, false);
|
||||
this._justPressedKeys.put(locationAwareKeyCode, false);
|
||||
this._releasedKeys.put(locationAwareKeyCode, true);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Clears all stored pressed keys without making the keys go through
|
||||
* the release state.
|
||||
* Note: prefer to use `releaseAllPressedKeys` instead, as it corresponds
|
||||
* to a normal key release.
|
||||
*/
|
||||
clearAllPressedKeys(): void {
|
||||
this._pressedKeys.clear();
|
||||
this._justPressedKeys.clear();
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the location-aware code of the last key that was pressed.
|
||||
* @return The location-aware code of the last key pressed.
|
||||
@@ -152,14 +180,21 @@ namespace gdjs {
|
||||
}
|
||||
|
||||
/**
|
||||
* Return true if the key corresponding to the location-aware keyCode is pressed.
|
||||
* Return true if the key corresponding to the location-aware keyCode is pressed
|
||||
* (either it was just pressed or is still held down).
|
||||
* @param locationAwareKeyCode The location-aware key code to be tested.
|
||||
*/
|
||||
isKeyPressed(locationAwareKeyCode: number): boolean {
|
||||
return (
|
||||
this._pressedKeys.containsKey(locationAwareKeyCode) &&
|
||||
this._pressedKeys.get(locationAwareKeyCode)
|
||||
);
|
||||
return !!this._pressedKeys.get(locationAwareKeyCode);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return true if the key corresponding to the location-aware keyCode
|
||||
* was just pressed during the last frame.
|
||||
* @param locationAwareKeyCode The location-aware key code to be tested.
|
||||
*/
|
||||
wasKeyJustPressed(locationAwareKeyCode: number): boolean {
|
||||
return !!this._justPressedKeys.get(locationAwareKeyCode);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -167,10 +202,7 @@ namespace gdjs {
|
||||
* @param locationAwareKeyCode The location-aware key code to be tested.
|
||||
*/
|
||||
wasKeyReleased(locationAwareKeyCode: number) {
|
||||
return (
|
||||
this._releasedKeys.containsKey(locationAwareKeyCode) &&
|
||||
this._releasedKeys.get(locationAwareKeyCode)
|
||||
);
|
||||
return !!this._releasedKeys.get(locationAwareKeyCode);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -544,6 +576,7 @@ namespace gdjs {
|
||||
this._startedTouches.length = 0;
|
||||
this._endedTouches.length = 0;
|
||||
this._releasedKeys.clear();
|
||||
this._justPressedKeys.clear();
|
||||
this._releasedMouseButtons.length = 0;
|
||||
this._mouseWheelDelta = 0;
|
||||
this._lastStartedTouchIndex = 0;
|
||||
@@ -564,14 +597,6 @@ namespace gdjs {
|
||||
return this.getMouseWheelDelta() < 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Clears all stored pressed keys without making the keys go through
|
||||
* the release state.
|
||||
*/
|
||||
clearAllPressedKeys(): void {
|
||||
this._pressedKeys.clear();
|
||||
}
|
||||
|
||||
static _allTouchIds: Array<integer> = [];
|
||||
}
|
||||
}
|
||||
|
@@ -657,6 +657,14 @@ namespace gdjs {
|
||||
e.preventDefault();
|
||||
}
|
||||
|
||||
if (e.repeat) {
|
||||
// If `repeat` is true, this is not the first press of the key.
|
||||
// We only communicate the changes of states ("first" key down, key up)
|
||||
// to the manager, which then tracks the state of the key:
|
||||
// pressed, just pressed or released.
|
||||
return;
|
||||
}
|
||||
|
||||
manager.onKeyPressed(e.keyCode, e.location);
|
||||
};
|
||||
document.onkeyup = function (e) {
|
||||
|
@@ -405,7 +405,7 @@ namespace gdjs {
|
||||
if (this._profiler) {
|
||||
this._profiler.begin('objects (post-events)');
|
||||
}
|
||||
this._updateObjectsPostEvents();
|
||||
this._stepBehaviorsPostEvents();
|
||||
if (this._profiler) {
|
||||
this._profiler.end('objects (post-events)');
|
||||
}
|
||||
|
@@ -34,9 +34,14 @@ describe('gdjs.InputManager', () => {
|
||||
inputManager.onKeyPressed(33);
|
||||
expect(inputManager.getLastPressedKey()).to.be(33);
|
||||
expect(inputManager.isKeyPressed(32)).to.be(true);
|
||||
expect(inputManager.wasKeyJustPressed(32)).to.be(true);
|
||||
expect(inputManager.isKeyPressed(30)).to.be(false);
|
||||
expect(inputManager.wasKeyJustPressed(30)).to.be(false);
|
||||
expect(inputManager.isKeyPressed(33)).to.be(true);
|
||||
expect(inputManager.wasKeyJustPressed(33)).to.be(true);
|
||||
inputManager.onKeyReleased(32);
|
||||
expect(inputManager.isKeyPressed(32)).to.be(false);
|
||||
expect(inputManager.wasKeyJustPressed(32)).to.be(false);
|
||||
expect(inputManager.wasKeyReleased(32)).to.be(true);
|
||||
expect(inputManager.anyKeyReleased()).to.be(true);
|
||||
expect(inputManager.anyKeyPressed()).to.be(true);
|
||||
@@ -46,10 +51,14 @@ describe('gdjs.InputManager', () => {
|
||||
expect(inputManager.anyKeyPressed()).to.be(true);
|
||||
expect(inputManager.anyKeyReleased()).to.be(false);
|
||||
expect(inputManager.isKeyPressed(33)).to.be(true);
|
||||
expect(inputManager.wasKeyJustPressed(33)).to.be(false);
|
||||
expect(inputManager.wasKeyJustPressed(32)).to.be(false);
|
||||
|
||||
inputManager.onFrameEnded();
|
||||
inputManager.onKeyReleased(33);
|
||||
expect(inputManager.wasKeyReleased(33)).to.be(true);
|
||||
expect(inputManager.wasKeyJustPressed(33)).to.be(false);
|
||||
expect(inputManager.wasKeyJustPressed(32)).to.be(false);
|
||||
expect(inputManager.anyKeyPressed()).to.be(false);
|
||||
expect(inputManager.anyKeyReleased()).to.be(true);
|
||||
inputManager.onFrameEnded();
|
||||
@@ -61,12 +70,16 @@ describe('gdjs.InputManager', () => {
|
||||
expect(inputManager.getLastPressedKey()).to.be(2016);
|
||||
expect(inputManager.isKeyPressed(2016)).to.be(true);
|
||||
expect(inputManager.isKeyPressed(1016)).to.be(false);
|
||||
expect(inputManager.wasKeyJustPressed(2016)).to.be(true);
|
||||
expect(inputManager.wasKeyJustPressed(1016)).to.be(false);
|
||||
|
||||
// Pressed Control with no location - expect to default to left.
|
||||
inputManager.onKeyPressed(17);
|
||||
expect(inputManager.getLastPressedKey()).to.be(1017);
|
||||
expect(inputManager.isKeyPressed(1017)).to.be(true);
|
||||
expect(inputManager.isKeyPressed(2017)).to.be(false);
|
||||
expect(inputManager.wasKeyJustPressed(1017)).to.be(true);
|
||||
expect(inputManager.wasKeyJustPressed(2017)).to.be(false);
|
||||
|
||||
inputManager.onKeyReleased(16, 2);
|
||||
expect(inputManager.wasKeyReleased(2016)).to.be(true);
|
||||
|
@@ -285,6 +285,7 @@ interface Variable {
|
||||
boolean RenameChild([Const] DOMString oldName, [Const] DOMString newName);
|
||||
[Value] VectorString GetAllChildrenNames();
|
||||
void RemoveRecursively([Const, Ref] Variable variableToRemove);
|
||||
void ClearChildren();
|
||||
|
||||
[Ref] Variable GetAtIndex([Const] unsigned long index);
|
||||
[Ref] Variable PushNew();
|
||||
@@ -1143,6 +1144,7 @@ interface LayersContainer {
|
||||
boolean HasLayerNamed([Const] DOMString name);
|
||||
void RemoveLayer([Const] DOMString name);
|
||||
unsigned long GetLayersCount();
|
||||
unsigned long GetLayerPosition([Const] DOMString name);
|
||||
void SwapLayers(unsigned long firstLayerIndex, unsigned long secondLayerIndex);
|
||||
void MoveLayer(unsigned long oldIndex, unsigned long newIndex);
|
||||
void SerializeLayersTo([Ref] SerializerElement element);
|
||||
@@ -1173,6 +1175,7 @@ interface PropertyDescriptor {
|
||||
[Const, Ref] DOMString GetDescription();
|
||||
[Ref] PropertyDescriptor SetGroup([Const] DOMString label);
|
||||
[Const, Ref] DOMString GetGroup();
|
||||
[Ref] PropertyDescriptor ClearChoices();
|
||||
[Ref] PropertyDescriptor AddChoice([Const] DOMString value, [Const] DOMString label);
|
||||
[Const, Ref] VectorPropertyDescriptorChoice GetChoices();
|
||||
[Ref] PropertyDescriptor AddExtraInfo([Const] DOMString type);
|
||||
@@ -2144,6 +2147,9 @@ interface BehaviorMetadata {
|
||||
boolean IsHidden();
|
||||
[Ref] BehaviorMetadata SetHidden();
|
||||
|
||||
boolean IsRelevantForChildObjects();
|
||||
[Ref] BehaviorMetadata MarkAsIrrelevantForChildObjects();
|
||||
|
||||
QuickCustomization_Visibility GetQuickCustomizationVisibility();
|
||||
[Ref] BehaviorMetadata SetQuickCustomizationVisibility(QuickCustomization_Visibility visibility);
|
||||
|
||||
@@ -2932,6 +2938,7 @@ interface MetadataProvider {
|
||||
boolean STATIC_IsBadInstructionMetadata([Const, Ref] InstructionMetadata metadata);
|
||||
boolean STATIC_IsBadBehaviorMetadata([Const, Ref] BehaviorMetadata metadata);
|
||||
boolean STATIC_IsBadObjectMetadata([Const, Ref] ObjectMetadata metadata);
|
||||
boolean STATIC_IsBadEffectMetadata([Const, Ref] EffectMetadata metadata);
|
||||
};
|
||||
|
||||
enum ProjectDiagnostic_ErrorType {
|
||||
|
@@ -606,6 +606,7 @@ typedef std::vector<gd::PropertyDescriptorChoice> VectorPropertyDescriptorChoice
|
||||
#define STATIC_IsBadInstructionMetadata IsBadInstructionMetadata
|
||||
#define STATIC_IsBadBehaviorMetadata IsBadBehaviorMetadata
|
||||
#define STATIC_IsBadObjectMetadata IsBadObjectMetadata
|
||||
#define STATIC_IsBadEffectMetadata IsBadEffectMetadata
|
||||
|
||||
#define STATIC_RenameObjectInEvents RenameObjectInEvents
|
||||
#define STATIC_RemoveObjectInEvents RemoveObjectInEvents
|
||||
|
6
GDevelop.js/types.d.ts
vendored
6
GDevelop.js/types.d.ts
vendored
@@ -323,6 +323,7 @@ export class Variable extends EmscriptenObject {
|
||||
renameChild(oldName: string, newName: string): boolean;
|
||||
getAllChildrenNames(): VectorString;
|
||||
removeRecursively(variableToRemove: Variable): void;
|
||||
clearChildren(): void;
|
||||
getAtIndex(index: number): Variable;
|
||||
pushNew(): Variable;
|
||||
removeAtIndex(index: number): void;
|
||||
@@ -941,6 +942,7 @@ export class LayersContainer extends EmscriptenObject {
|
||||
hasLayerNamed(name: string): boolean;
|
||||
removeLayer(name: string): void;
|
||||
getLayersCount(): number;
|
||||
getLayerPosition(name: string): number;
|
||||
swapLayers(firstLayerIndex: number, secondLayerIndex: number): void;
|
||||
moveLayer(oldIndex: number, newIndex: number): void;
|
||||
serializeLayersTo(element: SerializerElement): void;
|
||||
@@ -970,6 +972,7 @@ export class PropertyDescriptor extends EmscriptenObject {
|
||||
getDescription(): string;
|
||||
setGroup(label: string): PropertyDescriptor;
|
||||
getGroup(): string;
|
||||
clearChoices(): PropertyDescriptor;
|
||||
addChoice(value: string, label: string): PropertyDescriptor;
|
||||
getChoices(): VectorPropertyDescriptorChoice;
|
||||
addExtraInfo(type: string): PropertyDescriptor;
|
||||
@@ -1670,6 +1673,8 @@ export class BehaviorMetadata extends EmscriptenObject {
|
||||
setPrivate(): BehaviorMetadata;
|
||||
isHidden(): boolean;
|
||||
setHidden(): BehaviorMetadata;
|
||||
isRelevantForChildObjects(): boolean;
|
||||
markAsIrrelevantForChildObjects(): BehaviorMetadata;
|
||||
getQuickCustomizationVisibility(): QuickCustomization_Visibility;
|
||||
setQuickCustomizationVisibility(visibility: QuickCustomization_Visibility): BehaviorMetadata;
|
||||
setOpenFullEditorLabel(label: string): BehaviorMetadata;
|
||||
@@ -2109,6 +2114,7 @@ export class MetadataProvider extends EmscriptenObject {
|
||||
static isBadInstructionMetadata(metadata: InstructionMetadata): boolean;
|
||||
static isBadBehaviorMetadata(metadata: BehaviorMetadata): boolean;
|
||||
static isBadObjectMetadata(metadata: ObjectMetadata): boolean;
|
||||
static isBadEffectMetadata(metadata: EffectMetadata): boolean;
|
||||
}
|
||||
|
||||
export class ProjectDiagnostic extends EmscriptenObject {
|
||||
|
@@ -33,6 +33,8 @@ declare class gdBehaviorMetadata {
|
||||
setPrivate(): gdBehaviorMetadata;
|
||||
isHidden(): boolean;
|
||||
setHidden(): gdBehaviorMetadata;
|
||||
isRelevantForChildObjects(): boolean;
|
||||
markAsIrrelevantForChildObjects(): gdBehaviorMetadata;
|
||||
getQuickCustomizationVisibility(): QuickCustomization_Visibility;
|
||||
setQuickCustomizationVisibility(visibility: QuickCustomization_Visibility): gdBehaviorMetadata;
|
||||
setOpenFullEditorLabel(label: string): gdBehaviorMetadata;
|
||||
|
@@ -7,6 +7,7 @@ declare class gdLayersContainer {
|
||||
hasLayerNamed(name: string): boolean;
|
||||
removeLayer(name: string): void;
|
||||
getLayersCount(): number;
|
||||
getLayerPosition(name: string): number;
|
||||
swapLayers(firstLayerIndex: number, secondLayerIndex: number): void;
|
||||
moveLayer(oldIndex: number, newIndex: number): void;
|
||||
serializeLayersTo(element: gdSerializerElement): void;
|
||||
|
@@ -26,6 +26,7 @@ declare class gdMetadataProvider {
|
||||
static isBadInstructionMetadata(metadata: gdInstructionMetadata): boolean;
|
||||
static isBadBehaviorMetadata(metadata: gdBehaviorMetadata): boolean;
|
||||
static isBadObjectMetadata(metadata: gdObjectMetadata): boolean;
|
||||
static isBadEffectMetadata(metadata: gdEffectMetadata): boolean;
|
||||
delete(): void;
|
||||
ptr: number;
|
||||
};
|
@@ -11,6 +11,7 @@ declare class gdPropertyDescriptor {
|
||||
getDescription(): string;
|
||||
setGroup(label: string): gdPropertyDescriptor;
|
||||
getGroup(): string;
|
||||
clearChoices(): gdPropertyDescriptor;
|
||||
addChoice(value: string, label: string): gdPropertyDescriptor;
|
||||
getChoices(): gdVectorPropertyDescriptorChoice;
|
||||
addExtraInfo(type: string): gdPropertyDescriptor;
|
||||
|
@@ -28,6 +28,7 @@ declare class gdVariable {
|
||||
renameChild(oldName: string, newName: string): boolean;
|
||||
getAllChildrenNames(): gdVectorString;
|
||||
removeRecursively(variableToRemove: gdVariable): void;
|
||||
clearChildren(): void;
|
||||
getAtIndex(index: number): gdVariable;
|
||||
pushNew(): gdVariable;
|
||||
removeAtIndex(index: number): void;
|
||||
|
@@ -33,9 +33,6 @@ Images resources, GDJS Runtime, extensions will be copied in resources, and [lib
|
||||
|
||||
You can run the standalone app with Electron. **Make sure that you've launched `npm start` (or `yarn start`) in the `app` folder before** (see above) and **keep it running** (in development, the app is served from a local server, even for the standalone app).
|
||||
|
||||
> Note for Windows: With **Node.js 14 or older**, there is an error related to `git-sh-setup` when running npm install.
|
||||
> To solve this problem: add [this folder to your path environment variable](https://stackoverflow.com/questions/49256190/how-to-fix-git-sh-setup-file-not-found-in-windows) **OR** run `npm install` in newIDE/electron-app/app **before** npm install in newIDE/electron-app.
|
||||
|
||||
```bash
|
||||
cd newIDE/app && npm start # Be sure to have this running in another terminal, before the rest!
|
||||
|
||||
@@ -81,17 +78,11 @@ It's pretty easy to create new themes. Check the [README about themes](./README-
|
||||
|
||||
### Development of the game engine or extensions
|
||||
|
||||
Make sure to have the standalone app running with Electron.
|
||||
- If you want to create/modify _extensions_, check the [README about extensions](./README-extensions.md) for step-by-step explanations to get started in 5 minutes.
|
||||
- The _game engine core_ ([GDJS](https://github.com/4ian/GDevelop/tree/master/GDJS)) is in [GDJS/Runtime folder](https://github.com/4ian/GDevelop/tree/master/GDJS/Runtime).
|
||||
|
||||
- If you want to create/modify _extensions_, check the [README about extensions](./README-extensions.md) for step-by-step explanations to get started in 5 minutes.
|
||||
|
||||
- The _game engine core_ ([GDJS](https://github.com/4ian/GDevelop/tree/master/GDJS)) is in [GDJS/Runtime folder](https://github.com/4ian/GDevelop/tree/master/GDJS/Runtime).
|
||||
|
||||
If you modify any file while the IDE is running with Electron, a watcher will _automatically import_ your changes (look at the console to be sure).
|
||||
|
||||
You can then _launch a preview_ in GDevelop (again, be sure to be using [the standalone app running with Electron](https://github.com/4ian/GDevelop/blob/master/newIDE/README.md#development-of-the-standalone-app) to be sure to have your changes reflected immediately).
|
||||
|
||||
> If you deactivated the watcher in preferences, run the `import-GDJS-Runtime.js` script manually (`cd newIDE/app/scripts` then `node import-GDJS-Runtime.js`) after every change, before launching a preview.
|
||||
If you modify any file while the editor is running, a watcher will _automatically rebuild_ the engine (look at the console to be sure).
|
||||
You can then _launch a preview_ in GDevelop.
|
||||
|
||||
### Recommended tools for development
|
||||
|
||||
|
@@ -33,11 +33,14 @@ export const parameters = {
|
||||
// that we don't use.
|
||||
controls: { hideNoControlsWarning: true },
|
||||
docs: { disable: true },
|
||||
mockAddonConfigs: {
|
||||
globalMockData: [],
|
||||
},
|
||||
};
|
||||
|
||||
export const decorators = [
|
||||
themeDecorator,
|
||||
GDevelopJsInitializerDecorator,
|
||||
i18nProviderDecorator,
|
||||
BrowserDropDownMenuDisablerDecorator
|
||||
]
|
||||
BrowserDropDownMenuDisablerDecorator,
|
||||
];
|
||||
|
590
newIDE/app/package-lock.json
generated
590
newIDE/app/package-lock.json
generated
@@ -81,10 +81,13 @@
|
||||
"@storybook/react": "7.4.6",
|
||||
"@storybook/react-webpack5": "7.4.6",
|
||||
"@storybook/theming": "7.4.6",
|
||||
"@types/node": "22.18.0",
|
||||
"adm-zip": "^0.5.10",
|
||||
"axios-mock-adapter": "^1.22.0",
|
||||
"babel-core": "^7.0.0-bridge.0",
|
||||
"babel-loader": "8.1.0",
|
||||
"chokidar": "4.0.3",
|
||||
"concurrently": "9.2.1",
|
||||
"flow-bin": "0.131.0",
|
||||
"flow-coverage-report": "^0.4.0",
|
||||
"folder-hash": "^3.0.0",
|
||||
@@ -96,6 +99,7 @@
|
||||
"react-scripts": "5.0.1",
|
||||
"recursive-copy": "^2.0.14",
|
||||
"recursive-readdir": "^2.2.2",
|
||||
"serve-handler": "6.1.6",
|
||||
"shelljs": "0.8.4",
|
||||
"storybook": "7.4.6",
|
||||
"storybook-addon-mock": "4.3.0",
|
||||
@@ -7065,6 +7069,12 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@storybook/builder-webpack5/node_modules/@types/node": {
|
||||
"version": "16.18.126",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-16.18.126.tgz",
|
||||
"integrity": "sha512-OTcgaiwfGFBKacvfwuHzzn1KLxH/er8mluiy8/uM3sGXHaRe73RrSIj01jow9t4kJEW633Ov+cOexXeiApTyAw==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/@storybook/builder-webpack5/node_modules/ajv": {
|
||||
"version": "8.12.0",
|
||||
"dev": true,
|
||||
@@ -8053,6 +8063,12 @@
|
||||
"url": "https://opencollective.com/storybook"
|
||||
}
|
||||
},
|
||||
"node_modules/@storybook/core-common/node_modules/@types/node": {
|
||||
"version": "16.18.126",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-16.18.126.tgz",
|
||||
"integrity": "sha512-OTcgaiwfGFBKacvfwuHzzn1KLxH/er8mluiy8/uM3sGXHaRe73RrSIj01jow9t4kJEW633Ov+cOexXeiApTyAw==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/@storybook/core-common/node_modules/brace-expansion": {
|
||||
"version": "2.0.1",
|
||||
"dev": true,
|
||||
@@ -8375,6 +8391,12 @@
|
||||
"url": "https://opencollective.com/storybook"
|
||||
}
|
||||
},
|
||||
"node_modules/@storybook/core-server/node_modules/@types/node": {
|
||||
"version": "16.18.126",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-16.18.126.tgz",
|
||||
"integrity": "sha512-OTcgaiwfGFBKacvfwuHzzn1KLxH/er8mluiy8/uM3sGXHaRe73RrSIj01jow9t4kJEW633Ov+cOexXeiApTyAw==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/@storybook/core-server/node_modules/fs-extra": {
|
||||
"version": "11.1.1",
|
||||
"dev": true,
|
||||
@@ -8546,6 +8568,12 @@
|
||||
"url": "https://opencollective.com/storybook"
|
||||
}
|
||||
},
|
||||
"node_modules/@storybook/core-webpack/node_modules/@types/node": {
|
||||
"version": "16.18.126",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-16.18.126.tgz",
|
||||
"integrity": "sha512-OTcgaiwfGFBKacvfwuHzzn1KLxH/er8mluiy8/uM3sGXHaRe73RrSIj01jow9t4kJEW633Ov+cOexXeiApTyAw==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/@storybook/csf": {
|
||||
"version": "0.1.1",
|
||||
"dev": true,
|
||||
@@ -8843,6 +8871,12 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@storybook/preset-react-webpack/node_modules/@types/node": {
|
||||
"version": "16.18.126",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-16.18.126.tgz",
|
||||
"integrity": "sha512-OTcgaiwfGFBKacvfwuHzzn1KLxH/er8mluiy8/uM3sGXHaRe73RrSIj01jow9t4kJEW633Ov+cOexXeiApTyAw==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/@storybook/preset-react-webpack/node_modules/fs-extra": {
|
||||
"version": "11.1.1",
|
||||
"dev": true,
|
||||
@@ -9067,6 +9101,18 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@storybook/react-webpack5/node_modules/@types/node": {
|
||||
"version": "16.18.126",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-16.18.126.tgz",
|
||||
"integrity": "sha512-OTcgaiwfGFBKacvfwuHzzn1KLxH/er8mluiy8/uM3sGXHaRe73RrSIj01jow9t4kJEW633Ov+cOexXeiApTyAw==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/@storybook/react/node_modules/@types/node": {
|
||||
"version": "16.18.126",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-16.18.126.tgz",
|
||||
"integrity": "sha512-OTcgaiwfGFBKacvfwuHzzn1KLxH/er8mluiy8/uM3sGXHaRe73RrSIj01jow9t4kJEW633Ov+cOexXeiApTyAw==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/@storybook/react/node_modules/acorn": {
|
||||
"version": "7.4.1",
|
||||
"dev": true,
|
||||
@@ -10055,8 +10101,12 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/node": {
|
||||
"version": "16.18.12",
|
||||
"license": "MIT"
|
||||
"version": "22.18.0",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.18.0.tgz",
|
||||
"integrity": "sha512-m5ObIqwsUp6BZzyiy4RdZpzWGub9bqLJMvZDD0QMXhxjqMHMENlj+SqF5QxoUwaQNFe+8kz8XM8ZQhqkQPTgMQ==",
|
||||
"dependencies": {
|
||||
"undici-types": "~6.21.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/node-fetch": {
|
||||
"version": "2.6.6",
|
||||
@@ -12199,11 +12249,15 @@
|
||||
}
|
||||
},
|
||||
"node_modules/binary-extensions": {
|
||||
"version": "2.1.0",
|
||||
"version": "2.3.0",
|
||||
"resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz",
|
||||
"integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/bl": {
|
||||
@@ -12712,29 +12766,18 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/chokidar": {
|
||||
"version": "3.5.3",
|
||||
"version": "4.0.3",
|
||||
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz",
|
||||
"integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
"type": "individual",
|
||||
"url": "https://paulmillr.com/funding/"
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"anymatch": "~3.1.2",
|
||||
"braces": "~3.0.2",
|
||||
"glob-parent": "~5.1.2",
|
||||
"is-binary-path": "~2.1.0",
|
||||
"is-glob": "~4.0.1",
|
||||
"normalize-path": "~3.0.0",
|
||||
"readdirp": "~3.6.0"
|
||||
"readdirp": "^4.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 8.10.0"
|
||||
"node": ">= 14.16.0"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"fsevents": "~2.3.2"
|
||||
"funding": {
|
||||
"url": "https://paulmillr.com/funding/"
|
||||
}
|
||||
},
|
||||
"node_modules/chownr": {
|
||||
@@ -13153,6 +13196,171 @@
|
||||
"typedarray": "^0.0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/concurrently": {
|
||||
"version": "9.2.1",
|
||||
"resolved": "https://registry.npmjs.org/concurrently/-/concurrently-9.2.1.tgz",
|
||||
"integrity": "sha512-fsfrO0MxV64Znoy8/l1vVIjjHa29SZyyqPgQBwhiDcaW8wJc2W3XWVOGx4M3oJBnv/zdUZIIp1gDeS98GzP8Ng==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"chalk": "4.1.2",
|
||||
"rxjs": "7.8.2",
|
||||
"shell-quote": "1.8.3",
|
||||
"supports-color": "8.1.1",
|
||||
"tree-kill": "1.2.2",
|
||||
"yargs": "17.7.2"
|
||||
},
|
||||
"bin": {
|
||||
"conc": "dist/bin/concurrently.js",
|
||||
"concurrently": "dist/bin/concurrently.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/open-cli-tools/concurrently?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/concurrently/node_modules/ansi-regex": {
|
||||
"version": "5.0.1",
|
||||
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
|
||||
"integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/concurrently/node_modules/cliui": {
|
||||
"version": "8.0.1",
|
||||
"resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz",
|
||||
"integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"string-width": "^4.2.0",
|
||||
"strip-ansi": "^6.0.1",
|
||||
"wrap-ansi": "^7.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/concurrently/node_modules/is-fullwidth-code-point": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
|
||||
"integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/concurrently/node_modules/rxjs": {
|
||||
"version": "7.8.2",
|
||||
"resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.2.tgz",
|
||||
"integrity": "sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"tslib": "^2.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/concurrently/node_modules/string-width": {
|
||||
"version": "4.2.3",
|
||||
"resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
|
||||
"integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"emoji-regex": "^8.0.0",
|
||||
"is-fullwidth-code-point": "^3.0.0",
|
||||
"strip-ansi": "^6.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/concurrently/node_modules/strip-ansi": {
|
||||
"version": "6.0.1",
|
||||
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
|
||||
"integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"ansi-regex": "^5.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/concurrently/node_modules/supports-color": {
|
||||
"version": "8.1.1",
|
||||
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz",
|
||||
"integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"has-flag": "^4.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/chalk/supports-color?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/concurrently/node_modules/tslib": {
|
||||
"version": "2.8.1",
|
||||
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
|
||||
"integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/concurrently/node_modules/wrap-ansi": {
|
||||
"version": "7.0.0",
|
||||
"resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz",
|
||||
"integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"ansi-styles": "^4.0.0",
|
||||
"string-width": "^4.1.0",
|
||||
"strip-ansi": "^6.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/chalk/wrap-ansi?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/concurrently/node_modules/y18n": {
|
||||
"version": "5.0.8",
|
||||
"resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz",
|
||||
"integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/concurrently/node_modules/yargs": {
|
||||
"version": "17.7.2",
|
||||
"resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz",
|
||||
"integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"cliui": "^8.0.1",
|
||||
"escalade": "^3.1.1",
|
||||
"get-caller-file": "^2.0.5",
|
||||
"require-directory": "^2.1.1",
|
||||
"string-width": "^4.2.3",
|
||||
"y18n": "^5.0.5",
|
||||
"yargs-parser": "^21.1.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/concurrently/node_modules/yargs-parser": {
|
||||
"version": "21.1.1",
|
||||
"resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz",
|
||||
"integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/confusing-browser-globals": {
|
||||
"version": "1.0.11",
|
||||
"dev": true,
|
||||
@@ -16779,6 +16987,30 @@
|
||||
"webpack": "^5.11.0"
|
||||
}
|
||||
},
|
||||
"node_modules/fork-ts-checker-webpack-plugin/node_modules/chokidar": {
|
||||
"version": "3.6.0",
|
||||
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz",
|
||||
"integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"anymatch": "~3.1.2",
|
||||
"braces": "~3.0.2",
|
||||
"glob-parent": "~5.1.2",
|
||||
"is-binary-path": "~2.1.0",
|
||||
"is-glob": "~4.0.1",
|
||||
"normalize-path": "~3.0.0",
|
||||
"readdirp": "~3.6.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 8.10.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://paulmillr.com/funding/"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"fsevents": "~2.3.2"
|
||||
}
|
||||
},
|
||||
"node_modules/fork-ts-checker-webpack-plugin/node_modules/cosmiconfig": {
|
||||
"version": "7.1.0",
|
||||
"dev": true,
|
||||
@@ -16850,6 +17082,18 @@
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/fork-ts-checker-webpack-plugin/node_modules/readdirp": {
|
||||
"version": "3.6.0",
|
||||
"resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz",
|
||||
"integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"picomatch": "^2.2.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/fork-ts-checker-webpack-plugin/node_modules/resolve-from": {
|
||||
"version": "4.0.0",
|
||||
"dev": true,
|
||||
@@ -18364,8 +18608,9 @@
|
||||
},
|
||||
"node_modules/is-binary-path": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz",
|
||||
"integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"binary-extensions": "^2.0.0"
|
||||
},
|
||||
@@ -22613,14 +22858,6 @@
|
||||
"shell-quote": "^1.7.3"
|
||||
}
|
||||
},
|
||||
"node_modules/launch-editor/node_modules/shell-quote": {
|
||||
"version": "1.8.1",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/lazy-universal-dotenv": {
|
||||
"version": "4.0.0",
|
||||
"dev": true,
|
||||
@@ -24920,6 +25157,12 @@
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/path-is-inside": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/path-is-inside/-/path-is-inside-1.0.2.tgz",
|
||||
"integrity": "sha512-DUWJr3+ULp4zXmol/SZkFf3JGsS9/SIv+Y3Rt93/UjPpDpklB5f1er4O3POIbUuUJ3FXgqte2Q7SrU6zAqwk8w==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/path-key": {
|
||||
"version": "2.0.1",
|
||||
"dev": true,
|
||||
@@ -26800,6 +27043,30 @@
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/react-scripts/node_modules/chokidar": {
|
||||
"version": "3.6.0",
|
||||
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz",
|
||||
"integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"anymatch": "~3.1.2",
|
||||
"braces": "~3.0.2",
|
||||
"glob-parent": "~5.1.2",
|
||||
"is-binary-path": "~2.1.0",
|
||||
"is-glob": "~4.0.1",
|
||||
"normalize-path": "~3.0.0",
|
||||
"readdirp": "~3.6.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 8.10.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://paulmillr.com/funding/"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"fsevents": "~2.3.2"
|
||||
}
|
||||
},
|
||||
"node_modules/react-scripts/node_modules/cosmiconfig": {
|
||||
"version": "7.1.0",
|
||||
"dev": true,
|
||||
@@ -28518,6 +28785,18 @@
|
||||
"node": ">= 12.13.0"
|
||||
}
|
||||
},
|
||||
"node_modules/react-scripts/node_modules/readdirp": {
|
||||
"version": "3.6.0",
|
||||
"resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz",
|
||||
"integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"picomatch": "^2.2.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/react-scripts/node_modules/resolve-from": {
|
||||
"version": "4.0.0",
|
||||
"dev": true,
|
||||
@@ -28621,14 +28900,6 @@
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/react-scripts/node_modules/shell-quote": {
|
||||
"version": "1.8.1",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/react-scripts/node_modules/source-map": {
|
||||
"version": "0.6.1",
|
||||
"dev": true,
|
||||
@@ -29111,14 +29382,16 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/readdirp": {
|
||||
"version": "3.6.0",
|
||||
"version": "4.1.2",
|
||||
"resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz",
|
||||
"integrity": "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"picomatch": "^2.2.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8.10.0"
|
||||
"node": ">= 14.18.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "individual",
|
||||
"url": "https://paulmillr.com/funding/"
|
||||
}
|
||||
},
|
||||
"node_modules/recast": {
|
||||
@@ -30089,6 +30362,87 @@
|
||||
"randombytes": "^2.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/serve-handler": {
|
||||
"version": "6.1.6",
|
||||
"resolved": "https://registry.npmjs.org/serve-handler/-/serve-handler-6.1.6.tgz",
|
||||
"integrity": "sha512-x5RL9Y2p5+Sh3D38Fh9i/iQ5ZK+e4xuXRd/pGbM4D13tgo/MGwbttUk8emytcr1YYzBYs+apnUngBDFYfpjPuQ==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"bytes": "3.0.0",
|
||||
"content-disposition": "0.5.2",
|
||||
"mime-types": "2.1.18",
|
||||
"minimatch": "3.1.2",
|
||||
"path-is-inside": "1.0.2",
|
||||
"path-to-regexp": "3.3.0",
|
||||
"range-parser": "1.2.0"
|
||||
}
|
||||
},
|
||||
"node_modules/serve-handler/node_modules/bytes": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/bytes/-/bytes-3.0.0.tgz",
|
||||
"integrity": "sha512-pMhOfFDPiv9t5jjIXkHosWmkSyQbvsgEVNkz0ERHbuLh2T/7j4Mqqpz523Fe8MVY89KC6Sh/QfS2sM+SjgFDcw==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/serve-handler/node_modules/content-disposition": {
|
||||
"version": "0.5.2",
|
||||
"resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.2.tgz",
|
||||
"integrity": "sha512-kRGRZw3bLlFISDBgwTSA1TMBFN6J6GWDeubmDE3AF+3+yXL8hTWv8r5rkLbqYXY4RjPk/EzHnClI3zQf1cFmHA==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/serve-handler/node_modules/mime-db": {
|
||||
"version": "1.33.0",
|
||||
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.33.0.tgz",
|
||||
"integrity": "sha512-BHJ/EKruNIqJf/QahvxwQZXKygOQ256myeN/Ew+THcAa5q+PjyTTMMeNQC4DZw5AwfvelsUrA6B67NKMqXDbzQ==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/serve-handler/node_modules/mime-types": {
|
||||
"version": "2.1.18",
|
||||
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.18.tgz",
|
||||
"integrity": "sha512-lc/aahn+t4/SWV/qcmumYjymLsWfN3ELhpmVuUFjgsORruuZPVSwAQryq+HHGvO/SI2KVX26bx+En+zhM8g8hQ==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"mime-db": "~1.33.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/serve-handler/node_modules/minimatch": {
|
||||
"version": "3.1.2",
|
||||
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
|
||||
"integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"brace-expansion": "^1.1.7"
|
||||
},
|
||||
"engines": {
|
||||
"node": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/serve-handler/node_modules/path-to-regexp": {
|
||||
"version": "3.3.0",
|
||||
"resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-3.3.0.tgz",
|
||||
"integrity": "sha512-qyCH421YQPS2WFDxDjftfc1ZR5WKQzVzqsp4n9M2kQhVOo/ByahFoUNJfl58kOcEGfQ//7weFTDhm+ss8Ecxgw==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/serve-handler/node_modules/range-parser": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.0.tgz",
|
||||
"integrity": "sha512-kA5WQoNVo4t9lNx2kQNFCxKeBl5IbbSNBl1M/tLkw9WCn+hxNBAW5Qh8gdhs63CJnhjJ2zQWFoqPJP2sK1AV5A==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/serve-index": {
|
||||
"version": "1.9.1",
|
||||
"dev": true,
|
||||
@@ -30198,6 +30552,18 @@
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/shell-quote": {
|
||||
"version": "1.8.3",
|
||||
"resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.8.3.tgz",
|
||||
"integrity": "sha512-ObmnIF4hXNg1BqhnHmgbDETF8dLPCggZWBjkQfhZpbszZnYur5DUljTcCHii5LC3J5E0yeO/1LIMyH+UvHQgyw==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/shelljs": {
|
||||
"version": "0.8.4",
|
||||
"dev": true,
|
||||
@@ -31330,6 +31696,42 @@
|
||||
"node": ">= 8"
|
||||
}
|
||||
},
|
||||
"node_modules/tailwindcss/node_modules/chokidar": {
|
||||
"version": "3.6.0",
|
||||
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz",
|
||||
"integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"anymatch": "~3.1.2",
|
||||
"braces": "~3.0.2",
|
||||
"glob-parent": "~5.1.2",
|
||||
"is-binary-path": "~2.1.0",
|
||||
"is-glob": "~4.0.1",
|
||||
"normalize-path": "~3.0.0",
|
||||
"readdirp": "~3.6.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 8.10.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://paulmillr.com/funding/"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"fsevents": "~2.3.2"
|
||||
}
|
||||
},
|
||||
"node_modules/tailwindcss/node_modules/chokidar/node_modules/glob-parent": {
|
||||
"version": "5.1.2",
|
||||
"resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz",
|
||||
"integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"is-glob": "^4.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 6"
|
||||
}
|
||||
},
|
||||
"node_modules/tailwindcss/node_modules/fast-glob": {
|
||||
"version": "3.2.12",
|
||||
"dev": true,
|
||||
@@ -31474,6 +31876,18 @@
|
||||
"postcss": "^8.2.14"
|
||||
}
|
||||
},
|
||||
"node_modules/tailwindcss/node_modules/readdirp": {
|
||||
"version": "3.6.0",
|
||||
"resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz",
|
||||
"integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"picomatch": "^2.2.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/tailwindcss/node_modules/yaml": {
|
||||
"version": "2.3.1",
|
||||
"dev": true,
|
||||
@@ -31911,6 +32325,15 @@
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/tree-kill": {
|
||||
"version": "1.2.2",
|
||||
"resolved": "https://registry.npmjs.org/tree-kill/-/tree-kill-1.2.2.tgz",
|
||||
"integrity": "sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==",
|
||||
"dev": true,
|
||||
"bin": {
|
||||
"tree-kill": "cli.js"
|
||||
}
|
||||
},
|
||||
"node_modules/trim-lines": {
|
||||
"version": "3.0.1",
|
||||
"license": "MIT",
|
||||
@@ -32119,6 +32542,11 @@
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/undici-types": {
|
||||
"version": "6.21.0",
|
||||
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz",
|
||||
"integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="
|
||||
},
|
||||
"node_modules/unicode-canonical-property-names-ecmascript": {
|
||||
"version": "2.0.0",
|
||||
"dev": true,
|
||||
@@ -32299,6 +32727,42 @@
|
||||
"webpack-virtual-modules": "^0.5.0"
|
||||
}
|
||||
},
|
||||
"node_modules/unplugin/node_modules/chokidar": {
|
||||
"version": "3.6.0",
|
||||
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz",
|
||||
"integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"anymatch": "~3.1.2",
|
||||
"braces": "~3.0.2",
|
||||
"glob-parent": "~5.1.2",
|
||||
"is-binary-path": "~2.1.0",
|
||||
"is-glob": "~4.0.1",
|
||||
"normalize-path": "~3.0.0",
|
||||
"readdirp": "~3.6.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 8.10.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://paulmillr.com/funding/"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"fsevents": "~2.3.2"
|
||||
}
|
||||
},
|
||||
"node_modules/unplugin/node_modules/readdirp": {
|
||||
"version": "3.6.0",
|
||||
"resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz",
|
||||
"integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"picomatch": "^2.2.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/unplugin/node_modules/webpack-sources": {
|
||||
"version": "3.2.3",
|
||||
"dev": true,
|
||||
@@ -32856,6 +33320,30 @@
|
||||
"ajv": "^8.8.2"
|
||||
}
|
||||
},
|
||||
"node_modules/webpack-dev-server/node_modules/chokidar": {
|
||||
"version": "3.6.0",
|
||||
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz",
|
||||
"integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"anymatch": "~3.1.2",
|
||||
"braces": "~3.0.2",
|
||||
"glob-parent": "~5.1.2",
|
||||
"is-binary-path": "~2.1.0",
|
||||
"is-glob": "~4.0.1",
|
||||
"normalize-path": "~3.0.0",
|
||||
"readdirp": "~3.6.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 8.10.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://paulmillr.com/funding/"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"fsevents": "~2.3.2"
|
||||
}
|
||||
},
|
||||
"node_modules/webpack-dev-server/node_modules/ipaddr.js": {
|
||||
"version": "2.1.0",
|
||||
"dev": true,
|
||||
@@ -32885,6 +33373,18 @@
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/webpack-dev-server/node_modules/readdirp": {
|
||||
"version": "3.6.0",
|
||||
"resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz",
|
||||
"integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"picomatch": "^2.2.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/webpack-dev-server/node_modules/rimraf": {
|
||||
"version": "3.0.2",
|
||||
"dev": true,
|
||||
|
@@ -17,10 +17,13 @@
|
||||
"@storybook/react": "7.4.6",
|
||||
"@storybook/react-webpack5": "7.4.6",
|
||||
"@storybook/theming": "7.4.6",
|
||||
"@types/node": "22.18.0",
|
||||
"adm-zip": "^0.5.10",
|
||||
"axios-mock-adapter": "^1.22.0",
|
||||
"babel-core": "^7.0.0-bridge.0",
|
||||
"babel-loader": "8.1.0",
|
||||
"chokidar": "4.0.3",
|
||||
"concurrently": "9.2.1",
|
||||
"flow-bin": "0.131.0",
|
||||
"flow-coverage-report": "^0.4.0",
|
||||
"folder-hash": "^3.0.0",
|
||||
@@ -32,6 +35,7 @@
|
||||
"react-scripts": "5.0.1",
|
||||
"recursive-copy": "^2.0.14",
|
||||
"recursive-readdir": "^2.2.2",
|
||||
"serve-handler": "6.1.6",
|
||||
"shelljs": "0.8.4",
|
||||
"storybook": "7.4.6",
|
||||
"storybook-addon-mock": "4.3.0",
|
||||
@@ -116,7 +120,7 @@
|
||||
"import-resources": "npm run import-zipped-external-editors && npm run build-theme-resources && cd scripts && node import-libGD.js && node import-GDJS-Runtime.js && node import-monaco-editor.js && node import-zipped-external-libs.js",
|
||||
"make-version-metadata": "cd scripts && node make-version-metadata.js",
|
||||
"make-service-worker": "cd scripts && node make-service-worker.js",
|
||||
"start": "npm run import-resources && npm run make-version-metadata && react-app-rewired start",
|
||||
"start": "npm run import-resources && npm run make-version-metadata && concurrently \"react-app-rewired start\" \"node scripts/watch-serve-GDJS-runtime.js\" -n \"editor,game engine\"",
|
||||
"electron-app": "cd ../electron-app && npm run start",
|
||||
"build": "npm run import-resources && npm run make-version-metadata && react-app-rewired build && npm run make-service-worker",
|
||||
"format": "prettier --write \"src/!(locales)/**/*.js\"",
|
||||
|
@@ -632,6 +632,56 @@ const generateExtensionRawText = (
|
||||
...expressionsReferenceTexts,
|
||||
];
|
||||
}),
|
||||
{ text: '' },
|
||||
...extension
|
||||
.getExtensionEffectTypes()
|
||||
.toJSArray()
|
||||
.map(
|
||||
/**
|
||||
* @param {string} effectType
|
||||
* @returns {RawText}
|
||||
*/
|
||||
effectType => {
|
||||
const effectMetadata = extension.getEffectMetadata(effectType);
|
||||
const properties = effectMetadata.getProperties();
|
||||
const propertyNames = properties.keys().toJSArray();
|
||||
|
||||
return {
|
||||
text: [
|
||||
`### Effect "${effectMetadata.getFullName()}"`,
|
||||
'',
|
||||
`${effectMetadata.getDescription().replace(/\n/g, ' ')}`,
|
||||
'',
|
||||
...[
|
||||
effectMetadata.isMarkedAsUnique()
|
||||
? 'This effect can be added only once on a layer.'
|
||||
: null,
|
||||
effectMetadata.isMarkedAsOnlyWorkingFor2D()
|
||||
? effectMetadata.isMarkedAsNotWorkingForObjects()
|
||||
? 'This effect is for 2D layers only.'
|
||||
: 'This effect is for 2D layers or objects only.'
|
||||
: null,
|
||||
effectMetadata.isMarkedAsOnlyWorkingFor3D()
|
||||
? 'This effect is for 3D layers only.'
|
||||
: null,
|
||||
].filter(Boolean),
|
||||
'',
|
||||
`Properties of this effect are:`,
|
||||
'',
|
||||
...propertyNames.map(propertyName => {
|
||||
const propertyMetadata = properties.get(propertyName);
|
||||
return [
|
||||
propertyMetadata.getDescription()
|
||||
? `- **${propertyMetadata.getLabel()}**: ${propertyMetadata.getDescription()}.`
|
||||
: `- **${propertyMetadata.getLabel()}**.`,
|
||||
`Default value is \`${propertyMetadata.getValue()}\`. For events, write: \`"${propertyName}"\`.`,
|
||||
].join(' ');
|
||||
}),
|
||||
'',
|
||||
].join(`\n`),
|
||||
};
|
||||
}
|
||||
),
|
||||
generateExtensionFooterText({ extension }),
|
||||
].filter(Boolean);
|
||||
};
|
||||
|
239
newIDE/app/scripts/watch-serve-GDJS-runtime.js
Normal file
239
newIDE/app/scripts/watch-serve-GDJS-runtime.js
Normal file
@@ -0,0 +1,239 @@
|
||||
#!/usr/bin/env node
|
||||
// @ts-check
|
||||
|
||||
/*
|
||||
* This script watches GDJS Runtime and Extensions files for changes
|
||||
* and automatically runs import-GDJS-Runtime.js when changes are detected.
|
||||
*
|
||||
* Usage: node watch-GDJS-runtime.js
|
||||
* Stop with Ctrl+C
|
||||
*/
|
||||
|
||||
const chokidar = require('chokidar');
|
||||
const debounce = require('lodash.debounce');
|
||||
const child_process = require('child_process');
|
||||
const path = require('path');
|
||||
const handler = require('serve-handler');
|
||||
const http = require('http');
|
||||
|
||||
const PORT = 5002;
|
||||
|
||||
const gdevelopRootPath = path.join(__dirname, '..', '..', '..');
|
||||
const gdjsRootPath = path.join(__dirname, '..', 'resources', 'GDJS');
|
||||
|
||||
// Serve the built files for GDJS.
|
||||
const server = http.createServer((request, response) => {
|
||||
return handler(request, response, {
|
||||
public: gdjsRootPath,
|
||||
cleanUrls: false,
|
||||
headers: [
|
||||
{
|
||||
source: '**/*',
|
||||
headers: [
|
||||
// Tell the browser not to cache the files:
|
||||
{
|
||||
key: 'Cache-Control',
|
||||
value: 'no-cache',
|
||||
},
|
||||
// Allow CORS because the web-app is served from a different origin:
|
||||
{
|
||||
key: 'Access-Control-Allow-Origin',
|
||||
value: '*',
|
||||
},
|
||||
{
|
||||
key: 'Access-Control-Allow-Headers',
|
||||
value: '*',
|
||||
},
|
||||
{
|
||||
key: 'Access-Control-Allow-Credentials',
|
||||
value: 'true',
|
||||
},
|
||||
{
|
||||
key: 'Access-Control-Allow-Private-Network',
|
||||
value: 'true',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* Launch the import-GDJS-Runtime script.
|
||||
* Cleaning the GDJS output folder and copying sources are both
|
||||
* skipped to speed up the build.
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
const importGDJSRuntime = () => {
|
||||
return new Promise((resolve, reject) => {
|
||||
const startTime = performance.now();
|
||||
const scriptPath = path.join(__dirname, 'import-GDJS-Runtime.js');
|
||||
|
||||
console.log('🔄 Running import-GDJS-Runtime.js...');
|
||||
|
||||
child_process.exec(
|
||||
`node "${scriptPath}" --skip-clean --skip-sources`,
|
||||
(error, stdout, stderr) => {
|
||||
if (error) {
|
||||
console.error(`❌ GDJS Runtime update error: ${error.message}`);
|
||||
reject(error);
|
||||
return;
|
||||
}
|
||||
|
||||
if (stdout) {
|
||||
console.log(stdout);
|
||||
}
|
||||
|
||||
if (stderr) {
|
||||
console.error(`⚠️ GDJS Runtime update warnings: ${stderr}`);
|
||||
}
|
||||
|
||||
const duration = (performance.now() - startTime).toFixed(0);
|
||||
console.log(`✅ GDJS Runtime updated in ${duration}ms.`);
|
||||
|
||||
resolve();
|
||||
}
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Callback for file changes, debounced to avoid running too frequently
|
||||
*/
|
||||
const onWatchEvent = debounce(
|
||||
(event, filePath) => {
|
||||
const eventName = event || 'unknown-event';
|
||||
const resolvedFilename =
|
||||
path.relative(gdevelopRootPath, filePath) || 'unknown-file';
|
||||
|
||||
console.log(
|
||||
`📁 Detected "${eventName}" in ${resolvedFilename}, updating GDJS Runtime...`
|
||||
);
|
||||
|
||||
importGDJSRuntime().catch(error => {
|
||||
console.error('❌ Failed to update GDJS Runtime:', error.message);
|
||||
});
|
||||
},
|
||||
100 // Avoid running the script too much in case multiple changes are fired at the same time
|
||||
);
|
||||
|
||||
/**
|
||||
* Set up file watchers for GDJS and Extensions sources
|
||||
*/
|
||||
const setupWatcher = () => {
|
||||
const watchPaths = [
|
||||
path.join(gdevelopRootPath, 'GDJS', 'Runtime'),
|
||||
path.join(gdevelopRootPath, 'Extensions'),
|
||||
];
|
||||
|
||||
const watcher = chokidar.watch(watchPaths, {
|
||||
ignored: (filePath, stats) => {
|
||||
if (!stats) return false;
|
||||
if (!stats.isFile()) return false;
|
||||
|
||||
const ignoredExtensions = [
|
||||
'.d.ts',
|
||||
'.map',
|
||||
'.md',
|
||||
'.png',
|
||||
'.svg',
|
||||
'.svgz',
|
||||
'.txt',
|
||||
'.DS_Store',
|
||||
'.prettierrc',
|
||||
];
|
||||
const ignoredFolderNames = ['tests', 'example', 'diagrams'];
|
||||
|
||||
if (ignoredExtensions.some(ext => filePath.endsWith(ext))) return true;
|
||||
if (
|
||||
ignoredFolderNames.some(
|
||||
folder =>
|
||||
filePath.includes(folder + '/') || filePath.includes(folder + '\\')
|
||||
)
|
||||
)
|
||||
return true;
|
||||
|
||||
// Uncomment to log all watched files:
|
||||
// console.log('Watched file path: ' + filePath);
|
||||
return false;
|
||||
},
|
||||
persistent: true,
|
||||
awaitWriteFinish: {
|
||||
stabilityThreshold: 100,
|
||||
pollInterval: 50,
|
||||
},
|
||||
ignoreInitial: true,
|
||||
});
|
||||
|
||||
// Set up event handlers:
|
||||
const startTime = performance.now();
|
||||
watcher
|
||||
.on('all', onWatchEvent)
|
||||
.on('error', error => {
|
||||
console.error(
|
||||
'❌ GDJS watcher error. Please try to relaunch it. Full error is:',
|
||||
error
|
||||
);
|
||||
})
|
||||
.on('ready', () => {
|
||||
const duration = (performance.now() - startTime).toFixed(2);
|
||||
console.log(`✅ GDJS watcher ready in ${duration}ms.`);
|
||||
console.log(
|
||||
`ℹ️ Watching GDJS engine files for changes in:\n${watchPaths
|
||||
.map(path => `- ${path}`)
|
||||
.join('\n')}.`
|
||||
);
|
||||
});
|
||||
return watcher;
|
||||
};
|
||||
|
||||
const watcher = setupWatcher();
|
||||
|
||||
// Handle graceful shutdown:
|
||||
const shutdown = () => {
|
||||
console.log('\nℹ️ Shutting GDJS Runtime file watcher and server...');
|
||||
|
||||
server.close(() => {
|
||||
console.log('✅ GDJS Runtime server closed.');
|
||||
});
|
||||
|
||||
watcher
|
||||
.close()
|
||||
.then(() => {
|
||||
console.log('✅ GDJS Runtime watcher closed.');
|
||||
process.exit(0);
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('❌ Error closing GDJS Runtime watcher:', error);
|
||||
process.exit(1);
|
||||
});
|
||||
};
|
||||
|
||||
// Handle Ctrl+C and other termination signals.
|
||||
process.on('SIGINT', shutdown);
|
||||
process.on('SIGTERM', shutdown);
|
||||
process.on('SIGHUP', shutdown);
|
||||
|
||||
// Start the server (used to serve the game engine to the web-app).
|
||||
server.on('error', error => {
|
||||
// @ts-ignore
|
||||
if (error.code === 'EADDRINUSE') {
|
||||
console.error(
|
||||
`❌ Error: Port ${PORT} is already in use and can't be used to serve the game engine to the web-app.`
|
||||
);
|
||||
}
|
||||
// @ts-ignore
|
||||
else if (error.code === 'EACCES') {
|
||||
console.error(
|
||||
`❌ Error: Permission denied to bind to port ${PORT} and can't be used to serve the game engine to the web-app.`
|
||||
);
|
||||
} else {
|
||||
console.error('❌ Game engine server error:', error);
|
||||
}
|
||||
process.exit(1);
|
||||
});
|
||||
server.listen(PORT, () => {
|
||||
console.log(
|
||||
`ℹ️ GDJS Runtime served for the web-app at http://localhost:${PORT}.`
|
||||
);
|
||||
});
|
@@ -49,10 +49,11 @@ export const getAiConfigurationPresetsWithAvailability = ({
|
||||
};
|
||||
|
||||
export const getDefaultAiConfigurationPresetId = (
|
||||
mode: 'chat' | 'agent',
|
||||
aiConfigurationPresetsWithAvailability: Array<AiConfigurationPresetWithAvailability>
|
||||
): string => {
|
||||
const defaultPresetWithAvailability = aiConfigurationPresetsWithAvailability.find(
|
||||
preset => preset.isDefault
|
||||
preset => preset.isDefault && preset.mode === mode
|
||||
);
|
||||
|
||||
return (
|
||||
|
@@ -92,6 +92,12 @@ export const ChatMessages = React.memo<Props>(function ChatMessages({
|
||||
const feedbackKey = `${messageIndex}-${messageContentIndex}`;
|
||||
const currentFeedback = messageFeedbacks[feedbackKey];
|
||||
|
||||
const trimmedText = messageContent.text.trim();
|
||||
if (!trimmedText) {
|
||||
// Sometimes the AI can return an empty string or a string with just a line break.
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Line key={key} justifyContent="flex-start">
|
||||
<ChatBubble
|
||||
@@ -159,7 +165,7 @@ export const ChatMessages = React.memo<Props>(function ChatMessages({
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<ChatMarkdownText source={messageContent.text} />
|
||||
<ChatMarkdownText source={trimmedText} />
|
||||
</ChatBubble>
|
||||
</Line>
|
||||
);
|
||||
|
@@ -18,7 +18,9 @@ import GDevelopThemeContext from '../../UI/Theme/GDevelopThemeContext';
|
||||
import classes from './FunctionCallRow.module.css';
|
||||
import {
|
||||
editorFunctions,
|
||||
editorFunctionsWithoutProject,
|
||||
type EditorFunction,
|
||||
type EditorFunctionWithoutProject,
|
||||
type EditorCallbacks,
|
||||
} from '../../EditorFunctions';
|
||||
import Link from '../../UI/Link';
|
||||
@@ -84,8 +86,10 @@ export const FunctionCallRow = React.memo<Props>(function FunctionCallRow({
|
||||
!!editorFunctionCallResult &&
|
||||
editorFunctionCallResult.status === 'working';
|
||||
|
||||
const editorFunction: EditorFunction | null =
|
||||
editorFunctions[functionCall.name] || null;
|
||||
const editorFunction: EditorFunction | EditorFunctionWithoutProject | null =
|
||||
editorFunctions[functionCall.name] ||
|
||||
editorFunctionsWithoutProject[functionCall.name] ||
|
||||
null;
|
||||
let text;
|
||||
let details;
|
||||
let hasDetailsToShow = false;
|
||||
|
@@ -466,7 +466,10 @@ export const AiRequestChat = React.forwardRef<Props, AiRequestChatInterface>(
|
||||
|
||||
const chosenOrDefaultAiConfigurationPresetId =
|
||||
aiConfigurationPresetId ||
|
||||
getDefaultAiConfigurationPresetId(aiConfigurationPresetsWithAvailability);
|
||||
getDefaultAiConfigurationPresetId(
|
||||
newAiRequestMode,
|
||||
aiConfigurationPresetsWithAvailability
|
||||
);
|
||||
|
||||
if (!aiRequest) {
|
||||
return (
|
||||
|
@@ -5,6 +5,7 @@ import { I18n } from '@lingui/react';
|
||||
import {
|
||||
type RenderEditorContainerPropsWithRef,
|
||||
type SceneEventsOutsideEditorChanges,
|
||||
type InstancesOutsideEditorChanges,
|
||||
} from '../MainFrame/EditorContainers/BaseEditor';
|
||||
import { type ObjectWithContext } from '../ObjectsList/EnumerateObjects';
|
||||
import Paper from '../UI/Paper';
|
||||
@@ -49,11 +50,15 @@ import {
|
||||
sendAiRequestMessageSent,
|
||||
sendAiRequestStarted,
|
||||
} from '../Utils/Analytics/EventSender';
|
||||
import { useCreateAiProjectDialog } from './UseCreateAiProjectDialog';
|
||||
import { type ExampleShortHeader } from '../Utils/GDevelopServices/Example';
|
||||
import {
|
||||
type ExampleShortHeader,
|
||||
listAllExamples,
|
||||
} from '../Utils/GDevelopServices/Example';
|
||||
import UrlStorageProvider from '../ProjectsStorage/UrlStorageProvider';
|
||||
import { prepareAiUserContent } from './PrepareAiUserContent';
|
||||
import { AiRequestContext } from './AiRequestContext';
|
||||
import { getAiConfigurationPresetsWithAvailability } from './AiConfiguration';
|
||||
import { type CreateProjectResult } from '../Utils/UseCreateProject';
|
||||
|
||||
const gd: libGDevelop = global.gd;
|
||||
|
||||
@@ -67,6 +72,7 @@ const useProcessFunctionCalls = ({
|
||||
getEditorFunctionCallResults,
|
||||
addEditorFunctionCallResults,
|
||||
onSceneEventsModifiedOutsideEditor,
|
||||
onInstancesModifiedOutsideEditor,
|
||||
onExtensionInstalled,
|
||||
}: {|
|
||||
i18n: I18nType,
|
||||
@@ -85,6 +91,9 @@ const useProcessFunctionCalls = ({
|
||||
onSceneEventsModifiedOutsideEditor: (
|
||||
changes: SceneEventsOutsideEditorChanges
|
||||
) => void,
|
||||
onInstancesModifiedOutsideEditor: (
|
||||
changes: InstancesOutsideEditorChanges
|
||||
) => void,
|
||||
onExtensionInstalled: (extensionNames: Array<string>) => void,
|
||||
|}) => {
|
||||
const { ensureExtensionInstalled } = useEnsureExtensionInstalled({
|
||||
@@ -133,7 +142,7 @@ const useProcessFunctionCalls = ({
|
||||
ignore?: boolean,
|
||||
|}
|
||||
) => {
|
||||
if (!project || !selectedAiRequest) return;
|
||||
if (!selectedAiRequest) return;
|
||||
|
||||
addEditorFunctionCallResults(
|
||||
selectedAiRequest.id,
|
||||
@@ -159,6 +168,7 @@ const useProcessFunctionCalls = ({
|
||||
});
|
||||
},
|
||||
onSceneEventsModifiedOutsideEditor,
|
||||
onInstancesModifiedOutsideEditor,
|
||||
ensureExtensionInstalled,
|
||||
searchAndInstallAsset,
|
||||
});
|
||||
@@ -179,6 +189,7 @@ const useProcessFunctionCalls = ({
|
||||
searchAndInstallAsset,
|
||||
generateEvents,
|
||||
onSceneEventsModifiedOutsideEditor,
|
||||
onInstancesModifiedOutsideEditor,
|
||||
triggerSendEditorFunctionCallResults,
|
||||
editorCallbacks,
|
||||
]
|
||||
@@ -327,13 +338,15 @@ type Props = {|
|
||||
storageProvider: ?StorageProvider,
|
||||
setToolbar: (?React.Node) => void,
|
||||
i18n: I18nType,
|
||||
onCreateEmptyProject: (newProjectSetup: NewProjectSetup) => Promise<void>,
|
||||
onCreateProjectFromExample: (
|
||||
exampleShortHeader: ExampleShortHeader,
|
||||
newProjectSetup: NewProjectSetup,
|
||||
i18n: I18nType,
|
||||
isQuickCustomization?: boolean
|
||||
) => Promise<void>,
|
||||
) => Promise<CreateProjectResult>,
|
||||
onCreateEmptyProject: (
|
||||
newProjectSetup: NewProjectSetup
|
||||
) => Promise<CreateProjectResult>,
|
||||
onOpenLayout: (
|
||||
sceneName: string,
|
||||
options: {|
|
||||
@@ -349,6 +362,9 @@ type Props = {|
|
||||
onSceneEventsModifiedOutsideEditor: (
|
||||
changes: SceneEventsOutsideEditorChanges
|
||||
) => void,
|
||||
onInstancesModifiedOutsideEditor: (
|
||||
changes: InstancesOutsideEditorChanges
|
||||
) => void,
|
||||
onExtensionInstalled: (extensionNames: Array<string>) => void,
|
||||
initialMode: 'chat' | 'agent' | null,
|
||||
initialAiRequestId: string | null,
|
||||
@@ -372,6 +388,9 @@ export type AskAiEditorInterface = {|
|
||||
onSceneEventsModifiedOutsideEditor: (
|
||||
changes: SceneEventsOutsideEditorChanges
|
||||
) => void,
|
||||
onInstancesModifiedOutsideEditor: (
|
||||
changes: InstancesOutsideEditorChanges
|
||||
) => void,
|
||||
startOrOpenChat: ({|
|
||||
mode: 'chat' | 'agent',
|
||||
aiRequestId: string | null,
|
||||
@@ -397,10 +416,11 @@ export const AskAiEditor = React.memo<Props>(
|
||||
fileMetadata,
|
||||
storageProvider,
|
||||
i18n,
|
||||
onCreateEmptyProject,
|
||||
onCreateProjectFromExample,
|
||||
onCreateEmptyProject,
|
||||
onOpenLayout,
|
||||
onSceneEventsModifiedOutsideEditor,
|
||||
onInstancesModifiedOutsideEditor,
|
||||
onExtensionInstalled,
|
||||
initialMode,
|
||||
initialAiRequestId,
|
||||
@@ -408,11 +428,57 @@ export const AskAiEditor = React.memo<Props>(
|
||||
}: Props,
|
||||
ref
|
||||
) => {
|
||||
const onCreateProject = React.useCallback(
|
||||
async ({
|
||||
name,
|
||||
exampleSlug,
|
||||
}: {|
|
||||
name: string,
|
||||
exampleSlug: string | null,
|
||||
|}) => {
|
||||
const newProjectSetup: NewProjectSetup = {
|
||||
projectName: name,
|
||||
storageProvider: UrlStorageProvider,
|
||||
saveAsLocation: null,
|
||||
dontOpenAnySceneOrProjectManager: true,
|
||||
};
|
||||
|
||||
if (exampleSlug) {
|
||||
const { exampleShortHeaders } = await listAllExamples();
|
||||
const exampleShortHeader = exampleShortHeaders.find(
|
||||
header => header.slug === exampleSlug
|
||||
);
|
||||
if (exampleShortHeader) {
|
||||
const { createdProject } = await onCreateProjectFromExample(
|
||||
exampleShortHeader,
|
||||
newProjectSetup,
|
||||
i18n,
|
||||
false
|
||||
);
|
||||
return { exampleSlug, createdProject };
|
||||
}
|
||||
|
||||
// The example was not found - still create an empty project.
|
||||
}
|
||||
|
||||
const { createdProject } = await onCreateEmptyProject({
|
||||
projectName: name,
|
||||
storageProvider: UrlStorageProvider,
|
||||
saveAsLocation: null,
|
||||
dontOpenAnySceneOrProjectManager: true,
|
||||
});
|
||||
|
||||
return { exampleSlug: null, createdProject };
|
||||
},
|
||||
[onCreateProjectFromExample, onCreateEmptyProject, i18n]
|
||||
);
|
||||
|
||||
const editorCallbacks: EditorCallbacks = React.useMemo(
|
||||
() => ({
|
||||
onOpenLayout,
|
||||
onCreateProject,
|
||||
}),
|
||||
[onOpenLayout]
|
||||
[onOpenLayout, onCreateProject]
|
||||
);
|
||||
|
||||
const {
|
||||
@@ -488,11 +554,6 @@ export const AskAiEditor = React.memo<Props>(
|
||||
setLastSendError,
|
||||
} = aiRequestStorage;
|
||||
|
||||
const {
|
||||
createAiProject,
|
||||
renderCreateAiProjectDialog,
|
||||
} = useCreateAiProjectDialog();
|
||||
|
||||
const updateToolbar = React.useCallback(
|
||||
() => {
|
||||
if (setToolbar) {
|
||||
@@ -518,6 +579,7 @@ export const AskAiEditor = React.memo<Props>(
|
||||
onSceneObjectEdited: noop,
|
||||
onSceneObjectsDeleted: noop,
|
||||
onSceneEventsModifiedOutsideEditor: noop,
|
||||
onInstancesModifiedOutsideEditor: noop,
|
||||
startOrOpenChat: onStartOrOpenChat,
|
||||
}));
|
||||
|
||||
@@ -582,29 +644,6 @@ export const AskAiEditor = React.memo<Props>(
|
||||
} = newAiRequestOptions;
|
||||
startNewAiRequest(null);
|
||||
|
||||
// If no project is opened, create a new empty one if the request is for
|
||||
// the AI agent.
|
||||
if (mode === 'agent' && !project) {
|
||||
try {
|
||||
console.info(
|
||||
'No project opened, opening the dialog to create a new project.'
|
||||
);
|
||||
const result = await createAiProject();
|
||||
if (result === 'canceled') {
|
||||
return;
|
||||
}
|
||||
console.info('New project created - starting AI request.');
|
||||
startNewAiRequest({
|
||||
mode,
|
||||
userRequest,
|
||||
aiConfigurationPresetId,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error creating a new empty project:', error);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Ensure the user has enough credits to pay for the request, or ask them
|
||||
// to buy some more.
|
||||
let payWithCredits = false;
|
||||
@@ -655,6 +694,7 @@ export const AskAiEditor = React.memo<Props>(
|
||||
fileMetadata,
|
||||
storageProviderName,
|
||||
mode,
|
||||
toolsVersion: 'v3',
|
||||
aiConfiguration: {
|
||||
presetId: aiConfigurationPresetId,
|
||||
},
|
||||
@@ -718,7 +758,6 @@ export const AskAiEditor = React.memo<Props>(
|
||||
setSendingAiRequest,
|
||||
upToDateSelectedAiRequestId,
|
||||
updateAiRequest,
|
||||
createAiProject,
|
||||
newAiRequestOptions,
|
||||
onOpenAskAi,
|
||||
]
|
||||
@@ -858,7 +897,11 @@ export const AskAiEditor = React.memo<Props>(
|
||||
}
|
||||
}
|
||||
|
||||
if (selectedAiRequest && createdSceneNames) {
|
||||
if (
|
||||
selectedAiRequest &&
|
||||
createdSceneNames &&
|
||||
createdSceneNames.length > 0
|
||||
) {
|
||||
onOpenAskAi({
|
||||
mode: selectedAiRequest.mode || 'agent',
|
||||
aiRequestId: selectedAiRequestId,
|
||||
@@ -944,6 +987,7 @@ export const AskAiEditor = React.memo<Props>(
|
||||
getEditorFunctionCallResults,
|
||||
addEditorFunctionCallResults,
|
||||
onSceneEventsModifiedOutsideEditor,
|
||||
onInstancesModifiedOutsideEditor,
|
||||
i18n,
|
||||
onExtensionInstalled,
|
||||
});
|
||||
@@ -1000,10 +1044,6 @@ export const AskAiEditor = React.memo<Props>(
|
||||
/>
|
||||
</div>
|
||||
</Paper>
|
||||
{renderCreateAiProjectDialog({
|
||||
onCreateEmptyProject,
|
||||
onCreateProjectFromExample,
|
||||
})}
|
||||
<AskAiHistory
|
||||
open={isHistoryOpen}
|
||||
onClose={onCloseHistory}
|
||||
@@ -1038,12 +1078,15 @@ export const renderAskAiEditorContainer = (
|
||||
storageProvider={props.storageProvider}
|
||||
setToolbar={props.setToolbar}
|
||||
isActive={props.isActive}
|
||||
onCreateEmptyProject={props.onCreateEmptyProject}
|
||||
onCreateProjectFromExample={props.onCreateProjectFromExample}
|
||||
onCreateEmptyProject={props.onCreateEmptyProject}
|
||||
onOpenLayout={props.onOpenLayout}
|
||||
onSceneEventsModifiedOutsideEditor={
|
||||
props.onSceneEventsModifiedOutsideEditor
|
||||
}
|
||||
onInstancesModifiedOutsideEditor={
|
||||
props.onInstancesModifiedOutsideEditor
|
||||
}
|
||||
onExtensionInstalled={props.onExtensionInstalled}
|
||||
initialMode={
|
||||
(props.extraEditorProps && props.extraEditorProps.mode) || null
|
||||
|
@@ -1,146 +0,0 @@
|
||||
// @flow
|
||||
|
||||
import React from 'react';
|
||||
import { I18n } from '@lingui/react';
|
||||
import { type I18n as I18nType } from '@lingui/core';
|
||||
import { Trans } from '@lingui/macro';
|
||||
import EmptyAndStartingPointProjects from '../ProjectCreation/EmptyAndStartingPointProjects';
|
||||
import { ColumnStackLayout } from '../UI/Layout';
|
||||
import FlatButton from '../UI/FlatButton';
|
||||
import Dialog from '../UI/Dialog';
|
||||
import { type ExampleShortHeader } from '../Utils/GDevelopServices/Example';
|
||||
import UrlStorageProvider from '../ProjectsStorage/UrlStorageProvider';
|
||||
import { generateProjectName } from '../ProjectCreation/NewProjectSetupDialog';
|
||||
import { type NewProjectSetup } from '../ProjectCreation/NewProjectSetupDialog';
|
||||
import { Spacer } from '../UI/Grid';
|
||||
|
||||
type RenderCreateAiProjectDialogProps = {
|
||||
onCreateEmptyProject: (newProjectSetup: NewProjectSetup) => Promise<void>,
|
||||
onCreateProjectFromExample: (
|
||||
exampleShortHeader: ExampleShortHeader,
|
||||
newProjectSetup: NewProjectSetup,
|
||||
i18n: I18nType,
|
||||
isQuickCustomization?: boolean
|
||||
) => Promise<void>,
|
||||
};
|
||||
|
||||
type CreateAiProjectDialogProps = {
|
||||
onClose: () => void,
|
||||
onSelectExampleShortHeader: (
|
||||
exampleShortHeader: ExampleShortHeader
|
||||
) => Promise<void>,
|
||||
onSelectEmptyProject: () => Promise<void>,
|
||||
};
|
||||
|
||||
const CreateAiProjectDialog = ({
|
||||
onClose,
|
||||
onSelectExampleShortHeader,
|
||||
onSelectEmptyProject,
|
||||
}: CreateAiProjectDialogProps) => {
|
||||
return (
|
||||
<Dialog
|
||||
open
|
||||
title={<Trans>Ok! Choose a game style to start with</Trans>}
|
||||
id="ai-project-dialog"
|
||||
maxWidth="md"
|
||||
actions={[
|
||||
<FlatButton
|
||||
key="cancel"
|
||||
label={<Trans>Cancel</Trans>}
|
||||
onClick={onClose}
|
||||
/>,
|
||||
]}
|
||||
onRequestClose={onClose}
|
||||
flexColumnBody
|
||||
>
|
||||
<ColumnStackLayout noMargin>
|
||||
<Spacer />
|
||||
<EmptyAndStartingPointProjects
|
||||
onSelectExampleShortHeader={exampleShortHeader => {
|
||||
onSelectExampleShortHeader(exampleShortHeader);
|
||||
}}
|
||||
onSelectEmptyProject={() => {
|
||||
onSelectEmptyProject();
|
||||
}}
|
||||
/>
|
||||
{/* Use a spacer to avoid extra scrollbars when template tiles are hovered. */}
|
||||
<Spacer />
|
||||
</ColumnStackLayout>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
|
||||
type CreateAiProjectResult = 'canceled' | 'created';
|
||||
|
||||
export const useCreateAiProjectDialog = () => {
|
||||
const [createPromise, setCreatePromise] = React.useState<null | {|
|
||||
onFinished: (result: CreateAiProjectResult) => void,
|
||||
promise: Promise<CreateAiProjectResult>,
|
||||
|}>(null);
|
||||
|
||||
const createAiProject: () => Promise<CreateAiProjectResult> = React.useCallback(
|
||||
() => {
|
||||
if (createPromise) {
|
||||
return createPromise.promise;
|
||||
}
|
||||
|
||||
// Make a promise that we can resolve later from the creation dialog.
|
||||
let resolve: (result: CreateAiProjectResult) => void = () => {};
|
||||
const promise = new Promise(resolveFn => {
|
||||
resolve = resolveFn;
|
||||
});
|
||||
|
||||
setCreatePromise({
|
||||
onFinished: (result: CreateAiProjectResult) => {
|
||||
setCreatePromise(null);
|
||||
resolve(result);
|
||||
},
|
||||
promise,
|
||||
});
|
||||
return promise;
|
||||
},
|
||||
[createPromise]
|
||||
);
|
||||
|
||||
return {
|
||||
createAiProject,
|
||||
renderCreateAiProjectDialog: (props: RenderCreateAiProjectDialogProps) => {
|
||||
if (!createPromise) return null;
|
||||
|
||||
return (
|
||||
<I18n>
|
||||
{({ i18n }) => (
|
||||
<CreateAiProjectDialog
|
||||
onClose={() => {
|
||||
createPromise.onFinished('canceled');
|
||||
}}
|
||||
onSelectExampleShortHeader={async exampleShortHeader => {
|
||||
const newProjectSetup: NewProjectSetup = {
|
||||
storageProvider: UrlStorageProvider,
|
||||
saveAsLocation: null,
|
||||
dontOpenAnySceneOrProjectManager: true,
|
||||
};
|
||||
await props.onCreateProjectFromExample(
|
||||
exampleShortHeader,
|
||||
newProjectSetup,
|
||||
i18n,
|
||||
false // isQuickCustomization
|
||||
);
|
||||
createPromise.onFinished('created');
|
||||
}}
|
||||
onSelectEmptyProject={async () => {
|
||||
await props.onCreateEmptyProject({
|
||||
projectName: generateProjectName('AI starter'),
|
||||
storageProvider: UrlStorageProvider,
|
||||
saveAsLocation: null,
|
||||
dontOpenAnySceneOrProjectManager: true,
|
||||
});
|
||||
createPromise.onFinished('created');
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</I18n>
|
||||
);
|
||||
},
|
||||
};
|
||||
};
|
@@ -51,6 +51,7 @@ export const useSearchAndInstallAsset = ({
|
||||
status: 'nothing-found',
|
||||
message: 'No assets found.',
|
||||
createdObjects: [],
|
||||
assetShortHeader: null,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -58,9 +59,10 @@ export const useSearchAndInstallAsset = ({
|
||||
// For now, we just return the first asset.
|
||||
const chosenResult = assetSearch.results[0];
|
||||
if (!chosenResult) throw new Error('No asset found.');
|
||||
const assetShortHeader = chosenResult.asset;
|
||||
|
||||
const installOutput = await installAsset({
|
||||
assetShortHeader: chosenResult.asset,
|
||||
assetShortHeader,
|
||||
objectsContainer: scene.getObjects(),
|
||||
requestedObjectName: objectName,
|
||||
});
|
||||
@@ -70,6 +72,7 @@ export const useSearchAndInstallAsset = ({
|
||||
status: 'error',
|
||||
message: 'Asset found but failed to install asset.',
|
||||
createdObjects: [],
|
||||
assetShortHeader: null,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -77,6 +80,7 @@ export const useSearchAndInstallAsset = ({
|
||||
status: 'asset-installed',
|
||||
message: 'Asset installed successfully.',
|
||||
createdObjects: installOutput.createdObjects,
|
||||
assetShortHeader,
|
||||
};
|
||||
},
|
||||
[installAsset, profile, getAuthorizationHeader]
|
||||
|
@@ -65,7 +65,7 @@ export const AnnouncementsFeed = ({
|
||||
|
||||
const classesForClickableContainer = useStylesForClickableContainer();
|
||||
|
||||
if (error) {
|
||||
if (error && !hideLoader) {
|
||||
return (
|
||||
<PlaceholderError onRetry={fetchAnnouncementsAndPromotions}>
|
||||
<Trans>
|
||||
|
@@ -155,6 +155,7 @@ export const getBundleTiles = ({
|
||||
receivedBundles,
|
||||
openedShopCategory,
|
||||
hasAssetFiltersApplied,
|
||||
onlyShowAssets,
|
||||
}: {|
|
||||
allBundleListingDatas: ?Array<BundleListingData>,
|
||||
displayedBundleListingDatas: ?Array<BundleListingData>,
|
||||
@@ -162,12 +163,14 @@ export const getBundleTiles = ({
|
||||
receivedBundles: ?Array<any>,
|
||||
openedShopCategory?: ?string,
|
||||
hasAssetFiltersApplied?: boolean,
|
||||
onlyShowAssets?: boolean,
|
||||
|}): Array<React.Node> => {
|
||||
if (
|
||||
!allBundleListingDatas ||
|
||||
!displayedBundleListingDatas ||
|
||||
!onBundleSelection ||
|
||||
hasAssetFiltersApplied
|
||||
hasAssetFiltersApplied ||
|
||||
onlyShowAssets
|
||||
)
|
||||
return [];
|
||||
|
||||
|
@@ -580,6 +580,7 @@ const AssetsList = React.forwardRef<Props, AssetsListInterface>(
|
||||
onBundleSelection,
|
||||
receivedBundles,
|
||||
hasAssetFiltersApplied,
|
||||
onlyShowAssets,
|
||||
}),
|
||||
[
|
||||
allBundleListingDatas,
|
||||
@@ -587,6 +588,7 @@ const AssetsList = React.forwardRef<Props, AssetsListInterface>(
|
||||
onBundleSelection,
|
||||
receivedBundles,
|
||||
hasAssetFiltersApplied,
|
||||
onlyShowAssets,
|
||||
]
|
||||
);
|
||||
|
||||
|
@@ -33,6 +33,7 @@ type Props = {|
|
||||
id?: string,
|
||||
objectType: string,
|
||||
objectBehaviorsTypes: Array<string>,
|
||||
isChildObject: boolean,
|
||||
behaviorShortHeader: BehaviorShortHeader,
|
||||
matches: ?Array<SearchMatch>,
|
||||
onChoose: () => void,
|
||||
@@ -45,6 +46,7 @@ export const BehaviorListItem = ({
|
||||
id,
|
||||
objectType,
|
||||
objectBehaviorsTypes,
|
||||
isChildObject,
|
||||
behaviorShortHeader,
|
||||
matches,
|
||||
onChoose,
|
||||
@@ -53,20 +55,28 @@ export const BehaviorListItem = ({
|
||||
platform,
|
||||
}: Props) => {
|
||||
const alreadyAdded = objectBehaviorsTypes.includes(behaviorShortHeader.type);
|
||||
// An empty object type means the base object, i.e: any object.
|
||||
|
||||
const behaviorMetadata = gd.MetadataProvider.getBehaviorMetadata(
|
||||
platform,
|
||||
behaviorShortHeader.type
|
||||
);
|
||||
const isObjectCompatible =
|
||||
// An empty object type means the base object, i.e: any object.
|
||||
(!behaviorShortHeader.objectType ||
|
||||
objectType === behaviorShortHeader.objectType) &&
|
||||
(!isChildObject || behaviorMetadata.isRelevantForChildObjects()) &&
|
||||
behaviorShortHeader.allRequiredBehaviorTypes.every(requiredBehaviorType => {
|
||||
const behaviorMetadata = gd.MetadataProvider.getBehaviorMetadata(
|
||||
platform,
|
||||
requiredBehaviorType
|
||||
);
|
||||
return (
|
||||
!behaviorMetadata.isHidden() ||
|
||||
objectBehaviorsTypes.includes(requiredBehaviorType)
|
||||
(!isChildObject || behaviorMetadata.isRelevantForChildObjects()) &&
|
||||
(!behaviorMetadata.isHidden() ||
|
||||
objectBehaviorsTypes.includes(requiredBehaviorType))
|
||||
);
|
||||
});
|
||||
|
||||
const isEngineCompatible = isCompatibleWithGDevelopVersion(
|
||||
getIDEVersion(),
|
||||
behaviorShortHeader.gdevelopVersion
|
||||
|
@@ -62,6 +62,7 @@ type Props = {|
|
||||
project: gdProject,
|
||||
objectType: string,
|
||||
objectBehaviorsTypes: Array<string>,
|
||||
isChildObject: boolean,
|
||||
installedBehaviorMetadataList: Array<BehaviorShortHeader>,
|
||||
deprecatedBehaviorMetadataList: Array<BehaviorShortHeader>,
|
||||
onInstall: (behaviorShortHeader: BehaviorShortHeader) => Promise<boolean>,
|
||||
@@ -76,6 +77,7 @@ export const BehaviorStore = ({
|
||||
project,
|
||||
objectType,
|
||||
objectBehaviorsTypes,
|
||||
isChildObject,
|
||||
installedBehaviorMetadataList,
|
||||
deprecatedBehaviorMetadataList,
|
||||
onInstall,
|
||||
@@ -316,6 +318,7 @@ export const BehaviorStore = ({
|
||||
key={behaviorShortHeader.type}
|
||||
objectType={objectType}
|
||||
objectBehaviorsTypes={objectBehaviorsTypes}
|
||||
isChildObject={isChildObject}
|
||||
onHeightComputed={onHeightComputed}
|
||||
behaviorShortHeader={behaviorShortHeader}
|
||||
matches={getExtensionsMatches(behaviorShortHeader)}
|
||||
|
@@ -1,80 +1,53 @@
|
||||
// @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 { Column, Line, Spacer } from '../../UI/Grid';
|
||||
import BundlePageHeader from './BundlePageHeader';
|
||||
import { BundleStoreContext } from './BundleStoreContext';
|
||||
import PlaceholderLoader from '../../UI/PlaceholderLoader';
|
||||
import {
|
||||
getBundle,
|
||||
type Bundle,
|
||||
type Course,
|
||||
} from '../../Utils/GDevelopServices/Asset';
|
||||
import Text from '../../UI/Text';
|
||||
import {
|
||||
type PrivateAssetPackListingData,
|
||||
type BundleListingData,
|
||||
type PrivateGameTemplateListingData,
|
||||
type CourseListingData,
|
||||
} from '../../Utils/GDevelopServices/Shop';
|
||||
import { type SubscriptionPlanWithPricingSystems } from '../../Utils/GDevelopServices/Usage';
|
||||
import { extractGDevelopApiErrorStatusAndCode } from '../../Utils/GDevelopServices/Errors';
|
||||
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';
|
||||
getProductsIncludedInBundle,
|
||||
getProductsIncludedInBundleTiles,
|
||||
} from '../ProductPageHelper';
|
||||
import { PrivateGameTemplateStoreContext } from '../PrivateGameTemplates/PrivateGameTemplateStoreContext';
|
||||
import { AssetStoreContext } from '../AssetStoreContext';
|
||||
import AuthenticatedUserContext from '../../Profile/AuthenticatedUserContext';
|
||||
import { GridList, GridListTile } from '@material-ui/core';
|
||||
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 Text from '../../UI/Text';
|
||||
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';
|
||||
import { planIdSortingFunction } from '../../Profile/Subscription/PlanCard';
|
||||
import SubscriptionPlanPricingSummary from '../../Profile/Subscription/PromotionSubscriptionDialog/SubscriptionPlanPricingSummary';
|
||||
import { ResponsiveLineStackLayout } from '../../UI/Layout';
|
||||
import SubscriptionPlanTableSummary from '../../Profile/Subscription/PromotionSubscriptionDialog/SubscriptionPlanTableSummary';
|
||||
import GDevelopThemeContext from '../../UI/Theme/GDevelopThemeContext';
|
||||
import { LARGE_WIDGET_SIZE } from '../../MainFrame/EditorContainers/HomePage/CardWidget';
|
||||
import SectionContainer, {
|
||||
SectionRow,
|
||||
} from '../../MainFrame/EditorContainers/HomePage/SectionContainer';
|
||||
import { type CourseCompletion } from '../../MainFrame/EditorContainers/HomePage/UseCourses';
|
||||
import CourseCard from '../../MainFrame/EditorContainers/HomePage/LearnSection/CourseCard';
|
||||
|
||||
const cellSpacing = 10;
|
||||
|
||||
const getTemplateColumns = (
|
||||
windowSize: WindowSizeType,
|
||||
isLandscape: boolean
|
||||
) => {
|
||||
const getColumns = (windowSize: WindowSizeType, isLandscape: boolean) => {
|
||||
switch (windowSize) {
|
||||
case 'small':
|
||||
return isLandscape ? 4 : 2;
|
||||
@@ -88,80 +61,50 @@ const getTemplateColumns = (
|
||||
return 3;
|
||||
}
|
||||
};
|
||||
const MAX_COLUMNS = getTemplateColumns('xlarge', true);
|
||||
|
||||
const cellSpacing = 10;
|
||||
const MAX_COLUMNS = getColumns('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>,
|
||||
onBack?: () => void | Promise<void>,
|
||||
getSubscriptionPlansWithPricingSystems: () => Array<SubscriptionPlanWithPricingSystems> | null,
|
||||
onBundleOpen: BundleListingData => void,
|
||||
onGameTemplateOpen: PrivateGameTemplateListingData => void,
|
||||
onAssetPackOpen: (
|
||||
privateAssetPackListingData: PrivateAssetPackListingData,
|
||||
options?: {|
|
||||
forceProductPage?: boolean,
|
||||
|}
|
||||
privateAssetPackListingData: PrivateAssetPackListingData
|
||||
) => void,
|
||||
onCourseOpen: CourseListingData => void,
|
||||
simulateAppStoreProduct?: boolean,
|
||||
courses: ?Array<Course>,
|
||||
receivedCourses: ?Array<Course>,
|
||||
getCourseCompletion: (courseId: string) => CourseCompletion | null,
|
||||
noPadding?: boolean,
|
||||
|};
|
||||
|
||||
const BundleInformationPage = ({
|
||||
bundleListingData,
|
||||
bundleListingDatasFromSameCreator,
|
||||
receivedCourses,
|
||||
onBundleOpen,
|
||||
onGameTemplateOpen,
|
||||
onBack,
|
||||
getSubscriptionPlansWithPricingSystems,
|
||||
onAssetPackOpen,
|
||||
onGameTemplateOpen,
|
||||
onBundleOpen,
|
||||
onCourseOpen,
|
||||
simulateAppStoreProduct,
|
||||
courses,
|
||||
receivedCourses,
|
||||
getCourseCompletion,
|
||||
noPadding,
|
||||
}: Props) => {
|
||||
const { id, name, sellerId } = bundleListingData;
|
||||
const { bundleListingDatas } = React.useContext(BundleStoreContext);
|
||||
const { windowSize, isLandscape, isMobile } = useResponsiveWindowSize();
|
||||
const { bundleListingDatas } = React.useContext(BundleStoreContext); // If archived, should use the one passed.
|
||||
const { privateGameTemplateListingDatas } = React.useContext(
|
||||
PrivateGameTemplateStoreContext
|
||||
);
|
||||
@@ -169,181 +112,125 @@ const BundleInformationPage = ({
|
||||
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);
|
||||
palette: { type: paletteType },
|
||||
} = React.useContext(GDevelopThemeContext);
|
||||
|
||||
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 || []),
|
||||
],
|
||||
const courseAndTheirListingDataIncludedInBundle = React.useMemo(
|
||||
(): Array<{|
|
||||
course: Course,
|
||||
courseListingData: CourseListingData,
|
||||
|}> | null => {
|
||||
if (!bundle || !bundleListingData || !courses) return null;
|
||||
const productListingDatasInBundle = getProductsIncludedInBundle({
|
||||
productListingData: bundleListingData,
|
||||
productListingDatas: [...(listedCourses || [])],
|
||||
});
|
||||
|
||||
if (!productsIncludedInBundle) return [];
|
||||
if (!productListingDatasInBundle) return null;
|
||||
// $FlowIgnore - Flow doesn't understand that we have filtered the products to only include courses.
|
||||
const courseListingDatasInBundle: CourseListingData[] = productListingDatasInBundle.filter(
|
||||
productListingData => productListingData.productType === 'COURSE'
|
||||
);
|
||||
|
||||
const additionalThumbnails = productsIncludedInBundle
|
||||
.map(product => (product.thumbnailUrls || []).slice(0, 2))
|
||||
.reduce((acc, thumbnails) => acc.concat(thumbnails), []);
|
||||
return additionalThumbnails;
|
||||
return (courseListingDatasInBundle || [])
|
||||
.map(courseListingData => {
|
||||
const course = courses.find(
|
||||
course => course.id === courseListingData.id
|
||||
);
|
||||
if (!course) return null;
|
||||
return {
|
||||
course,
|
||||
courseListingData,
|
||||
};
|
||||
})
|
||||
.filter(Boolean);
|
||||
},
|
||||
[
|
||||
bundleListingDatas,
|
||||
privateGameTemplateListingDatas,
|
||||
privateAssetPackListingDatas,
|
||||
listedCourses,
|
||||
bundleListingData,
|
||||
]
|
||||
[bundle, bundleListingData, listedCourses, courses]
|
||||
);
|
||||
|
||||
const productsIncludedInBundleTiles = React.useMemo(
|
||||
const productsExceptCoursesIncludedInBundleTiles = 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 && bundleListingData
|
||||
? getProductsIncludedInBundleTiles({
|
||||
product: bundle,
|
||||
productListingDatas: [
|
||||
...(bundleListingDatas || []),
|
||||
...(privateGameTemplateListingDatas || []),
|
||||
...(privateAssetPackListingDatas || []),
|
||||
],
|
||||
productListingData: bundleListingData,
|
||||
receivedProducts: [
|
||||
...(receivedBundles || []),
|
||||
...(receivedGameTemplates || []),
|
||||
...(receivedAssetPacks || []),
|
||||
],
|
||||
onPrivateAssetPackOpen: onAssetPackOpen,
|
||||
onPrivateGameTemplateOpen: onGameTemplateOpen,
|
||||
onBundleOpen,
|
||||
onCourseOpen,
|
||||
discountedPrice: true,
|
||||
})
|
||||
: null,
|
||||
[
|
||||
bundle,
|
||||
bundleListingDatas,
|
||||
privateGameTemplateListingDatas,
|
||||
privateAssetPackListingDatas,
|
||||
listedCourses,
|
||||
receivedBundles,
|
||||
receivedGameTemplates,
|
||||
receivedAssetPacks,
|
||||
receivedCourses,
|
||||
bundleListingData,
|
||||
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 subscriptionPlansWithPricingSystems = getSubscriptionPlansWithPricingSystems();
|
||||
|
||||
const otherBundlesFromTheSameAuthorTiles = React.useMemo(
|
||||
() =>
|
||||
getOtherProductsFromSameAuthorTiles({
|
||||
otherProductListingDatasFromSameCreator: bundleListingDatasFromSameCreator,
|
||||
currentProductListingData: bundleListingData,
|
||||
receivedProducts: receivedBundles,
|
||||
onProductOpen: onBundleOpen,
|
||||
}),
|
||||
[
|
||||
bundleListingDatasFromSameCreator,
|
||||
bundleListingData,
|
||||
receivedBundles,
|
||||
onBundleOpen,
|
||||
]
|
||||
const highestSubscriptionPlanIncludedInBundle = React.useMemo(
|
||||
() => {
|
||||
if (!bundleListingData) return null;
|
||||
|
||||
const sortedIncludedRedemptionCodes = (
|
||||
bundleListingData.includedRedemptionCodes || []
|
||||
).sort((a, b) =>
|
||||
planIdSortingFunction(
|
||||
a.givenSubscriptionPlanId,
|
||||
b.givenSubscriptionPlanId
|
||||
)
|
||||
);
|
||||
|
||||
if (!sortedIncludedRedemptionCodes.length) return null;
|
||||
|
||||
const planId =
|
||||
sortedIncludedRedemptionCodes[sortedIncludedRedemptionCodes.length - 1]
|
||||
.givenSubscriptionPlanId;
|
||||
|
||||
return subscriptionPlansWithPricingSystems
|
||||
? subscriptionPlansWithPricingSystems.find(
|
||||
subscriptionPlanWithPricingSystems =>
|
||||
subscriptionPlanWithPricingSystems.id === planId
|
||||
)
|
||||
: null;
|
||||
},
|
||||
[bundleListingData, subscriptionPlansWithPricingSystems]
|
||||
);
|
||||
|
||||
React.useEffect(
|
||||
() => {
|
||||
(async () => {
|
||||
setIsFetching(true);
|
||||
try {
|
||||
const [bundle, profile] = await Promise.all([
|
||||
getBundle(id),
|
||||
getUserPublicProfile(sellerId),
|
||||
]);
|
||||
const bundle = await getBundle(bundleListingData.id);
|
||||
|
||||
setBundle(bundle);
|
||||
setSellerPublicProfile(profile);
|
||||
} catch (error) {
|
||||
const extractedStatusAndCode = extractGDevelopApiErrorStatusAndCode(
|
||||
error
|
||||
@@ -359,328 +246,149 @@ const BundleInformationPage = ({
|
||||
<Trans>An error occurred, please try again later.</Trans>
|
||||
);
|
||||
}
|
||||
} finally {
|
||||
setIsFetching(false);
|
||||
}
|
||||
})();
|
||||
},
|
||||
[id, sellerId]
|
||||
[bundleListingData.id]
|
||||
);
|
||||
|
||||
const onClickBuy = React.useCallback(
|
||||
async () => {
|
||||
if (!bundle || isOwningAnotherVariant) return;
|
||||
if (isAlreadyReceived) {
|
||||
onBundleOpen(bundleListingData);
|
||||
return;
|
||||
}
|
||||
const customSectionPaperStyle = {
|
||||
// $FlowIgnore
|
||||
...(noPadding
|
||||
? {
|
||||
padding: 0,
|
||||
}
|
||||
: {}),
|
||||
...(bundleListingData.visibleUntil && !noPadding
|
||||
? {
|
||||
backgroundAttachment: 'local',
|
||||
backgroundRepeat: 'no-repeat',
|
||||
backgroundPosition: 'top',
|
||||
backgroundSize: isMobile && !isLandscape ? 'contain' : 'auto',
|
||||
backgroundImage: `${
|
||||
paletteType === 'dark'
|
||||
? 'linear-gradient(180deg, #6B1A18 0px, #2D2331 5%, #1d1d2600 10%)'
|
||||
: 'linear-gradient(180deg, #F03F18 0px, #f5f5f700 10%)'
|
||||
}`,
|
||||
}
|
||||
: {}),
|
||||
};
|
||||
|
||||
try {
|
||||
const price = bundleListingData.prices.find(
|
||||
price => price.usageType === 'default'
|
||||
);
|
||||
if (errorText) {
|
||||
return (
|
||||
<SectionContainer
|
||||
flexBody
|
||||
backAction={onBack}
|
||||
customPaperStyle={customSectionPaperStyle}
|
||||
>
|
||||
<SectionRow expand>
|
||||
<Line alignItems="center" justifyContent="center" expand>
|
||||
<AlertMessage kind="error">{errorText}</AlertMessage>
|
||||
</Line>
|
||||
</SectionRow>
|
||||
</SectionContainer>
|
||||
);
|
||||
}
|
||||
|
||||
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]
|
||||
);
|
||||
if (!bundleListingData || !bundle) {
|
||||
return (
|
||||
<SectionContainer flexBody customPaperStyle={customSectionPaperStyle}>
|
||||
<SectionRow expand>
|
||||
<PlaceholderLoader />
|
||||
</SectionRow>
|
||||
</SectionContainer>
|
||||
);
|
||||
}
|
||||
|
||||
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
|
||||
<SectionContainer
|
||||
backAction={onBack}
|
||||
customPaperStyle={customSectionPaperStyle}
|
||||
>
|
||||
<BundlePageHeader
|
||||
bundleListingData={bundleListingData}
|
||||
bundle={bundle}
|
||||
i18n={i18n}
|
||||
/>
|
||||
<Line noMargin>
|
||||
<Text size="section-title">
|
||||
<Trans>What's included:</Trans>
|
||||
</Text>
|
||||
</Line>
|
||||
{courseAndTheirListingDataIncludedInBundle &&
|
||||
courseAndTheirListingDataIncludedInBundle.length > 0 && (
|
||||
<Line>
|
||||
<GridList
|
||||
cols={getColumns(windowSize, isLandscape)}
|
||||
style={styles.grid}
|
||||
cellHeight="auto"
|
||||
spacing={cellSpacing}
|
||||
>
|
||||
<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,
|
||||
{courseAndTheirListingDataIncludedInBundle.map(
|
||||
({ course, courseListingData }) => {
|
||||
const completion = getCourseCompletion(course.id);
|
||||
return (
|
||||
<GridListTile key={course.id}>
|
||||
<CourseCard
|
||||
course={course}
|
||||
courseListingData={courseListingData}
|
||||
completion={completion}
|
||||
onClick={() => {
|
||||
onCourseOpen(courseListingData);
|
||||
}}
|
||||
>
|
||||
<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>
|
||||
</>
|
||||
discountedPrice
|
||||
/>
|
||||
</GridListTile>
|
||||
);
|
||||
}
|
||||
)}
|
||||
</ScrollView>
|
||||
</Column>
|
||||
) : null}
|
||||
{!!purchasingBundleListingData && (
|
||||
<BundlePurchaseDialog
|
||||
bundleListingData={purchasingBundleListingData}
|
||||
usageType="default"
|
||||
onClose={() => setPurchasingBundleListingData(null)}
|
||||
/>
|
||||
</GridList>
|
||||
</Line>
|
||||
)}
|
||||
{productsExceptCoursesIncludedInBundleTiles && (
|
||||
<Line>
|
||||
<GridList
|
||||
cols={getColumns(windowSize, isLandscape)}
|
||||
cellHeight="auto"
|
||||
spacing={cellSpacing}
|
||||
style={styles.grid}
|
||||
>
|
||||
{productsExceptCoursesIncludedInBundleTiles}
|
||||
</GridList>
|
||||
</Line>
|
||||
)}
|
||||
{isRedemptionCodesDialogOpen && (
|
||||
<RedemptionCodesDialog
|
||||
onClose={() => setIsRedemptionCodesDialogOpen(false)}
|
||||
/>
|
||||
{highestSubscriptionPlanIncludedInBundle && (
|
||||
<ResponsiveLineStackLayout expand noColumnMargin>
|
||||
<Column noMargin justifyContent="center">
|
||||
<Line expand>
|
||||
<SubscriptionPlanPricingSummary
|
||||
subscriptionPlanWithPricingSystems={
|
||||
highestSubscriptionPlanIncludedInBundle
|
||||
}
|
||||
disabled={false}
|
||||
onClickChoosePlan={async () => {}}
|
||||
seatsCount={0}
|
||||
setSeatsCount={() => {}}
|
||||
period={'month'}
|
||||
setPeriod={() => {}}
|
||||
onlyShowDiscountedPrice
|
||||
/>
|
||||
</Line>
|
||||
</Column>
|
||||
<Spacer />
|
||||
<Column noMargin>
|
||||
<SubscriptionPlanTableSummary
|
||||
subscriptionPlanWithPricingSystems={
|
||||
highestSubscriptionPlanIncludedInBundle
|
||||
}
|
||||
hideActions
|
||||
/>
|
||||
</Column>
|
||||
</ResponsiveLineStackLayout>
|
||||
)}
|
||||
</>
|
||||
</SectionContainer>
|
||||
)}
|
||||
</I18n>
|
||||
);
|
||||
|
517
newIDE/app/src/AssetStore/Bundles/BundlePageHeader.js
Normal file
517
newIDE/app/src/AssetStore/Bundles/BundlePageHeader.js
Normal file
@@ -0,0 +1,517 @@
|
||||
// @flow
|
||||
|
||||
import * as React from 'react';
|
||||
import { Trans } from '@lingui/macro';
|
||||
import { I18n } from '@lingui/react';
|
||||
import { type I18n as I18nType } from '@lingui/core';
|
||||
import { type Bundle } from '../../Utils/GDevelopServices/Asset';
|
||||
import { type BundleListingData } from '../../Utils/GDevelopServices/Shop';
|
||||
import Paper from '../../UI/Paper';
|
||||
import Text from '../../UI/Text';
|
||||
import { Column, Line } from '../../UI/Grid';
|
||||
import {
|
||||
ColumnStackLayout,
|
||||
LineStackLayout,
|
||||
ResponsiveLineStackLayout,
|
||||
} from '../../UI/Layout';
|
||||
import { useResponsiveWindowSize } from '../../UI/Responsive/ResponsiveWindowMeasurer';
|
||||
import { selectMessageByLocale } from '../../Utils/i18n/MessageByLocale';
|
||||
import { renderProductPrice } from '../../AssetStore/ProductPriceTag';
|
||||
import {
|
||||
getProductsIncludedInBundle,
|
||||
getUserProductPurchaseUsageType,
|
||||
PurchaseProductButtons,
|
||||
} from '../../AssetStore/ProductPageHelper';
|
||||
import { shouldUseAppStoreProduct } from '../../Utils/AppStorePurchases';
|
||||
import AuthenticatedUserContext from '../../Profile/AuthenticatedUserContext';
|
||||
import { BundleStoreContext } from '../../AssetStore/Bundles/BundleStoreContext';
|
||||
import { sendBundleBuyClicked } from '../../Utils/Analytics/EventSender';
|
||||
import BundlePurchaseDialog from '../../AssetStore/Bundles/BundlePurchaseDialog';
|
||||
import RedemptionCodesDialog from '../../RedemptionCode/RedemptionCodesDialog';
|
||||
import {
|
||||
getEstimatedSavingsFormatted,
|
||||
renderEstimatedTotalPriceFormatted,
|
||||
} from '../../AssetStore/Bundles/Utils';
|
||||
import { PrivateGameTemplateStoreContext } from '../../AssetStore/PrivateGameTemplates/PrivateGameTemplateStoreContext';
|
||||
import { CreditsPackageStoreContext } from '../../AssetStore/CreditsPackages/CreditsPackageStoreContext';
|
||||
import { AssetStoreContext } from '../../AssetStore/AssetStoreContext';
|
||||
import CourseStoreContext from '../../Course/CourseStoreContext';
|
||||
import SecureCheckout from '../../AssetStore/SecureCheckout/SecureCheckout';
|
||||
import FlatButton from '../../UI/FlatButton';
|
||||
import Chip from '../../UI/Chip';
|
||||
import ProductLimitedTimeOffer from '../../AssetStore/ProductLimitedTimeOffer';
|
||||
import Skeleton from '@material-ui/lab/Skeleton';
|
||||
import { getSummaryLines } from './Utils';
|
||||
import { SectionRow } from '../../MainFrame/EditorContainers/HomePage/SectionContainer';
|
||||
|
||||
const styles = {
|
||||
title: { overflowWrap: 'anywhere', textWrap: 'wrap' },
|
||||
image: { width: 300, aspectRatio: '16 / 9' },
|
||||
imageContainer: {
|
||||
display: 'flex',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
position: 'relative',
|
||||
borderRadius: 8,
|
||||
overflow: 'hidden',
|
||||
},
|
||||
discountedPrice: { textDecoration: 'line-through', opacity: 0.7 },
|
||||
discountChip: {
|
||||
height: 24,
|
||||
backgroundColor: '#F03F18',
|
||||
color: 'white',
|
||||
},
|
||||
};
|
||||
|
||||
type Props = {|
|
||||
bundleListingData: BundleListingData,
|
||||
bundle: Bundle,
|
||||
simulateAppStoreProduct?: boolean,
|
||||
i18n: I18nType,
|
||||
|};
|
||||
|
||||
const BundlePageHeader = ({
|
||||
bundle,
|
||||
bundleListingData,
|
||||
simulateAppStoreProduct,
|
||||
i18n,
|
||||
}: Props) => {
|
||||
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 { receivedBundles, bundlePurchases } = authenticatedUser;
|
||||
const [
|
||||
purchasingBundleListingData,
|
||||
setPurchasingBundleListingData,
|
||||
] = React.useState<?BundleListingData>(null);
|
||||
const { isMobile, isMediumScreen } = useResponsiveWindowSize();
|
||||
const isMobileOrMediumScreen = isMobile || isMediumScreen;
|
||||
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 productListingDatasIncludedInBundle = React.useMemo(
|
||||
() =>
|
||||
bundleListingData &&
|
||||
bundleListingDatas &&
|
||||
privateGameTemplateListingDatas &&
|
||||
privateAssetPackListingDatas &&
|
||||
listedCourses &&
|
||||
creditsPackageListingDatas
|
||||
? 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 summaryLines = React.useMemo(
|
||||
() => {
|
||||
if (
|
||||
!productListingDatasIncludedInBundle ||
|
||||
!redemptionCodesIncludedInBundle ||
|
||||
!bundleListingData
|
||||
)
|
||||
return isMobile ? (
|
||||
<ColumnStackLayout noMargin>
|
||||
<Column expand noMargin>
|
||||
<Skeleton height={25} />
|
||||
<Skeleton height={20} />
|
||||
<Skeleton height={20} />
|
||||
</Column>
|
||||
<Column expand noMargin>
|
||||
<Skeleton height={25} />
|
||||
<Skeleton height={20} />
|
||||
<Skeleton height={20} />
|
||||
</Column>
|
||||
</ColumnStackLayout>
|
||||
) : (
|
||||
<Column expand noMargin>
|
||||
<Skeleton height={25} />
|
||||
<Skeleton height={20} />
|
||||
<Skeleton height={20} />
|
||||
</Column>
|
||||
);
|
||||
|
||||
if (isAlreadyReceived) {
|
||||
return (
|
||||
<Line noMargin>
|
||||
<FlatButton
|
||||
primary
|
||||
label={<Trans>See my subscription codes</Trans>}
|
||||
onClick={() => setIsRedemptionCodesDialogOpen(true)}
|
||||
/>
|
||||
</Line>
|
||||
);
|
||||
}
|
||||
|
||||
const summaryLines = getSummaryLines({
|
||||
redemptionCodesIncludedInBundle,
|
||||
bundleListingData,
|
||||
productListingDatasIncludedInBundle,
|
||||
});
|
||||
|
||||
if (isMobile) {
|
||||
return summaryLines.mobileLines;
|
||||
}
|
||||
return summaryLines.desktopLines;
|
||||
},
|
||||
[
|
||||
isAlreadyReceived,
|
||||
redemptionCodesIncludedInBundle,
|
||||
bundleListingData,
|
||||
productListingDatasIncludedInBundle,
|
||||
isMobile,
|
||||
]
|
||||
);
|
||||
|
||||
const estimatedTotalPriceFormatted = React.useMemo(
|
||||
() =>
|
||||
renderEstimatedTotalPriceFormatted({
|
||||
i18n,
|
||||
bundleListingData,
|
||||
productListingDatasIncludedInBundle,
|
||||
redemptionCodesIncludedInBundle,
|
||||
}),
|
||||
[
|
||||
i18n,
|
||||
bundleListingData,
|
||||
productListingDatasIncludedInBundle,
|
||||
redemptionCodesIncludedInBundle,
|
||||
]
|
||||
);
|
||||
|
||||
const estimatedSavingsFormatted = React.useMemo(
|
||||
() =>
|
||||
getEstimatedSavingsFormatted({
|
||||
i18n,
|
||||
bundleListingData,
|
||||
productListingDatasIncludedInBundle,
|
||||
redemptionCodesIncludedInBundle,
|
||||
}),
|
||||
[
|
||||
i18n,
|
||||
bundleListingData,
|
||||
productListingDatasIncludedInBundle,
|
||||
redemptionCodesIncludedInBundle,
|
||||
]
|
||||
);
|
||||
|
||||
const productPrice = React.useMemo(
|
||||
() =>
|
||||
renderProductPrice({
|
||||
i18n,
|
||||
productListingData: bundleListingData,
|
||||
usageType: 'default',
|
||||
plainText: true,
|
||||
}),
|
||||
[i18n, bundleListingData]
|
||||
);
|
||||
|
||||
const onClickBuy = React.useCallback(
|
||||
async () => {
|
||||
if (!bundle) return;
|
||||
if (isAlreadyReceived) {
|
||||
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]
|
||||
);
|
||||
|
||||
return (
|
||||
<I18n>
|
||||
{({ i18n }) => (
|
||||
<Column noOverflowParent noMargin>
|
||||
{!isAlreadyReceived && !isMobileOrMediumScreen && (
|
||||
<SectionRow>
|
||||
<Paper background="medium" style={{ padding: 16 }}>
|
||||
{!!bundleListingData && (
|
||||
<LineStackLayout noMargin justifyContent="space-between">
|
||||
{bundleListingData.visibleUntil ? (
|
||||
<ProductLimitedTimeOffer
|
||||
visibleUntil={bundleListingData.visibleUntil}
|
||||
/>
|
||||
) : estimatedSavingsFormatted ? (
|
||||
<Column
|
||||
noMargin
|
||||
alignItems="flex-start"
|
||||
justifyContent="center"
|
||||
>
|
||||
<Chip
|
||||
label={<Trans>Bundle</Trans>}
|
||||
style={styles.discountChip}
|
||||
/>
|
||||
<Text color="secondary">
|
||||
<Trans>
|
||||
Get{' '}
|
||||
{estimatedSavingsFormatted.savingsPriceFormatted}{' '}
|
||||
worth of value for less!
|
||||
</Trans>
|
||||
</Text>
|
||||
</Column>
|
||||
) : (
|
||||
<Skeleton height={24} width={100} />
|
||||
)}
|
||||
{estimatedTotalPriceFormatted &&
|
||||
estimatedSavingsFormatted &&
|
||||
productPrice ? (
|
||||
<LineStackLayout justifyContent="flex-end" noMargin>
|
||||
<Column noMargin alignItems="flex-end">
|
||||
<LineStackLayout>
|
||||
<Text noMargin color="secondary" size="block-title">
|
||||
<span style={styles.discountedPrice}>
|
||||
{estimatedTotalPriceFormatted}
|
||||
</span>
|
||||
</Text>
|
||||
{bundleListingData.visibleUntil && (
|
||||
<Chip
|
||||
label={
|
||||
<Trans>
|
||||
{
|
||||
estimatedSavingsFormatted.savingsPercentageFormatted
|
||||
}{' '}
|
||||
OFF
|
||||
</Trans>
|
||||
}
|
||||
style={styles.discountChip}
|
||||
/>
|
||||
)}
|
||||
</LineStackLayout>
|
||||
<Text noMargin size="block-title">
|
||||
{productPrice}
|
||||
</Text>
|
||||
</Column>
|
||||
<ColumnStackLayout noMargin alignItems="center">
|
||||
<PurchaseProductButtons
|
||||
i18n={i18n}
|
||||
productListingData={bundleListingData}
|
||||
selectedUsageType="default"
|
||||
onUsageTypeChange={() => {}}
|
||||
simulateAppStoreProduct={simulateAppStoreProduct}
|
||||
isAlreadyReceived={isAlreadyReceived}
|
||||
onClickBuy={onClickBuy}
|
||||
onClickBuyWithCredits={() => {}}
|
||||
customLabel={
|
||||
<Trans>
|
||||
Buy now and save{' '}
|
||||
{
|
||||
estimatedSavingsFormatted.savingsPriceFormatted
|
||||
}
|
||||
</Trans>
|
||||
}
|
||||
/>
|
||||
{!shouldUseOrSimulateAppStoreProduct && (
|
||||
<SecureCheckout />
|
||||
)}
|
||||
</ColumnStackLayout>
|
||||
</LineStackLayout>
|
||||
) : (
|
||||
<Column noMargin justifyContent="flex-end">
|
||||
<Skeleton height={24} width={100} />
|
||||
<Skeleton height={24} width={100} />
|
||||
</Column>
|
||||
)}
|
||||
</LineStackLayout>
|
||||
)}
|
||||
</Paper>
|
||||
</SectionRow>
|
||||
)}
|
||||
{!isAlreadyReceived &&
|
||||
isMobileOrMediumScreen &&
|
||||
bundleListingData &&
|
||||
bundleListingData.visibleUntil && (
|
||||
<SectionRow>
|
||||
<ProductLimitedTimeOffer
|
||||
visibleUntil={bundleListingData.visibleUntil}
|
||||
/>
|
||||
</SectionRow>
|
||||
)}
|
||||
<SectionRow>
|
||||
<ResponsiveLineStackLayout
|
||||
noMargin
|
||||
alignItems="center"
|
||||
justifyContent="flex-start"
|
||||
forceMobileLayout={isMediumScreen}
|
||||
expand
|
||||
>
|
||||
<div style={styles.imageContainer}>
|
||||
<img
|
||||
src={bundle.previewImageUrls[0]}
|
||||
style={styles.image}
|
||||
alt=""
|
||||
/>
|
||||
</div>
|
||||
<ColumnStackLayout
|
||||
expand
|
||||
justifyContent="flex-start"
|
||||
noMargin={isMobile}
|
||||
>
|
||||
<Text size="title" noMargin style={styles.title}>
|
||||
{selectMessageByLocale(i18n, bundle.nameByLocale)}
|
||||
</Text>
|
||||
<Line noMargin>
|
||||
<Text noMargin>
|
||||
{selectMessageByLocale(
|
||||
i18n,
|
||||
bundle.longDescriptionByLocale
|
||||
)}
|
||||
</Text>
|
||||
</Line>
|
||||
{summaryLines}
|
||||
</ColumnStackLayout>
|
||||
</ResponsiveLineStackLayout>
|
||||
</SectionRow>
|
||||
{!isAlreadyReceived && isMobileOrMediumScreen && bundleListingData && (
|
||||
<SectionRow>
|
||||
<Paper background="medium" style={{ padding: 16 }}>
|
||||
{estimatedTotalPriceFormatted &&
|
||||
estimatedSavingsFormatted &&
|
||||
productPrice ? (
|
||||
<ResponsiveLineStackLayout justifyContent="flex-end" noMargin>
|
||||
<LineStackLayout
|
||||
alignItems="center"
|
||||
justifyContent="center"
|
||||
>
|
||||
{bundleListingData.visibleUntil && (
|
||||
<Chip
|
||||
label={
|
||||
<Trans>
|
||||
{
|
||||
estimatedSavingsFormatted.savingsPercentageFormatted
|
||||
}{' '}
|
||||
OFF
|
||||
</Trans>
|
||||
}
|
||||
style={styles.discountChip}
|
||||
/>
|
||||
)}
|
||||
<Text noMargin color="secondary" size="block-title">
|
||||
<span style={styles.discountedPrice}>
|
||||
{estimatedTotalPriceFormatted}
|
||||
</span>
|
||||
</Text>
|
||||
<Text noMargin size="block-title">
|
||||
{productPrice}
|
||||
</Text>
|
||||
</LineStackLayout>
|
||||
<ColumnStackLayout noMargin alignItems="center">
|
||||
<PurchaseProductButtons
|
||||
i18n={i18n}
|
||||
productListingData={bundleListingData}
|
||||
selectedUsageType="default"
|
||||
onUsageTypeChange={() => {}}
|
||||
simulateAppStoreProduct={simulateAppStoreProduct}
|
||||
isAlreadyReceived={isAlreadyReceived}
|
||||
onClickBuy={onClickBuy}
|
||||
onClickBuyWithCredits={() => {}}
|
||||
customLabel={
|
||||
<Trans>
|
||||
Buy now and save{' '}
|
||||
{
|
||||
(
|
||||
getEstimatedSavingsFormatted({
|
||||
i18n,
|
||||
bundleListingData,
|
||||
productListingDatasIncludedInBundle,
|
||||
redemptionCodesIncludedInBundle,
|
||||
}) || {}
|
||||
).savingsPriceFormatted
|
||||
}
|
||||
</Trans>
|
||||
}
|
||||
/>
|
||||
{!shouldUseOrSimulateAppStoreProduct && (
|
||||
<SecureCheckout />
|
||||
)}
|
||||
</ColumnStackLayout>
|
||||
</ResponsiveLineStackLayout>
|
||||
) : (
|
||||
<Column noMargin expand justifyContent="flex-end">
|
||||
<Skeleton height={isMobile ? 60 : 40} />
|
||||
<Skeleton height={isMobile ? 60 : 40} />
|
||||
</Column>
|
||||
)}
|
||||
</Paper>
|
||||
</SectionRow>
|
||||
)}
|
||||
{!!purchasingBundleListingData && (
|
||||
<BundlePurchaseDialog
|
||||
bundleListingData={purchasingBundleListingData}
|
||||
usageType="default"
|
||||
onClose={() => setPurchasingBundleListingData(null)}
|
||||
/>
|
||||
)}
|
||||
{isRedemptionCodesDialogOpen && (
|
||||
<RedemptionCodesDialog
|
||||
onClose={() => setIsRedemptionCodesDialogOpen(false)}
|
||||
/>
|
||||
)}
|
||||
</Column>
|
||||
)}
|
||||
</I18n>
|
||||
);
|
||||
};
|
||||
|
||||
export default BundlePageHeader;
|
@@ -2,6 +2,7 @@
|
||||
|
||||
import * as React from 'react';
|
||||
import { I18n } from '@lingui/react';
|
||||
import { type I18n as I18nType } from '@lingui/core';
|
||||
import { Trans } from '@lingui/macro';
|
||||
import Divider from '@material-ui/core/Divider';
|
||||
import {
|
||||
@@ -48,8 +49,12 @@ 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 {
|
||||
getEstimatedSavingsFormatted,
|
||||
renderEstimatedTotalPriceFormatted,
|
||||
} from './Utils';
|
||||
import { formatDurationOfRedemptionCode } from '../../RedemptionCode/Utils';
|
||||
import ProductLimitedTimeOffer from '../ProductLimitedTimeOffer';
|
||||
|
||||
const highlightColor = '#6CF9F7';
|
||||
|
||||
@@ -62,7 +67,7 @@ const styles = {
|
||||
display: 'flex',
|
||||
flex: 1,
|
||||
flexDirection: 'column',
|
||||
gap: 8,
|
||||
gap: 16,
|
||||
justifyContent: 'space-between',
|
||||
},
|
||||
bundlePreviewContainer: {
|
||||
@@ -254,9 +259,11 @@ const getColumnsFromWindowSize = (windowSize: WindowSizeType) => {
|
||||
|
||||
type Props = {|
|
||||
onDisplayBundle: (bundleListingData: BundleListingData) => void,
|
||||
i18n: I18nType,
|
||||
category: string,
|
||||
|};
|
||||
|
||||
const BundlePreviewBanner = ({ onDisplayBundle }: Props) => {
|
||||
const BundlePreviewBanner = ({ onDisplayBundle, i18n, category }: Props) => {
|
||||
const { isMobile, isLandscape, windowSize } = useResponsiveWindowSize();
|
||||
const numberOfTilesToDisplay = getColumnsFromWindowSize(windowSize) - 1; // Reserve one tile for the bundle preview.
|
||||
const { privateGameTemplateListingDatas } = React.useContext(
|
||||
@@ -272,28 +279,34 @@ const BundlePreviewBanner = ({ onDisplayBundle }: Props) => {
|
||||
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.
|
||||
// - the first bundle of that category if none are owned.
|
||||
// - the first owned bundle of that category (as a listing data if still listed, or as an archived listing data otherwise)
|
||||
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 bundleListingDataOfCategory = bundleListingDatas.filter(bundle =>
|
||||
bundle.categories.includes(category)
|
||||
)[0];
|
||||
const receivedBundleOfCategory = receivedBundles.filter(bundle =>
|
||||
bundle.categories.includes(category)
|
||||
)[0];
|
||||
if (!receivedBundleOfCategory) {
|
||||
// Display the first bundle if none are found with that category.
|
||||
return bundleListingDataOfCategory || bundleListingDatas[0];
|
||||
}
|
||||
const receivedBundle = receivedBundles[0];
|
||||
const bundleListingData = bundleListingDatas.find(
|
||||
bundleListingData => bundleListingData.id === receivedBundle.id
|
||||
const bundleListingDataMatchingOwnedBundle = bundleListingDatas.find(
|
||||
bundleListingData =>
|
||||
bundleListingData.id === receivedBundleOfCategory.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,
|
||||
});
|
||||
return (
|
||||
bundleListingDataMatchingOwnedBundle ||
|
||||
getArchivedBundleListingData({
|
||||
bundle: receivedBundleOfCategory,
|
||||
})
|
||||
);
|
||||
},
|
||||
[bundleListingDatas, receivedBundles]
|
||||
[bundleListingDatas, receivedBundles, category]
|
||||
);
|
||||
|
||||
const userBundlePurchaseUsageType = React.useMemo(
|
||||
@@ -342,6 +355,51 @@ const BundlePreviewBanner = ({ onDisplayBundle }: Props) => {
|
||||
[bundleListingData]
|
||||
);
|
||||
|
||||
const estimatedTotalPriceFormatted = React.useMemo(
|
||||
() =>
|
||||
renderEstimatedTotalPriceFormatted({
|
||||
i18n,
|
||||
bundleListingData,
|
||||
productListingDatasIncludedInBundle,
|
||||
redemptionCodesIncludedInBundle,
|
||||
}),
|
||||
[
|
||||
i18n,
|
||||
bundleListingData,
|
||||
productListingDatasIncludedInBundle,
|
||||
redemptionCodesIncludedInBundle,
|
||||
]
|
||||
);
|
||||
|
||||
const estimatedSavingsFormatted = React.useMemo(
|
||||
() =>
|
||||
getEstimatedSavingsFormatted({
|
||||
i18n,
|
||||
bundleListingData,
|
||||
productListingDatasIncludedInBundle,
|
||||
redemptionCodesIncludedInBundle,
|
||||
}),
|
||||
[
|
||||
i18n,
|
||||
bundleListingData,
|
||||
productListingDatasIncludedInBundle,
|
||||
redemptionCodesIncludedInBundle,
|
||||
]
|
||||
);
|
||||
|
||||
const productPrice = React.useMemo(
|
||||
() =>
|
||||
bundleListingData
|
||||
? renderProductPrice({
|
||||
i18n,
|
||||
productListingData: bundleListingData,
|
||||
usageType: 'default',
|
||||
plainText: true,
|
||||
})
|
||||
: null,
|
||||
[i18n, bundleListingData]
|
||||
);
|
||||
|
||||
const courseTiles = React.useMemo(
|
||||
() => {
|
||||
if (isMobile && !isLandscape) {
|
||||
@@ -433,27 +491,6 @@ const BundlePreviewBanner = ({ onDisplayBundle }: Props) => {
|
||||
}}
|
||||
>
|
||||
<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
|
||||
@@ -478,20 +515,74 @@ const BundlePreviewBanner = ({ onDisplayBundle }: Props) => {
|
||||
) : (
|
||||
<Skeleton height={30} />
|
||||
)}
|
||||
{isMobile && (
|
||||
<BundlePreviewTile bundleListingData={bundleListingData} />
|
||||
)}
|
||||
</ColumnStackLayout>
|
||||
{bundleListingData ? (
|
||||
<ColumnStackLayout noMargin>
|
||||
{!isAlreadyReceived && (
|
||||
<Text noMargin color="secondary">
|
||||
<span style={styles.discountedPrice}>
|
||||
{renderEstimatedTotalPriceFormatted({
|
||||
i18n,
|
||||
bundleListingData,
|
||||
productListingDatasIncludedInBundle,
|
||||
redemptionCodesIncludedInBundle,
|
||||
})}
|
||||
</span>
|
||||
</Text>
|
||||
{estimatedSavingsFormatted &&
|
||||
estimatedTotalPriceFormatted &&
|
||||
productPrice !== null ? (
|
||||
<LineStackLayout
|
||||
alignItems="center"
|
||||
justifyContent={
|
||||
bundleListingData.visibleUntil && !isMobile
|
||||
? 'flex-start'
|
||||
: 'space-between'
|
||||
}
|
||||
noMargin
|
||||
>
|
||||
<LineStackLayout noMargin alignItems="center">
|
||||
{!isAlreadyReceived && estimatedTotalPriceFormatted && (
|
||||
<Text noMargin color="secondary" size="sub-title">
|
||||
<span style={styles.discountedPrice}>
|
||||
{estimatedTotalPriceFormatted}
|
||||
</span>
|
||||
</Text>
|
||||
)}
|
||||
{(isAlreadyReceived ||
|
||||
!bundleListingData.visibleUntil) && (
|
||||
<Chip
|
||||
label={
|
||||
isAlreadyReceived ? (
|
||||
<Trans>Owned</Trans>
|
||||
) : (
|
||||
<Trans>
|
||||
{
|
||||
estimatedSavingsFormatted.savingsPercentageFormatted
|
||||
}{' '}
|
||||
OFF
|
||||
</Trans>
|
||||
)
|
||||
}
|
||||
style={
|
||||
isAlreadyReceived
|
||||
? styles.ownedChip
|
||||
: styles.discountChip
|
||||
}
|
||||
/>
|
||||
)}
|
||||
</LineStackLayout>
|
||||
{!isAlreadyReceived && (
|
||||
<Text noMargin size="block-title">
|
||||
{productPrice}
|
||||
</Text>
|
||||
)}
|
||||
</LineStackLayout>
|
||||
) : (
|
||||
<Skeleton variant="rect" height={20} />
|
||||
)}
|
||||
{!isAlreadyReceived && bundleListingData.visibleUntil && (
|
||||
<Line noMargin expand>
|
||||
<Column noMargin expand>
|
||||
<ProductLimitedTimeOffer
|
||||
visibleUntil={bundleListingData.visibleUntil}
|
||||
hideMinutesAndSeconds
|
||||
alignCenter
|
||||
/>
|
||||
</Column>
|
||||
</Line>
|
||||
)}
|
||||
<RaisedButton
|
||||
primary
|
||||
@@ -499,15 +590,7 @@ const BundlePreviewBanner = ({ onDisplayBundle }: Props) => {
|
||||
isAlreadyReceived ? (
|
||||
<Trans>Browse bundle</Trans>
|
||||
) : (
|
||||
<Trans>
|
||||
Buy for{' '}
|
||||
{renderProductPrice({
|
||||
i18n,
|
||||
productListingData: bundleListingData,
|
||||
usageType: 'default',
|
||||
plainText: true,
|
||||
})}
|
||||
</Trans>
|
||||
<Trans>Discover this bundle</Trans>
|
||||
)
|
||||
}
|
||||
onClick={() => onDisplayBundle(bundleListingData)}
|
||||
@@ -520,7 +603,9 @@ const BundlePreviewBanner = ({ onDisplayBundle }: Props) => {
|
||||
)}
|
||||
</div>
|
||||
{courseTiles}
|
||||
<BundlePreviewTile bundleListingData={bundleListingData} />
|
||||
{!isMobile && (
|
||||
<BundlePreviewTile bundleListingData={bundleListingData} />
|
||||
)}
|
||||
</ResponsiveLineStackLayout>
|
||||
</Column>
|
||||
</Paper>
|
||||
|
@@ -8,12 +8,263 @@ import {
|
||||
type CreditsPackageListingData,
|
||||
type IncludedRedemptionCode,
|
||||
} from '../../Utils/GDevelopServices/Shop';
|
||||
import { renderPriceFormatted } from '../ProductPriceTag';
|
||||
import * as React from 'react';
|
||||
import { Trans } from '@lingui/macro';
|
||||
import { I18n } from '@lingui/react';
|
||||
import Text from '../../UI/Text';
|
||||
import { Column, Line, Spacer } from '../../UI/Grid';
|
||||
import { LineStackLayout, ResponsiveLineStackLayout } from '../../UI/Layout';
|
||||
import {
|
||||
getPlanIcon,
|
||||
getPlanInferredNameFromId,
|
||||
} from '../../Profile/Subscription/PlanCard';
|
||||
import Coin from '../../Credits/Icons/Coin';
|
||||
import { formatDurationOfRedemptionCode } from '../../RedemptionCode/Utils';
|
||||
import School from '../../UI/CustomSvgIcons/School';
|
||||
import Hammer from '../../UI/CustomSvgIcons/Hammer';
|
||||
import Store from '../../UI/CustomSvgIcons/Store';
|
||||
import { Divider } from '@material-ui/core';
|
||||
import { getCreditsAmountFromId } from '../../AssetStore/CreditsPackages/CreditsPackageStoreContext';
|
||||
|
||||
const styles = {
|
||||
discountedPrice: { textDecoration: 'line-through', opacity: 0.7 },
|
||||
coinIcon: {
|
||||
width: 13,
|
||||
height: 13,
|
||||
position: 'relative',
|
||||
top: -1,
|
||||
},
|
||||
};
|
||||
|
||||
const getEstimatedRedemptionCodePrice = (
|
||||
redemptionCode: IncludedRedemptionCode,
|
||||
currencyCode: 'USD' | 'EUR'
|
||||
): number => {
|
||||
const planId = redemptionCode.givenSubscriptionPlanId;
|
||||
if (!planId) {
|
||||
return 0;
|
||||
}
|
||||
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));
|
||||
}
|
||||
|
||||
return estimatedAmountInCents;
|
||||
};
|
||||
|
||||
export const renderEstimatedRedemptionCodePriceFormatted = (
|
||||
bundleListingData: BundleListingData,
|
||||
redemptionCode: IncludedRedemptionCode,
|
||||
i18n: I18nType
|
||||
): string => {
|
||||
const bundlePrice = bundleListingData.prices.find(
|
||||
price => price.usageType === 'default'
|
||||
);
|
||||
const currencyCode = bundlePrice ? bundlePrice.currency : 'USD';
|
||||
const estimatedPrice = getEstimatedRedemptionCodePrice(
|
||||
redemptionCode,
|
||||
currencyCode
|
||||
);
|
||||
return renderPriceFormatted(estimatedPrice, currencyCode, i18n);
|
||||
};
|
||||
|
||||
const getEstimatedTotalPriceAndCurrencyCode = ({
|
||||
bundleListingData,
|
||||
productListingDatasIncludedInBundle,
|
||||
redemptionCodesIncludedInBundle,
|
||||
filter,
|
||||
}: {
|
||||
bundleListingData: ?BundleListingData,
|
||||
productListingDatasIncludedInBundle: ?Array<
|
||||
| PrivateAssetPackListingData
|
||||
| PrivateGameTemplateListingData
|
||||
| BundleListingData
|
||||
| CourseListingData
|
||||
| CreditsPackageListingData
|
||||
>,
|
||||
redemptionCodesIncludedInBundle: ?Array<IncludedRedemptionCode>,
|
||||
filter?:
|
||||
| 'ASSET_PACK'
|
||||
| 'GAME_TEMPLATE'
|
||||
| 'COURSE'
|
||||
| 'BUNDLE'
|
||||
| 'CREDITS_PACKAGE'
|
||||
| 'REDEMPTION_CODE',
|
||||
}): ?{
|
||||
totalPrice: number,
|
||||
bundlePrice: number,
|
||||
currencyCode: 'USD' | 'EUR',
|
||||
} => {
|
||||
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 bundlePriceValue = bundlePrice ? bundlePrice.value : 0;
|
||||
|
||||
for (const product of bundleListingData.includedListableProducts || []) {
|
||||
if (
|
||||
product.productType === 'ASSET_PACK' &&
|
||||
(!filter || filter === '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' &&
|
||||
(!filter || filter === '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' &&
|
||||
(!filter || filter === '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' &&
|
||||
(!filter || filter === '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' &&
|
||||
(!filter || filter === '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 &&
|
||||
(!filter || filter === 'REDEMPTION_CODE')
|
||||
) {
|
||||
for (const redemptionCode of redemptionCodesIncludedInBundle) {
|
||||
const estimatedAmountInCents = getEstimatedRedemptionCodePrice(
|
||||
redemptionCode,
|
||||
currencyCode
|
||||
);
|
||||
totalPrice += estimatedAmountInCents || 0;
|
||||
}
|
||||
}
|
||||
|
||||
return { totalPrice, bundlePrice: bundlePriceValue, currencyCode };
|
||||
};
|
||||
|
||||
export const renderEstimatedTotalPriceFormatted = ({
|
||||
i18n,
|
||||
bundleListingData,
|
||||
productListingDatasIncludedInBundle,
|
||||
redemptionCodesIncludedInBundle,
|
||||
filter,
|
||||
}: {|
|
||||
i18n: I18nType,
|
||||
bundleListingData: ?BundleListingData,
|
||||
productListingDatasIncludedInBundle: ?Array<
|
||||
| PrivateAssetPackListingData
|
||||
| PrivateGameTemplateListingData
|
||||
| BundleListingData
|
||||
| CourseListingData
|
||||
| CreditsPackageListingData
|
||||
>,
|
||||
redemptionCodesIncludedInBundle: ?Array<IncludedRedemptionCode>,
|
||||
filter?:
|
||||
| 'ASSET_PACK'
|
||||
| 'GAME_TEMPLATE'
|
||||
| 'COURSE'
|
||||
| 'BUNDLE'
|
||||
| 'CREDITS_PACKAGE'
|
||||
| 'REDEMPTION_CODE',
|
||||
|}): ?string => {
|
||||
const estimatedPriceAndCode = getEstimatedTotalPriceAndCurrencyCode({
|
||||
bundleListingData,
|
||||
productListingDatasIncludedInBundle,
|
||||
redemptionCodesIncludedInBundle,
|
||||
filter,
|
||||
});
|
||||
if (!estimatedPriceAndCode || !bundleListingData) return null;
|
||||
|
||||
return renderPriceFormatted(
|
||||
estimatedPriceAndCode.totalPrice,
|
||||
estimatedPriceAndCode.currencyCode,
|
||||
i18n
|
||||
);
|
||||
};
|
||||
|
||||
export const getEstimatedSavingsFormatted = ({
|
||||
i18n,
|
||||
bundleListingData,
|
||||
productListingDatasIncludedInBundle,
|
||||
redemptionCodesIncludedInBundle,
|
||||
}: {
|
||||
i18n: I18nType,
|
||||
bundleListingData: ?BundleListingData,
|
||||
@@ -25,118 +276,450 @@ export const renderEstimatedTotalPriceFormatted = ({
|
||||
| CreditsPackageListingData
|
||||
>,
|
||||
redemptionCodesIncludedInBundle: ?Array<IncludedRedemptionCode>,
|
||||
}): ?string => {
|
||||
let totalPrice = 0;
|
||||
if (
|
||||
!bundleListingData ||
|
||||
!productListingDatasIncludedInBundle ||
|
||||
!redemptionCodesIncludedInBundle
|
||||
)
|
||||
return null;
|
||||
}): ?{ savingsPriceFormatted: string, savingsPercentageFormatted: string } => {
|
||||
const estimatedTotalPriceAndCode = getEstimatedTotalPriceAndCurrencyCode({
|
||||
bundleListingData,
|
||||
productListingDatasIncludedInBundle,
|
||||
redemptionCodesIncludedInBundle,
|
||||
});
|
||||
if (!estimatedTotalPriceAndCode || !bundleListingData) return null;
|
||||
|
||||
const productPrices = bundleListingData.prices;
|
||||
const bundlePrice = productPrices.find(
|
||||
price => price.usageType === 'default'
|
||||
);
|
||||
const currencyCode = bundlePrice ? bundlePrice.currency : 'USD';
|
||||
const currencySymbol = currencyCode === 'USD' ? '$' : '€';
|
||||
const savings =
|
||||
estimatedTotalPriceAndCode.totalPrice -
|
||||
estimatedTotalPriceAndCode.bundlePrice;
|
||||
|
||||
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$/, '')}`;
|
||||
return {
|
||||
savingsPriceFormatted: renderPriceFormatted(
|
||||
savings,
|
||||
estimatedTotalPriceAndCode.currencyCode,
|
||||
i18n
|
||||
),
|
||||
savingsPercentageFormatted: `${Math.round(
|
||||
(savings / estimatedTotalPriceAndCode.totalPrice) * 100
|
||||
)}%`,
|
||||
};
|
||||
};
|
||||
|
||||
const getCoursesEstimatedHoursToComplete = (
|
||||
courseListingDatas: CourseListingData[]
|
||||
) => {
|
||||
// Ideally we'd look at the Course durationInWeeks, but to avoid too
|
||||
// many API calls, we just estimate.
|
||||
const totalHours = courseListingDatas.reduce((acc, course) => {
|
||||
const estimatedHours = 4 * 5; // Estimate 4h per day, 5 days a week.
|
||||
return acc + Math.round(estimatedHours);
|
||||
}, 0);
|
||||
return Math.round(totalHours);
|
||||
};
|
||||
|
||||
const CoursesLineSummary = ({
|
||||
courseListingDatasIncludedInBundle,
|
||||
bundleListingData,
|
||||
productListingDatasIncludedInBundle,
|
||||
redemptionCodesIncludedInBundle,
|
||||
}: {|
|
||||
courseListingDatasIncludedInBundle: CourseListingData[],
|
||||
bundleListingData: BundleListingData,
|
||||
productListingDatasIncludedInBundle: (
|
||||
| PrivateAssetPackListingData
|
||||
| PrivateGameTemplateListingData
|
||||
| BundleListingData
|
||||
| CreditsPackageListingData
|
||||
| CourseListingData
|
||||
)[],
|
||||
redemptionCodesIncludedInBundle: IncludedRedemptionCode[],
|
||||
|}) => (
|
||||
<I18n>
|
||||
{({ i18n }) => (
|
||||
<LineStackLayout noMargin alignItems="flex-start">
|
||||
<School />
|
||||
<Column noMargin>
|
||||
<Text noMargin size="sub-title">
|
||||
<Trans>
|
||||
{courseListingDatasIncludedInBundle.length === 1 ? (
|
||||
<Trans>
|
||||
{courseListingDatasIncludedInBundle.length} Course
|
||||
</Trans>
|
||||
) : (
|
||||
<Trans>
|
||||
{courseListingDatasIncludedInBundle.length} Courses
|
||||
</Trans>
|
||||
)}
|
||||
</Trans>
|
||||
</Text>
|
||||
<Text color="secondary" noMargin>
|
||||
<Trans>
|
||||
{getCoursesEstimatedHoursToComplete(
|
||||
courseListingDatasIncludedInBundle
|
||||
)}{' '}
|
||||
hours of material
|
||||
</Trans>
|
||||
</Text>
|
||||
<Text color="secondary" noMargin>
|
||||
<span style={styles.discountedPrice}>
|
||||
{renderEstimatedTotalPriceFormatted({
|
||||
i18n,
|
||||
bundleListingData,
|
||||
productListingDatasIncludedInBundle,
|
||||
redemptionCodesIncludedInBundle,
|
||||
filter: 'COURSE',
|
||||
})}
|
||||
</span>
|
||||
</Text>
|
||||
</Column>
|
||||
</LineStackLayout>
|
||||
)}
|
||||
</I18n>
|
||||
);
|
||||
|
||||
const AssetPacksLineSummary = ({
|
||||
assetPackListingDatasIncludedInBundle,
|
||||
bundleListingData,
|
||||
productListingDatasIncludedInBundle,
|
||||
redemptionCodesIncludedInBundle,
|
||||
}: {|
|
||||
assetPackListingDatasIncludedInBundle: PrivateAssetPackListingData[],
|
||||
bundleListingData: BundleListingData,
|
||||
productListingDatasIncludedInBundle: (
|
||||
| PrivateAssetPackListingData
|
||||
| PrivateGameTemplateListingData
|
||||
| BundleListingData
|
||||
| CreditsPackageListingData
|
||||
| CourseListingData
|
||||
)[],
|
||||
redemptionCodesIncludedInBundle: IncludedRedemptionCode[],
|
||||
|}) => (
|
||||
<I18n>
|
||||
{({ i18n }) => (
|
||||
<LineStackLayout noMargin alignItems="flex-start">
|
||||
<Store />
|
||||
<Column noMargin>
|
||||
<Text noMargin size="sub-title">
|
||||
<Trans>
|
||||
{assetPackListingDatasIncludedInBundle.length === 1 ? (
|
||||
<Trans>
|
||||
{assetPackListingDatasIncludedInBundle.length} Asset pack
|
||||
</Trans>
|
||||
) : (
|
||||
<Trans>
|
||||
{assetPackListingDatasIncludedInBundle.length} Asset packs
|
||||
</Trans>
|
||||
)}
|
||||
</Trans>
|
||||
</Text>
|
||||
<Text color="secondary" noMargin>
|
||||
<Trans>Commercial License</Trans>
|
||||
</Text>
|
||||
<Text color="secondary" noMargin>
|
||||
<span style={styles.discountedPrice}>
|
||||
{renderEstimatedTotalPriceFormatted({
|
||||
i18n,
|
||||
bundleListingData,
|
||||
productListingDatasIncludedInBundle,
|
||||
redemptionCodesIncludedInBundle,
|
||||
filter: 'ASSET_PACK',
|
||||
})}
|
||||
</span>
|
||||
</Text>
|
||||
</Column>
|
||||
</LineStackLayout>
|
||||
)}
|
||||
</I18n>
|
||||
);
|
||||
|
||||
const GameTemplatesLineSummary = ({
|
||||
gameTemplateListingDatasIncludedInBundle,
|
||||
bundleListingData,
|
||||
productListingDatasIncludedInBundle,
|
||||
redemptionCodesIncludedInBundle,
|
||||
}: {|
|
||||
gameTemplateListingDatasIncludedInBundle: PrivateGameTemplateListingData[],
|
||||
bundleListingData: BundleListingData,
|
||||
productListingDatasIncludedInBundle: (
|
||||
| PrivateAssetPackListingData
|
||||
| PrivateGameTemplateListingData
|
||||
| BundleListingData
|
||||
| CreditsPackageListingData
|
||||
| CourseListingData
|
||||
)[],
|
||||
redemptionCodesIncludedInBundle: IncludedRedemptionCode[],
|
||||
|}) => (
|
||||
<I18n>
|
||||
{({ i18n }) => (
|
||||
<LineStackLayout noMargin alignItems="flex-start">
|
||||
<Hammer />
|
||||
<Column noMargin>
|
||||
<Text noMargin size="sub-title">
|
||||
<Trans>
|
||||
{gameTemplateListingDatasIncludedInBundle.length === 1 ? (
|
||||
<Trans>
|
||||
{gameTemplateListingDatasIncludedInBundle.length} Game
|
||||
template
|
||||
</Trans>
|
||||
) : (
|
||||
<Trans>
|
||||
{gameTemplateListingDatasIncludedInBundle.length} Game
|
||||
templates
|
||||
</Trans>
|
||||
)}
|
||||
</Trans>
|
||||
</Text>
|
||||
<Text color="secondary" noMargin>
|
||||
<Trans>Lifetime access</Trans>
|
||||
</Text>
|
||||
<Text color="secondary" noMargin>
|
||||
<span style={styles.discountedPrice}>
|
||||
{renderEstimatedTotalPriceFormatted({
|
||||
i18n,
|
||||
bundleListingData,
|
||||
productListingDatasIncludedInBundle,
|
||||
redemptionCodesIncludedInBundle,
|
||||
filter: 'GAME_TEMPLATE',
|
||||
})}
|
||||
</span>
|
||||
</Text>
|
||||
</Column>
|
||||
</LineStackLayout>
|
||||
)}
|
||||
</I18n>
|
||||
);
|
||||
|
||||
const RedemptionCodeLineSummary = ({
|
||||
includedRedemptionCode,
|
||||
bundleListingData,
|
||||
}: {|
|
||||
includedRedemptionCode: IncludedRedemptionCode,
|
||||
bundleListingData: BundleListingData,
|
||||
|}) => (
|
||||
<I18n>
|
||||
{({ i18n }) => (
|
||||
<LineStackLayout noMargin alignItems="flex-start">
|
||||
{getPlanIcon({
|
||||
planId: includedRedemptionCode.givenSubscriptionPlanId,
|
||||
logoSize: 20,
|
||||
})}
|
||||
<Column noMargin>
|
||||
<Text noMargin size="sub-title">
|
||||
<Trans>
|
||||
{getPlanInferredNameFromId(
|
||||
includedRedemptionCode.givenSubscriptionPlanId
|
||||
)}
|
||||
subscription
|
||||
</Trans>
|
||||
</Text>
|
||||
<Text color="secondary" noMargin>
|
||||
{formatDurationOfRedemptionCode(
|
||||
includedRedemptionCode.durationInDays
|
||||
)}
|
||||
</Text>
|
||||
<Text color="secondary" noMargin>
|
||||
<span style={styles.discountedPrice}>
|
||||
{renderEstimatedRedemptionCodePriceFormatted(
|
||||
bundleListingData,
|
||||
includedRedemptionCode,
|
||||
i18n
|
||||
)}
|
||||
</span>
|
||||
</Text>
|
||||
</Column>
|
||||
</LineStackLayout>
|
||||
)}
|
||||
</I18n>
|
||||
);
|
||||
|
||||
const CreditsLineSummary = ({
|
||||
includedCreditsAmount,
|
||||
}: {
|
||||
includedCreditsAmount: number,
|
||||
}) => (
|
||||
<LineStackLayout noMargin alignItems="center">
|
||||
<Coin style={styles.coinIcon} />
|
||||
<Text noMargin size="sub-title">
|
||||
<Trans>{includedCreditsAmount} credits</Trans>
|
||||
</Text>
|
||||
</LineStackLayout>
|
||||
);
|
||||
|
||||
export const getSummaryLines = ({
|
||||
redemptionCodesIncludedInBundle,
|
||||
bundleListingData,
|
||||
productListingDatasIncludedInBundle,
|
||||
}: {|
|
||||
redemptionCodesIncludedInBundle: IncludedRedemptionCode[],
|
||||
bundleListingData: BundleListingData,
|
||||
productListingDatasIncludedInBundle: (
|
||||
| PrivateAssetPackListingData
|
||||
| PrivateGameTemplateListingData
|
||||
| BundleListingData
|
||||
| CreditsPackageListingData
|
||||
| CourseListingData
|
||||
)[],
|
||||
|}) => {
|
||||
const includedListableProducts =
|
||||
bundleListingData.includedListableProducts || [];
|
||||
const summaryLineItems = [];
|
||||
|
||||
const includedCourseListableProducts = includedListableProducts.filter(
|
||||
product => product.productType === 'COURSE'
|
||||
);
|
||||
|
||||
const courseListingDatasIncludedInBundle = includedCourseListableProducts
|
||||
.map(product => {
|
||||
// $FlowFixMe - We know it's a course because of the filter.
|
||||
const courseListingData: ?CourseListingData = productListingDatasIncludedInBundle.find(
|
||||
listingData =>
|
||||
listingData.id === product.productId &&
|
||||
listingData.productType === 'COURSE'
|
||||
);
|
||||
|
||||
return courseListingData;
|
||||
})
|
||||
.filter(Boolean);
|
||||
if (courseListingDatasIncludedInBundle.length) {
|
||||
summaryLineItems.push(
|
||||
<CoursesLineSummary
|
||||
courseListingDatasIncludedInBundle={courseListingDatasIncludedInBundle}
|
||||
bundleListingData={bundleListingData}
|
||||
productListingDatasIncludedInBundle={
|
||||
productListingDatasIncludedInBundle
|
||||
}
|
||||
redemptionCodesIncludedInBundle={redemptionCodesIncludedInBundle}
|
||||
/>
|
||||
);
|
||||
}
|
||||
const includedAssetPackListableProducts = includedListableProducts.filter(
|
||||
product => product.productType === 'ASSET_PACK'
|
||||
);
|
||||
|
||||
const assetPackListingDatasIncludedInBundle = includedAssetPackListableProducts
|
||||
.map(product => {
|
||||
// $FlowFixMe - We know it's an asset pack because of the filter.
|
||||
const assetPackListingData: ?PrivateAssetPackListingData = productListingDatasIncludedInBundle.find(
|
||||
listingData =>
|
||||
listingData.id === product.productId &&
|
||||
listingData.productType === 'ASSET_PACK'
|
||||
);
|
||||
|
||||
return assetPackListingData;
|
||||
})
|
||||
.filter(Boolean);
|
||||
|
||||
if (assetPackListingDatasIncludedInBundle.length) {
|
||||
summaryLineItems.push(
|
||||
<AssetPacksLineSummary
|
||||
assetPackListingDatasIncludedInBundle={
|
||||
assetPackListingDatasIncludedInBundle
|
||||
}
|
||||
bundleListingData={bundleListingData}
|
||||
productListingDatasIncludedInBundle={
|
||||
productListingDatasIncludedInBundle
|
||||
}
|
||||
redemptionCodesIncludedInBundle={redemptionCodesIncludedInBundle}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
const includedGameTemplateListableProducts = includedListableProducts.filter(
|
||||
product => product.productType === 'GAME_TEMPLATE'
|
||||
);
|
||||
|
||||
const gameTemplateListingDatasIncludedInBundle = includedGameTemplateListableProducts
|
||||
.map(product => {
|
||||
// $FlowFixMe - We know it's a game template because of the filter.
|
||||
const gameTemplateListingData: ?PrivateGameTemplateListingData = productListingDatasIncludedInBundle.find(
|
||||
listingData =>
|
||||
listingData.id === product.productId &&
|
||||
listingData.productType === 'GAME_TEMPLATE'
|
||||
);
|
||||
|
||||
return gameTemplateListingData;
|
||||
})
|
||||
.filter(Boolean);
|
||||
if (gameTemplateListingDatasIncludedInBundle.length) {
|
||||
summaryLineItems.push(
|
||||
<GameTemplatesLineSummary
|
||||
gameTemplateListingDatasIncludedInBundle={
|
||||
gameTemplateListingDatasIncludedInBundle
|
||||
}
|
||||
bundleListingData={bundleListingData}
|
||||
productListingDatasIncludedInBundle={
|
||||
productListingDatasIncludedInBundle
|
||||
}
|
||||
redemptionCodesIncludedInBundle={redemptionCodesIncludedInBundle}
|
||||
/>
|
||||
);
|
||||
}
|
||||
if (redemptionCodesIncludedInBundle.length) {
|
||||
redemptionCodesIncludedInBundle.forEach(code => {
|
||||
summaryLineItems.push(
|
||||
<RedemptionCodeLineSummary
|
||||
includedRedemptionCode={code}
|
||||
bundleListingData={bundleListingData}
|
||||
/>
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
const includedCreditsAmount = includedListableProducts
|
||||
.filter(product => product.productType === 'CREDIT_PACKAGE')
|
||||
.reduce(
|
||||
(total, product) => total + getCreditsAmountFromId(product.productId),
|
||||
0
|
||||
);
|
||||
if (includedCreditsAmount) {
|
||||
summaryLineItems.push(
|
||||
<CreditsLineSummary includedCreditsAmount={includedCreditsAmount} />
|
||||
);
|
||||
}
|
||||
|
||||
const mobileLineItems = [];
|
||||
const desktopLineItems = [];
|
||||
summaryLineItems.forEach((item, index) => {
|
||||
if (index !== 0) {
|
||||
desktopLineItems.push(
|
||||
<Line noMargin key={`divider-${index}`}>
|
||||
<Spacer />
|
||||
<Column>
|
||||
<Divider orientation="vertical" />
|
||||
</Column>
|
||||
<Spacer />
|
||||
</Line>
|
||||
);
|
||||
if (index % 2 === 1) {
|
||||
mobileLineItems.push(
|
||||
<Column key={`divider-${index}`}>
|
||||
<Divider orientation="vertical" />
|
||||
</Column>
|
||||
);
|
||||
}
|
||||
}
|
||||
desktopLineItems.push(item);
|
||||
mobileLineItems.push(
|
||||
<Column expand noMargin key={`item-${index}`}>
|
||||
{item}
|
||||
</Column>
|
||||
);
|
||||
});
|
||||
|
||||
// Desktop, everything on one line.
|
||||
const desktopLines = [];
|
||||
desktopLines.push(
|
||||
<ResponsiveLineStackLayout expand key="desktop-line">
|
||||
{desktopLineItems}
|
||||
</ResponsiveLineStackLayout>
|
||||
);
|
||||
// Mobile, 3 items by line (2 + 1 divider)
|
||||
const mobileLines = [];
|
||||
for (let i = 0; i < mobileLineItems.length; i += 3) {
|
||||
mobileLines.push(
|
||||
<LineStackLayout key={`mobile-line-${i}`} expand>
|
||||
{mobileLineItems.slice(i, i + 3)}
|
||||
</LineStackLayout>
|
||||
);
|
||||
}
|
||||
|
||||
return {
|
||||
mobileLines,
|
||||
desktopLines,
|
||||
};
|
||||
};
|
||||
|
@@ -20,6 +20,7 @@ import { useResponsiveWindowSize } from '../../UI/Responsive/ResponsiveWindowMea
|
||||
import Download from '../../UI/CustomSvgIcons/Download';
|
||||
import Add from '../../UI/CustomSvgIcons/Add';
|
||||
import ErrorBoundary from '../../UI/ErrorBoundary';
|
||||
import useAlertDialog from '../../UI/Alert/useAlertDialog';
|
||||
|
||||
type Props = {|
|
||||
project: gdProject,
|
||||
@@ -54,6 +55,7 @@ const ExtensionsSearchDialog = ({
|
||||
TRIVIAL_FIRST_EXTENSION,
|
||||
installExtension
|
||||
);
|
||||
const { showConfirmation, showAlert } = useAlertDialog();
|
||||
|
||||
const installOrImportExtension = async (
|
||||
i18n: I18nType,
|
||||
@@ -78,7 +80,9 @@ const ExtensionsSearchDialog = ({
|
||||
i18n,
|
||||
eventsFunctionsExtensionsState,
|
||||
project,
|
||||
onInstallExtension
|
||||
onInstallExtension,
|
||||
showConfirmation,
|
||||
showAlert
|
||||
);
|
||||
}
|
||||
|
||||
|
@@ -9,8 +9,14 @@ import {
|
||||
import { addSerializedExtensionsToProject } from '../InstallAsset';
|
||||
import { type EventsFunctionsExtensionsState } from '../../EventsFunctionsExtensionsLoader/EventsFunctionsExtensionsContext';
|
||||
import { t } from '@lingui/macro';
|
||||
import Window from '../../Utils/Window';
|
||||
import { retryIfFailed } from '../../Utils/RetryIfFailed';
|
||||
import { mapVector } from '../../Utils/MapFor';
|
||||
import {
|
||||
type ShowAlertDialogOptions,
|
||||
type ShowConfirmDialogOptions,
|
||||
} from '../../UI/Alert/AlertContext';
|
||||
|
||||
const gd: libGDevelop = global.gd;
|
||||
|
||||
/**
|
||||
* Download and add the extension in the project.
|
||||
@@ -51,7 +57,9 @@ export const importExtension = async (
|
||||
i18n: I18nType,
|
||||
eventsFunctionsExtensionsState: EventsFunctionsExtensionsState,
|
||||
project: gdProject,
|
||||
onWillInstallExtension: (extensionName: string) => void
|
||||
onWillInstallExtension: (extensionName: string) => void,
|
||||
showConfirmation: ShowConfirmDialogOptions => Promise<boolean>,
|
||||
showAlert: ShowAlertDialogOptions => Promise<void>
|
||||
): Promise<string | null> => {
|
||||
const eventsFunctionsExtensionOpener = eventsFunctionsExtensionsState.getEventsFunctionsExtensionOpener();
|
||||
if (!eventsFunctionsExtensionOpener) return null;
|
||||
@@ -65,12 +73,29 @@ export const importExtension = async (
|
||||
);
|
||||
|
||||
if (project.hasEventsFunctionsExtensionNamed(serializedExtension.name)) {
|
||||
const answer = Window.showConfirmDialog(
|
||||
i18n._(
|
||||
t`An extension with this name already exists in the project. Importing this extension will replace it: are you sure you want to continue?`
|
||||
)
|
||||
);
|
||||
const answer = await showConfirmation({
|
||||
title: t`Replace existing extension`,
|
||||
message: t`An extension with this name already exists in the project. Importing this extension will replace it.`,
|
||||
confirmButtonLabel: `Replace`,
|
||||
});
|
||||
if (!answer) return null;
|
||||
} else {
|
||||
let hasConflictWithBuiltInExtension = false;
|
||||
const allExtensions = gd
|
||||
.asPlatform(gd.JsPlatform.get())
|
||||
.getAllPlatformExtensions();
|
||||
mapVector(allExtensions, extension => {
|
||||
if (extension.getName() === serializedExtension.name) {
|
||||
hasConflictWithBuiltInExtension = true;
|
||||
}
|
||||
});
|
||||
if (hasConflictWithBuiltInExtension) {
|
||||
await showAlert({
|
||||
title: t`Invalid name`,
|
||||
message: t`The extension can't be imported because it has the same name as a built-in extension.`,
|
||||
});
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
onWillInstallExtension(serializedExtension.name);
|
||||
|
@@ -230,7 +230,7 @@ const getMergedInstalledWithDefaultEnumeratedObjectMetadataByCategory = ({
|
||||
iconFilename:
|
||||
'data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0idXRmLTgiPz4NCjwhLS0gR2VuZXJhdG9yOiBBZG9iZSBJbGx1c3RyYXRvciAyMy4wLjMsIFNWRyBFeHBvcnQgUGx1Zy1JbiAuIFNWRyBWZXJzaW9uOiA2LjAwIEJ1aWxkIDApICAtLT4NCjxzdmcgdmVyc2lvbj0iMS4xIiBpZD0iSWNvbnMiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgeG1sbnM6eGxpbms9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGxpbmsiIHg9IjBweCIgeT0iMHB4Ig0KCSB2aWV3Qm94PSIwIDAgMzIgMzIiIHN0eWxlPSJlbmFibGUtYmFja2dyb3VuZDpuZXcgMCAwIDMyIDMyOyIgeG1sOnNwYWNlPSJwcmVzZXJ2ZSI+DQo8c3R5bGUgdHlwZT0idGV4dC9jc3MiPg0KCS5zdDB7ZmlsbDpub25lO3N0cm9rZTojMDAwMDAwO3N0cm9rZS13aWR0aDoyO3N0cm9rZS1saW5lY2FwOnJvdW5kO3N0cm9rZS1saW5lam9pbjpyb3VuZDtzdHJva2UtbWl0ZXJsaW1pdDoxMDt9DQo8L3N0eWxlPg0KPGNpcmNsZSBjbGFzcz0ic3QwIiBjeD0iMjMiIGN5PSI3IiByPSIzIi8+DQo8bGluZSBjbGFzcz0ic3QwIiB4MT0iMyIgeTE9IjciIHgyPSIyMCIgeTI9IjciLz4NCjxsaW5lIGNsYXNzPSJzdDAiIHgxPSIyOSIgeTE9IjciIHgyPSIyNiIgeTI9IjciLz4NCjxjaXJjbGUgY2xhc3M9InN0MCIgY3g9IjEyIiBjeT0iMTYiIHI9IjMiLz4NCjxsaW5lIGNsYXNzPSJzdDAiIHgxPSIzIiB5MT0iMTYiIHgyPSI5IiB5Mj0iMTYiLz4NCjxsaW5lIGNsYXNzPSJzdDAiIHgxPSIyOSIgeTE9IjE2IiB4Mj0iMTUiIHkyPSIxNiIvPg0KPGNpcmNsZSBjbGFzcz0ic3QwIiBjeD0iMjMiIGN5PSIyNSIgcj0iMyIvPg0KPGxpbmUgY2xhc3M9InN0MCIgeDE9IjMiIHkxPSIyNSIgeDI9IjIwIiB5Mj0iMjUiLz4NCjxsaW5lIGNsYXNzPSJzdDAiIHgxPSIyOSIgeTE9IjI1IiB4Mj0iMjYiIHkyPSIyNSIvPg0KPC9zdmc+DQo=',
|
||||
isRenderedIn3D: false,
|
||||
assetStorePackTag: 'settings ui',
|
||||
assetStorePackTag: 'slider',
|
||||
requiredExtensions: [
|
||||
{
|
||||
extensionName: 'PanelSpriteSlider',
|
||||
@@ -245,7 +245,7 @@ const getMergedInstalledWithDefaultEnumeratedObjectMetadataByCategory = ({
|
||||
iconFilename:
|
||||
'data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0idXRmLTgiPz4NCjwhLS0gR2VuZXJhdG9yOiBBZG9iZSBJbGx1c3RyYXRvciAyMy4wLjMsIFNWRyBFeHBvcnQgUGx1Zy1JbiAuIFNWRyBWZXJzaW9uOiA2LjAwIEJ1aWxkIDApICAtLT4NCjxzdmcgdmVyc2lvbj0iMS4xIiBpZD0iSWNvbnMiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgeG1sbnM6eGxpbms9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGxpbmsiIHg9IjBweCIgeT0iMHB4Ig0KCSB2aWV3Qm94PSIwIDAgMzIgMzIiIHN0eWxlPSJlbmFibGUtYmFja2dyb3VuZDpuZXcgMCAwIDMyIDMyOyIgeG1sOnNwYWNlPSJwcmVzZXJ2ZSI+DQo8c3R5bGUgdHlwZT0idGV4dC9jc3MiPg0KCS5zdDB7ZmlsbDpub25lO3N0cm9rZTojMDAwMDAwO3N0cm9rZS13aWR0aDoyO3N0cm9rZS1saW5lY2FwOnJvdW5kO3N0cm9rZS1saW5lam9pbjpyb3VuZDtzdHJva2UtbWl0ZXJsaW1pdDoxMDt9DQo8L3N0eWxlPg0KPHBhdGggY2xhc3M9InN0MCIgZD0iTTIzLDIzSDljLTMuOSwwLTctMy4xLTctN3YwYzAtMy45LDMuMS03LDctN2gxNGMzLjksMCw3LDMuMSw3LDd2MEMzMCwxOS45LDI2LjksMjMsMjMsMjN6Ii8+DQo8Y2lyY2xlIGNsYXNzPSJzdDAiIGN4PSI5IiBjeT0iMTYiIHI9IjQiLz4NCjwvc3ZnPg0K',
|
||||
isRenderedIn3D: false,
|
||||
assetStorePackTag: 'settings ui',
|
||||
assetStorePackTag: 'toggle switch',
|
||||
requiredExtensions: [
|
||||
{
|
||||
extensionName: 'SpriteToggleSwitch',
|
||||
|
@@ -202,7 +202,9 @@ const PrivateAssetPackInformationPage = ({
|
||||
CreditsPackageStoreContext
|
||||
);
|
||||
const [selectedUsageType, setSelectedUsageType] = React.useState<string>(
|
||||
privateAssetPackListingData.prices[0].usageType
|
||||
privateAssetPackListingData.prices.length
|
||||
? privateAssetPackListingData.prices[0].usageType
|
||||
: ''
|
||||
);
|
||||
const [
|
||||
purchasingPrivateAssetPackListingData,
|
||||
@@ -235,9 +237,7 @@ const PrivateAssetPackInformationPage = ({
|
||||
const userAssetPackPurchaseUsageType = React.useMemo(
|
||||
() =>
|
||||
getUserProductPurchaseUsageType({
|
||||
productId: privateAssetPackListingData
|
||||
? privateAssetPackListingData.id
|
||||
: null,
|
||||
productId: privateAssetPackListingData.id,
|
||||
receivedProducts: [
|
||||
...(receivedAssetPacks || []),
|
||||
...(receivedBundles || []),
|
||||
@@ -736,10 +736,7 @@ const PrivateAssetPackInformationPage = ({
|
||||
label={<Trans>Browse assets</Trans>}
|
||||
/>
|
||||
) : (
|
||||
<>
|
||||
{!shouldUseOrSimulateAppStoreProduct && (
|
||||
<SecureCheckout />
|
||||
)}
|
||||
<ColumnStackLayout noMargin>
|
||||
{!errorText && (
|
||||
<PurchaseProductButtons
|
||||
i18n={i18n}
|
||||
@@ -752,7 +749,10 @@ const PrivateAssetPackInformationPage = ({
|
||||
onClickBuyWithCredits={onWillBuyWithCredits}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
{!shouldUseOrSimulateAppStoreProduct && (
|
||||
<SecureCheckout />
|
||||
)}
|
||||
</ColumnStackLayout>
|
||||
)}
|
||||
</ColumnStackLayout>
|
||||
</div>
|
||||
|
@@ -170,7 +170,9 @@ const PrivateGameTemplateInformationPage = ({
|
||||
null
|
||||
);
|
||||
const [selectedUsageType, setSelectedUsageType] = React.useState<string>(
|
||||
privateGameTemplateListingData.prices[0].usageType
|
||||
privateGameTemplateListingData.prices.length
|
||||
? privateGameTemplateListingData.prices[0].usageType
|
||||
: ''
|
||||
);
|
||||
const [
|
||||
purchasingPrivateGameTemplateListingData,
|
||||
@@ -202,9 +204,7 @@ const PrivateGameTemplateInformationPage = ({
|
||||
const userGameTemplatePurchaseUsageType = React.useMemo(
|
||||
() =>
|
||||
getUserProductPurchaseUsageType({
|
||||
productId: privateGameTemplateListingData
|
||||
? privateGameTemplateListingData.id
|
||||
: null,
|
||||
productId: privateGameTemplateListingData.id,
|
||||
receivedProducts: [
|
||||
...(receivedGameTemplates || []),
|
||||
...(receivedBundles || []),
|
||||
@@ -596,10 +596,7 @@ const PrivateGameTemplateInformationPage = ({
|
||||
/>
|
||||
<Spacer />
|
||||
{!isAlreadyReceived ? (
|
||||
<>
|
||||
{!shouldUseOrSimulateAppStoreProduct && (
|
||||
<SecureCheckout />
|
||||
)}
|
||||
<ColumnStackLayout noMargin>
|
||||
{!errorText && (
|
||||
<PurchaseProductButtons
|
||||
i18n={i18n}
|
||||
@@ -614,7 +611,10 @@ const PrivateGameTemplateInformationPage = ({
|
||||
onClickBuyWithCredits={onWillBuyWithCredits}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
{!shouldUseOrSimulateAppStoreProduct && (
|
||||
<SecureCheckout />
|
||||
)}
|
||||
</ColumnStackLayout>
|
||||
) : onCreateWithGameTemplate ? (
|
||||
<OpenProductButton
|
||||
productListingData={privateGameTemplateListingData}
|
||||
|
133
newIDE/app/src/AssetStore/ProductLimitedTimeOffer.js
Normal file
133
newIDE/app/src/AssetStore/ProductLimitedTimeOffer.js
Normal file
@@ -0,0 +1,133 @@
|
||||
// @flow
|
||||
import * as React from 'react';
|
||||
import { Trans } from '@lingui/macro';
|
||||
import Text from '../UI/Text';
|
||||
import { Column, Spacer } from '../UI/Grid';
|
||||
import { LineStackLayout } from '../UI/Layout';
|
||||
|
||||
const styles = {
|
||||
limitedTimeContainer: {
|
||||
display: 'flex',
|
||||
backgroundColor: '#F03F18',
|
||||
color: 'white',
|
||||
borderRadius: 4,
|
||||
padding: '8px 0',
|
||||
},
|
||||
};
|
||||
|
||||
type Props = {|
|
||||
visibleUntil: string,
|
||||
hideMinutesAndSeconds?: boolean,
|
||||
alignCenter?: boolean,
|
||||
|};
|
||||
|
||||
const ProductLimitedTimeOffer = ({
|
||||
visibleUntil,
|
||||
hideMinutesAndSeconds,
|
||||
alignCenter,
|
||||
}: Props) => {
|
||||
const [timeLeft, setTimeLeft] = React.useState<{|
|
||||
days: number,
|
||||
hours: number,
|
||||
minutes: number,
|
||||
seconds: number,
|
||||
|}>({ days: 0, hours: 0, minutes: 0, seconds: 0 });
|
||||
|
||||
React.useEffect(
|
||||
() => {
|
||||
const updateCountdown = () => {
|
||||
const now = new Date().getTime();
|
||||
const targetTime = new Date(visibleUntil).getTime();
|
||||
const difference = targetTime - now;
|
||||
|
||||
if (difference > 0) {
|
||||
setTimeLeft({
|
||||
days: Math.floor(difference / (1000 * 60 * 60 * 24)),
|
||||
hours: Math.floor(
|
||||
(difference % (1000 * 60 * 60 * 24)) / (1000 * 60 * 60)
|
||||
),
|
||||
minutes: Math.floor((difference % (1000 * 60 * 60)) / (1000 * 60)),
|
||||
seconds: Math.floor((difference % (1000 * 60)) / 1000),
|
||||
});
|
||||
} else {
|
||||
setTimeLeft({ days: 0, hours: 0, minutes: 0, seconds: 0 });
|
||||
}
|
||||
};
|
||||
|
||||
updateCountdown();
|
||||
const interval = setInterval(updateCountdown, 1000);
|
||||
|
||||
return () => clearInterval(interval);
|
||||
},
|
||||
[visibleUntil]
|
||||
);
|
||||
|
||||
if (!timeLeft) return null;
|
||||
|
||||
return (
|
||||
<div style={styles.limitedTimeContainer}>
|
||||
<Column
|
||||
justifyContent="center"
|
||||
expand
|
||||
alignItems={alignCenter ? 'center' : undefined}
|
||||
>
|
||||
<Text>
|
||||
<Trans>Limited time offer:</Trans>
|
||||
</Text>
|
||||
<LineStackLayout alignItems="center" noMargin>
|
||||
<Text noMargin size="block-title">
|
||||
{String(timeLeft.days).padStart(2, '0')}
|
||||
</Text>
|
||||
<Text noMargin>
|
||||
<Trans>Days</Trans>
|
||||
</Text>
|
||||
<Spacer />
|
||||
<Text
|
||||
noMargin
|
||||
size="block-title"
|
||||
style={{
|
||||
fontVariantNumeric: 'tabular-nums',
|
||||
}}
|
||||
>
|
||||
{String(timeLeft.hours).padStart(2, '0')}
|
||||
</Text>
|
||||
<Text noMargin>
|
||||
<Trans>Hours</Trans>
|
||||
</Text>
|
||||
{!hideMinutesAndSeconds && (
|
||||
<LineStackLayout alignItems="center" noMargin>
|
||||
<Spacer />
|
||||
<Text
|
||||
noMargin
|
||||
size="block-title"
|
||||
style={{
|
||||
fontVariantNumeric: 'tabular-nums',
|
||||
}}
|
||||
>
|
||||
{String(timeLeft.minutes).padStart(2, '0')}
|
||||
</Text>
|
||||
<Text noMargin>
|
||||
<Trans>Minutes</Trans>
|
||||
</Text>
|
||||
<Spacer />
|
||||
<Text
|
||||
noMargin
|
||||
size="block-title"
|
||||
style={{
|
||||
fontVariantNumeric: 'tabular-nums',
|
||||
}}
|
||||
>
|
||||
{String(timeLeft.seconds).padStart(2, '0')}
|
||||
</Text>
|
||||
<Text noMargin>
|
||||
<Trans>Seconds</Trans>
|
||||
</Text>
|
||||
</LineStackLayout>
|
||||
)}
|
||||
</LineStackLayout>
|
||||
</Column>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ProductLimitedTimeOffer;
|
@@ -319,6 +319,7 @@ export const getProductsIncludedInBundleTiles = ({
|
||||
onPrivateGameTemplateOpen,
|
||||
onBundleOpen,
|
||||
onCourseOpen,
|
||||
discountedPrice,
|
||||
}: {|
|
||||
product: ?PrivateAssetPack | PrivateGameTemplate | Bundle | Course,
|
||||
productListingDatas: ?Array<
|
||||
@@ -343,6 +344,7 @@ export const getProductsIncludedInBundleTiles = ({
|
||||
) => void,
|
||||
onBundleOpen?: (bundleListingData: BundleListingData) => void,
|
||||
onCourseOpen?: (courseListingData: CourseListingData) => void,
|
||||
discountedPrice?: boolean,
|
||||
|}): ?Array<React.Node> => {
|
||||
if (!product || !productListingDatas) return null;
|
||||
|
||||
@@ -378,6 +380,7 @@ export const getProductsIncludedInBundleTiles = ({
|
||||
onPrivateGameTemplateOpen(includedProductListingData)
|
||||
}
|
||||
owned={isProductOwned}
|
||||
discountedPrice={discountedPrice}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -395,6 +398,7 @@ export const getProductsIncludedInBundleTiles = ({
|
||||
key={includedProductListingData.id}
|
||||
onSelect={() => onPrivateAssetPackOpen(includedProductListingData)}
|
||||
owned={isProductOwned}
|
||||
discountedPrice={discountedPrice}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -412,6 +416,7 @@ export const getProductsIncludedInBundleTiles = ({
|
||||
key={includedProductListingData.id}
|
||||
onSelect={() => onBundleOpen(includedProductListingData)}
|
||||
owned={isProductOwned}
|
||||
discountedPrice={discountedPrice}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -429,6 +434,7 @@ export const getProductsIncludedInBundleTiles = ({
|
||||
key={includedProductListingData.id}
|
||||
onSelect={() => onCourseOpen(includedProductListingData)}
|
||||
owned={isProductOwned}
|
||||
discountedPrice={discountedPrice}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -529,6 +535,7 @@ export const PurchaseProductButtons = <
|
||||
isAlreadyReceived,
|
||||
onClickBuy,
|
||||
onClickBuyWithCredits,
|
||||
customLabel,
|
||||
}: {|
|
||||
productListingData: T,
|
||||
selectedUsageType: string,
|
||||
@@ -538,6 +545,7 @@ export const PurchaseProductButtons = <
|
||||
isAlreadyReceived: boolean,
|
||||
onClickBuy: () => void | Promise<void>,
|
||||
onClickBuyWithCredits?: () => void | Promise<void>,
|
||||
customLabel?: React.Node,
|
||||
|}) => {
|
||||
const { authenticated } = React.useContext(AuthenticatedUserContext);
|
||||
const shouldUseOrSimulateAppStoreProduct =
|
||||
@@ -578,10 +586,13 @@ export const PurchaseProductButtons = <
|
||||
<LineStackLayout>
|
||||
<RaisedButton
|
||||
primary
|
||||
label={<Trans>Buy for {formattedProductPriceText}</Trans>}
|
||||
label={
|
||||
customLabel || <Trans>Buy for {formattedProductPriceText}</Trans>
|
||||
}
|
||||
onClick={onClickBuyWithCredits}
|
||||
id={`buy-${productType}-with-credits`}
|
||||
icon={<Coin fontSize="small" />}
|
||||
size="medium"
|
||||
/>
|
||||
{!isAlreadyReceived && !authenticated && (
|
||||
<Text size="body-small">
|
||||
@@ -592,21 +603,27 @@ export const PurchaseProductButtons = <
|
||||
)}
|
||||
</LineStackLayout>
|
||||
) : (
|
||||
<LineStackLayout>
|
||||
<LineStackLayout noMargin>
|
||||
{creditPrice && (
|
||||
<FlatButton
|
||||
primary
|
||||
label={<Trans>Buy for {creditPrice.amount} credits</Trans>}
|
||||
label={
|
||||
customLabel || <Trans>Buy for {creditPrice.amount} credits</Trans>
|
||||
}
|
||||
onClick={onClickBuyWithCredits}
|
||||
id={`buy-${productType}-with-credits`}
|
||||
leftIcon={<Coin fontSize="small" />}
|
||||
size="medium"
|
||||
/>
|
||||
)}
|
||||
<RaisedButton
|
||||
primary
|
||||
label={<Trans>Buy for {formattedProductPriceText}</Trans>}
|
||||
label={
|
||||
customLabel || <Trans>Buy for {formattedProductPriceText}</Trans>
|
||||
}
|
||||
onClick={onClickBuy}
|
||||
id={`buy-${productType}`}
|
||||
size="medium"
|
||||
/>
|
||||
</LineStackLayout>
|
||||
);
|
||||
|
@@ -38,6 +38,22 @@ const styles = {
|
||||
marginTop: -3,
|
||||
marginBottom: -1,
|
||||
},
|
||||
discountedPrice: { textDecoration: 'line-through', opacity: 0.7 },
|
||||
};
|
||||
|
||||
export const renderPriceFormatted = (
|
||||
priceInCents: number,
|
||||
currencyCode: 'USD' | 'EUR',
|
||||
i18n: I18nType
|
||||
): string => {
|
||||
const currencySymbol = currencyCode === 'USD' ? '$' : '€';
|
||||
// Use a non-breaking space to ensure the currency stays next to the price.
|
||||
return `${currencySymbol}\u00A0${i18n
|
||||
.number(priceInCents / 100, {
|
||||
minimumFractionDigits: 2,
|
||||
maximumFractionDigits: 2,
|
||||
})
|
||||
.replace(/\D00$/, '')}`;
|
||||
};
|
||||
|
||||
type FormatProps = {|
|
||||
@@ -51,6 +67,7 @@ type FormatProps = {|
|
||||
usageType?: string,
|
||||
plainText?: boolean,
|
||||
showBothPrices?: 'column' | 'line', // If defined, will show both the credits price and the product price.
|
||||
discountedPrice?: boolean,
|
||||
|};
|
||||
|
||||
export const renderProductPrice = ({
|
||||
@@ -59,6 +76,7 @@ export const renderProductPrice = ({
|
||||
usageType,
|
||||
plainText,
|
||||
showBothPrices,
|
||||
discountedPrice,
|
||||
}: FormatProps): React.Node => {
|
||||
// For Credits packages & Bundles, on mobile, only show the app store product price.
|
||||
if (
|
||||
@@ -79,8 +97,9 @@ export const renderProductPrice = ({
|
||||
? creditPrices[0]
|
||||
: null;
|
||||
|
||||
// If we're on mobile, only show credits prices for other packages.
|
||||
if (shouldUseAppStoreProduct()) {
|
||||
// If we're on mobile, only show credits prices for other products,
|
||||
// except if we're showing the discounted price.
|
||||
if (shouldUseAppStoreProduct() && !discountedPrice) {
|
||||
if (!creditPrice) return '';
|
||||
return plainText ? (
|
||||
i18n._(t`${creditPrice.amount} credits`)
|
||||
@@ -103,17 +122,15 @@ export const renderProductPrice = ({
|
||||
: null;
|
||||
if (!price) return '';
|
||||
|
||||
const currencyCode = price.currency === 'USD' ? '$' : '€';
|
||||
const formattedPrice = `${currencyCode} ${i18n
|
||||
.number(price.value / 100, {
|
||||
minimumFractionDigits: 2,
|
||||
maximumFractionDigits: 2,
|
||||
})
|
||||
.replace(/\D00$/, '')}`;
|
||||
const formattedPrice = renderPriceFormatted(
|
||||
price.value,
|
||||
price.currency,
|
||||
i18n
|
||||
);
|
||||
|
||||
return plainText ? (
|
||||
formattedPrice
|
||||
) : showBothPrices && creditPrice ? (
|
||||
) : showBothPrices && creditPrice && !discountedPrice ? (
|
||||
showBothPrices === 'column' ? (
|
||||
<Column alignItems="flex-end">
|
||||
<div style={styles.creditPriceContainer}>
|
||||
@@ -149,7 +166,11 @@ export const renderProductPrice = ({
|
||||
)
|
||||
) : (
|
||||
<Text noMargin size="sub-title" color="inherit">
|
||||
{formattedPrice}
|
||||
{discountedPrice ? (
|
||||
<span style={styles.discountedPrice}>{formattedPrice}</span>
|
||||
) : (
|
||||
formattedPrice
|
||||
)}
|
||||
</Text>
|
||||
);
|
||||
};
|
||||
@@ -165,6 +186,7 @@ type ProductPriceOrOwnedProps = {|
|
||||
usageType?: string,
|
||||
owned?: boolean,
|
||||
showBothPrices?: 'column' | 'line',
|
||||
discountedPrice?: boolean,
|
||||
|};
|
||||
|
||||
export const OwnedLabel = () => {
|
||||
@@ -189,11 +211,18 @@ export const getProductPriceOrOwnedLabel = ({
|
||||
usageType,
|
||||
owned,
|
||||
showBothPrices,
|
||||
discountedPrice,
|
||||
}: ProductPriceOrOwnedProps): React.Node => {
|
||||
return owned ? (
|
||||
<OwnedLabel />
|
||||
) : (
|
||||
renderProductPrice({ i18n, productListingData, usageType, showBothPrices })
|
||||
renderProductPrice({
|
||||
i18n,
|
||||
productListingData,
|
||||
usageType,
|
||||
showBothPrices,
|
||||
discountedPrice,
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
@@ -211,6 +240,7 @@ type ProductPriceTagProps = {|
|
||||
*/
|
||||
withOverlay?: boolean,
|
||||
owned?: boolean,
|
||||
discountedPrice?: boolean,
|
||||
|};
|
||||
|
||||
const ProductPriceTag = ({
|
||||
@@ -218,6 +248,7 @@ const ProductPriceTag = ({
|
||||
usageType,
|
||||
withOverlay,
|
||||
owned,
|
||||
discountedPrice,
|
||||
}: ProductPriceTagProps) => {
|
||||
return (
|
||||
<I18n>
|
||||
@@ -227,6 +258,7 @@ const ProductPriceTag = ({
|
||||
productListingData,
|
||||
usageType,
|
||||
owned,
|
||||
discountedPrice,
|
||||
});
|
||||
|
||||
return <PriceTag withOverlay={withOverlay} label={label} />;
|
||||
|
@@ -1,52 +1,23 @@
|
||||
// @flow
|
||||
import React from 'react';
|
||||
import { LineStackLayout } from '../../UI/Layout';
|
||||
import Visa from '../../UI/CustomSvgIcons/Visa';
|
||||
import MasterCard from '../../UI/CustomSvgIcons/MasterCard';
|
||||
import Paypal from '../../UI/CustomSvgIcons/Paypal';
|
||||
import Text from '../../UI/Text';
|
||||
import { Trans } from '@lingui/macro';
|
||||
import GDevelopThemeContext from '../../UI/Theme/GDevelopThemeContext';
|
||||
|
||||
const styles = {
|
||||
logoContainer: {
|
||||
display: 'flex',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
padding: '4px 8px',
|
||||
borderRadius: 8,
|
||||
},
|
||||
};
|
||||
|
||||
const LogoContainer = ({ children }) => {
|
||||
const gdevelopTheme = React.useContext(GDevelopThemeContext);
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
...styles.logoContainer,
|
||||
background: gdevelopTheme.palette.secondary,
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
import ShieldChecked from '../../UI/CustomSvgIcons/ShieldChecked';
|
||||
|
||||
const SecureCheckout = () => {
|
||||
const gdevelopTheme = React.useContext(GDevelopThemeContext);
|
||||
return (
|
||||
<LineStackLayout>
|
||||
<Text>
|
||||
<Trans>Secure Checkout:</Trans>
|
||||
<LineStackLayout noMargin alignItems="center">
|
||||
<ShieldChecked style={{ color: gdevelopTheme.message.valid }} />
|
||||
<Text color="secondary">
|
||||
<Trans>Paypal secure</Trans>
|
||||
</Text>
|
||||
<ShieldChecked style={{ color: gdevelopTheme.message.valid }} />
|
||||
<Text color="secondary">
|
||||
<Trans>Stripe secure</Trans>
|
||||
</Text>
|
||||
<LogoContainer>
|
||||
<Paypal />
|
||||
</LogoContainer>
|
||||
<LogoContainer>
|
||||
<Visa />
|
||||
</LogoContainer>
|
||||
<LogoContainer>
|
||||
<MasterCard />
|
||||
</LogoContainer>
|
||||
</LineStackLayout>
|
||||
);
|
||||
};
|
||||
|
@@ -244,6 +244,7 @@ export const PrivateAssetPackTile = ({
|
||||
style,
|
||||
owned,
|
||||
disabled,
|
||||
discountedPrice,
|
||||
}: {|
|
||||
assetPackListingData: PrivateAssetPackListingData,
|
||||
onSelect: () => void,
|
||||
@@ -251,6 +252,7 @@ export const PrivateAssetPackTile = ({
|
||||
style?: any,
|
||||
owned: boolean,
|
||||
disabled?: boolean,
|
||||
discountedPrice?: boolean,
|
||||
|}) => {
|
||||
const gdevelopTheme = React.useContext(GDevelopThemeContext);
|
||||
return (
|
||||
@@ -291,6 +293,7 @@ export const PrivateAssetPackTile = ({
|
||||
productListingData={assetPackListingData}
|
||||
withOverlay
|
||||
owned={owned}
|
||||
discountedPrice={discountedPrice}
|
||||
/>
|
||||
</div>
|
||||
<Column>
|
||||
@@ -464,6 +467,7 @@ export const PrivateGameTemplateTile = ({
|
||||
style,
|
||||
owned,
|
||||
disabled,
|
||||
discountedPrice,
|
||||
}: {|
|
||||
privateGameTemplateListingData: PrivateGameTemplateListingData,
|
||||
onSelect: () => void,
|
||||
@@ -471,6 +475,7 @@ export const PrivateGameTemplateTile = ({
|
||||
style?: any,
|
||||
owned: boolean,
|
||||
disabled?: boolean,
|
||||
discountedPrice?: boolean,
|
||||
|}) => {
|
||||
const { isMobile } = useResponsiveWindowSize();
|
||||
const gdevelopTheme = React.useContext(GDevelopThemeContext);
|
||||
@@ -500,6 +505,7 @@ export const PrivateGameTemplateTile = ({
|
||||
productListingData={privateGameTemplateListingData}
|
||||
withOverlay
|
||||
owned={owned}
|
||||
discountedPrice={discountedPrice}
|
||||
/>
|
||||
</div>
|
||||
<Column>
|
||||
@@ -522,6 +528,7 @@ export const CourseTile = ({
|
||||
style,
|
||||
owned,
|
||||
disabled,
|
||||
discountedPrice,
|
||||
}: {|
|
||||
courseListingData: CourseListingData,
|
||||
onSelect: () => void,
|
||||
@@ -529,6 +536,7 @@ export const CourseTile = ({
|
||||
style?: any,
|
||||
owned: boolean,
|
||||
disabled?: boolean,
|
||||
discountedPrice?: boolean,
|
||||
|}) => {
|
||||
const { isMobile } = useResponsiveWindowSize();
|
||||
const gdevelopTheme = React.useContext(GDevelopThemeContext);
|
||||
@@ -556,6 +564,7 @@ export const CourseTile = ({
|
||||
productListingData={courseListingData}
|
||||
withOverlay
|
||||
owned={owned}
|
||||
discountedPrice={discountedPrice}
|
||||
/>
|
||||
</div>
|
||||
<Column>
|
||||
@@ -578,6 +587,7 @@ export const BundleTile = ({
|
||||
style,
|
||||
owned,
|
||||
disabled,
|
||||
discountedPrice,
|
||||
}: {|
|
||||
bundleListingData: BundleListingData,
|
||||
onSelect: () => void,
|
||||
@@ -585,6 +595,7 @@ export const BundleTile = ({
|
||||
style?: any,
|
||||
owned: boolean,
|
||||
disabled?: boolean,
|
||||
discountedPrice?: boolean,
|
||||
|}) => {
|
||||
const { isMobile } = useResponsiveWindowSize();
|
||||
const gdevelopTheme = React.useContext(GDevelopThemeContext);
|
||||
@@ -616,6 +627,7 @@ export const BundleTile = ({
|
||||
productListingData={bundleListingData}
|
||||
withOverlay
|
||||
owned={owned}
|
||||
discountedPrice={discountedPrice}
|
||||
/>
|
||||
) : (
|
||||
<OwnedLabel />
|
||||
|
@@ -1,5 +1,6 @@
|
||||
// @flow
|
||||
import * as React from 'react';
|
||||
import { I18n } from '@lingui/react';
|
||||
import { t, Trans } from '@lingui/macro';
|
||||
import ChevronArrowLeft from '../UI/CustomSvgIcons/ChevronArrowLeft';
|
||||
import Tune from '../UI/CustomSvgIcons/Tune';
|
||||
@@ -68,6 +69,8 @@ import { AssetSwappingAssetStoreSearchFilter } from './AssetStoreSearchFilter';
|
||||
import { delay } from '../Utils/Delay';
|
||||
import { BundleStoreContext } from './Bundles/BundleStoreContext';
|
||||
import BundleInformationPage from './Bundles/BundleInformationPage';
|
||||
import { type CourseCompletion } from '../MainFrame/EditorContainers/HomePage/UseCourses';
|
||||
import { type SubscriptionPlanWithPricingSystems } from '../Utils/GDevelopServices/Usage';
|
||||
|
||||
type Props = {|
|
||||
onlyShowAssets?: boolean, // TODO: if we add more options, use an array instead.
|
||||
@@ -76,8 +79,11 @@ type Props = {|
|
||||
privateGameTemplateListingData: PrivateGameTemplateListingData
|
||||
) => void,
|
||||
onOpenProfile?: () => void,
|
||||
courses?: ?Array<Course>,
|
||||
receivedCourses?: ?Array<Course>,
|
||||
onCourseOpen?: (courseId: string) => void,
|
||||
getSubscriptionPlansWithPricingSystems?: () => Array<SubscriptionPlanWithPricingSystems> | null,
|
||||
getCourseCompletion?: (courseId: string) => CourseCompletion | null,
|
||||
assetSwappedObject?: ?gdObject,
|
||||
minimalUI?: boolean,
|
||||
|};
|
||||
@@ -118,8 +124,11 @@ export const AssetStore = React.forwardRef<Props, AssetStoreInterface>(
|
||||
displayPromotions,
|
||||
onOpenPrivateGameTemplateListingData,
|
||||
onOpenProfile,
|
||||
courses,
|
||||
receivedCourses,
|
||||
onCourseOpen,
|
||||
getSubscriptionPlansWithPricingSystems,
|
||||
getCourseCompletion,
|
||||
assetSwappedObject,
|
||||
minimalUI,
|
||||
}: Props,
|
||||
@@ -660,291 +669,323 @@ export const AssetStore = React.forwardRef<Props, AssetStoreInterface>(
|
||||
]
|
||||
);
|
||||
|
||||
const onBack = React.useCallback(
|
||||
async () => {
|
||||
const page = shopNavigationState.backToPreviousPage();
|
||||
const isUpdatingSearchtext = reApplySearchTextIfNeeded(page);
|
||||
if (isUpdatingSearchtext) {
|
||||
// Updating the search is not instant, so we cannot apply the scroll position
|
||||
// right away. We force a wait as there's no easy way to know when results are completely updated.
|
||||
await delay(500);
|
||||
setScrollUpdateIsNeeded(page);
|
||||
applyBackScrollPosition(page); // We apply it manually, because the layout effect won't be called again.
|
||||
} else {
|
||||
setScrollUpdateIsNeeded(page);
|
||||
}
|
||||
},
|
||||
[
|
||||
shopNavigationState,
|
||||
reApplySearchTextIfNeeded,
|
||||
setScrollUpdateIsNeeded,
|
||||
applyBackScrollPosition,
|
||||
]
|
||||
);
|
||||
|
||||
return (
|
||||
<Column expand noMargin useFullHeight noOverflowParent id="asset-store">
|
||||
<>
|
||||
<LineStackLayout>
|
||||
{!(assetSwappedObject && minimalUI) && (
|
||||
<IconButton
|
||||
id="home-button"
|
||||
key="back-discover"
|
||||
tooltip={t`Back to discover`}
|
||||
onClick={() => {
|
||||
setSearchText('');
|
||||
const page = assetSwappedObject
|
||||
? shopNavigationState.openAssetSwapping()
|
||||
: shopNavigationState.openHome();
|
||||
setScrollUpdateIsNeeded(page);
|
||||
clearAllAssetStoreFilters();
|
||||
setIsFiltersPanelOpen(false);
|
||||
}}
|
||||
size="small"
|
||||
>
|
||||
<Home />
|
||||
</IconButton>
|
||||
)}
|
||||
<Column expand useFullHeight noMargin>
|
||||
<SearchBar
|
||||
placeholder={
|
||||
onlyShowAssets ? t`Search assets` : t`Search the shop`
|
||||
}
|
||||
value={searchText}
|
||||
onChange={(newValue: string) => {
|
||||
if (searchText === newValue || newValue.length === 1) {
|
||||
return;
|
||||
}
|
||||
setSearchText(newValue);
|
||||
if (isOnSearchResultPage) {
|
||||
// An existing search is already being done: just move to the
|
||||
// top search results.
|
||||
shopNavigationState.openSearchResultPage();
|
||||
const assetsListInterface = assetsList.current;
|
||||
if (assetsListInterface) {
|
||||
assetsListInterface.scrollToPosition(0);
|
||||
assetsListInterface.setPageBreakIndex(0);
|
||||
<I18n>
|
||||
{({ i18n }) => (
|
||||
<Column
|
||||
expand
|
||||
noMargin
|
||||
useFullHeight
|
||||
noOverflowParent
|
||||
id="asset-store"
|
||||
>
|
||||
<>
|
||||
<LineStackLayout>
|
||||
{!(assetSwappedObject && minimalUI) && (
|
||||
<IconButton
|
||||
id="home-button"
|
||||
key="back-discover"
|
||||
tooltip={t`Back to discover`}
|
||||
onClick={() => {
|
||||
setSearchText('');
|
||||
const page = assetSwappedObject
|
||||
? shopNavigationState.openAssetSwapping()
|
||||
: shopNavigationState.openHome();
|
||||
setScrollUpdateIsNeeded(page);
|
||||
clearAllAssetStoreFilters();
|
||||
setIsFiltersPanelOpen(false);
|
||||
}}
|
||||
size="small"
|
||||
>
|
||||
<Home />
|
||||
</IconButton>
|
||||
)}
|
||||
<Column expand useFullHeight noMargin>
|
||||
<SearchBar
|
||||
placeholder={
|
||||
onlyShowAssets ? t`Search assets` : t`Search the shop`
|
||||
}
|
||||
} else {
|
||||
// A new search is being initiated: navigate to the search page,
|
||||
// and clear the history as a new search was launched.
|
||||
if (!!newValue) {
|
||||
shopNavigationState.clearHistory();
|
||||
shopNavigationState.openSearchResultPage();
|
||||
openFiltersPanelIfAppropriate();
|
||||
}
|
||||
}
|
||||
}}
|
||||
onRequestSearch={() => {}}
|
||||
ref={searchBar}
|
||||
id="asset-store-search-bar"
|
||||
/>
|
||||
</Column>
|
||||
{!(assetSwappedObject && minimalUI) && (
|
||||
<IconButton
|
||||
onClick={() => setIsFiltersPanelOpen(!isFiltersPanelOpen)}
|
||||
disabled={!canShowFiltersPanel}
|
||||
selected={canShowFiltersPanel && isFiltersPanelOpen}
|
||||
size="small"
|
||||
>
|
||||
<Tune />
|
||||
</IconButton>
|
||||
)}
|
||||
</LineStackLayout>
|
||||
<Spacer />
|
||||
</>
|
||||
<Column noMargin>
|
||||
<Line justifyContent="space-between" noMargin alignItems="center">
|
||||
{(!isOnHomePage || !!openedShopCategory) &&
|
||||
!(assetSwappedObject && minimalUI) && (
|
||||
<>
|
||||
{shopNavigationState.isRootPage ? null : (
|
||||
<Column expand alignItems="flex-start" noMargin>
|
||||
<TextButton
|
||||
icon={<ChevronArrowLeft />}
|
||||
label={<Trans>Back</Trans>}
|
||||
onClick={async () => {
|
||||
const page = shopNavigationState.backToPreviousPage();
|
||||
const isUpdatingSearchtext = reApplySearchTextIfNeeded(
|
||||
page
|
||||
);
|
||||
if (isUpdatingSearchtext) {
|
||||
// Updating the search is not instant, so we cannot apply the scroll position
|
||||
// right away. We force a wait as there's no easy way to know when results are completely updated.
|
||||
await delay(500);
|
||||
setScrollUpdateIsNeeded(page);
|
||||
applyBackScrollPosition(page); // We apply it manually, because the layout effect won't be called again.
|
||||
} else {
|
||||
setScrollUpdateIsNeeded(page);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</Column>
|
||||
)}
|
||||
{(openedAssetPack ||
|
||||
openedPrivateAssetPackListingData ||
|
||||
filtersState.chosenCategory) && (
|
||||
value={searchText}
|
||||
onChange={(newValue: string) => {
|
||||
if (searchText === newValue || newValue.length === 1) {
|
||||
return;
|
||||
}
|
||||
setSearchText(newValue);
|
||||
if (isOnSearchResultPage) {
|
||||
// An existing search is already being done: just move to the
|
||||
// top search results.
|
||||
shopNavigationState.openSearchResultPage();
|
||||
const assetsListInterface = assetsList.current;
|
||||
if (assetsListInterface) {
|
||||
assetsListInterface.scrollToPosition(0);
|
||||
assetsListInterface.setPageBreakIndex(0);
|
||||
}
|
||||
} else {
|
||||
// A new search is being initiated: navigate to the search page,
|
||||
// and clear the history as a new search was launched.
|
||||
if (!!newValue) {
|
||||
shopNavigationState.clearHistory();
|
||||
shopNavigationState.openSearchResultPage();
|
||||
openFiltersPanelIfAppropriate();
|
||||
}
|
||||
}
|
||||
}}
|
||||
onRequestSearch={() => {}}
|
||||
ref={searchBar}
|
||||
id="asset-store-search-bar"
|
||||
/>
|
||||
</Column>
|
||||
{!(assetSwappedObject && minimalUI) && (
|
||||
<IconButton
|
||||
onClick={() => setIsFiltersPanelOpen(!isFiltersPanelOpen)}
|
||||
disabled={!canShowFiltersPanel}
|
||||
selected={canShowFiltersPanel && isFiltersPanelOpen}
|
||||
size="small"
|
||||
>
|
||||
<Tune />
|
||||
</IconButton>
|
||||
)}
|
||||
</LineStackLayout>
|
||||
<Spacer />
|
||||
</>
|
||||
<Column noMargin>
|
||||
<Line justifyContent="space-between" noMargin alignItems="center">
|
||||
{(!isOnHomePage || !!openedShopCategory) &&
|
||||
!(assetSwappedObject && minimalUI) && (
|
||||
<>
|
||||
{!openedAssetPack && !openedPrivateAssetPackListingData && (
|
||||
// Only show the category name if we're not on an asset pack page.
|
||||
<Column expand alignItems="center">
|
||||
<Text size="block-title" noMargin>
|
||||
{filtersState.chosenCategory
|
||||
? capitalize(
|
||||
filtersState.chosenCategory.node.name
|
||||
)
|
||||
: ''}
|
||||
</Text>
|
||||
{shopNavigationState.isRootPage ||
|
||||
// Don't show back action on bundle pages, as it's handled by the page itself.
|
||||
openedBundleListingData ? null : (
|
||||
<Column expand alignItems="flex-start" noMargin>
|
||||
<TextButton
|
||||
icon={<ChevronArrowLeft />}
|
||||
label={<Trans>Back</Trans>}
|
||||
onClick={onBack}
|
||||
/>
|
||||
</Column>
|
||||
)}
|
||||
<Column
|
||||
expand
|
||||
alignItems="flex-end"
|
||||
noMargin
|
||||
justifyContent="center"
|
||||
>
|
||||
{openedAssetPack &&
|
||||
openedAssetPack.content &&
|
||||
doesAssetPackContainAudio(openedAssetPack) &&
|
||||
!isAssetPackAudioOnly(openedAssetPack) ? (
|
||||
<PrivateAssetPackAudioFilesDownloadButton
|
||||
assetPack={openedAssetPack}
|
||||
/>
|
||||
) : null}
|
||||
</Column>
|
||||
{(openedAssetPack ||
|
||||
openedPrivateAssetPackListingData ||
|
||||
filtersState.chosenCategory) && (
|
||||
<>
|
||||
{!openedAssetPack &&
|
||||
!openedPrivateAssetPackListingData && (
|
||||
// Only show the category name if we're not on an asset pack page.
|
||||
<Column expand alignItems="center">
|
||||
<Text size="block-title" noMargin>
|
||||
{filtersState.chosenCategory
|
||||
? capitalize(
|
||||
filtersState.chosenCategory.node.name
|
||||
)
|
||||
: ''}
|
||||
</Text>
|
||||
</Column>
|
||||
)}
|
||||
<Column
|
||||
expand
|
||||
alignItems="flex-end"
|
||||
noMargin
|
||||
justifyContent="center"
|
||||
>
|
||||
{openedAssetPack &&
|
||||
openedAssetPack.content &&
|
||||
doesAssetPackContainAudio(openedAssetPack) &&
|
||||
!isAssetPackAudioOnly(openedAssetPack) ? (
|
||||
<PrivateAssetPackAudioFilesDownloadButton
|
||||
assetPack={openedAssetPack}
|
||||
/>
|
||||
) : null}
|
||||
</Column>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</Line>
|
||||
</Column>
|
||||
<Line
|
||||
expand
|
||||
noMargin
|
||||
overflow={
|
||||
'hidden' /* Somehow required on Chrome/Firefox to avoid children growing (but not on Safari) */
|
||||
}
|
||||
>
|
||||
{isOnHomePage ? (
|
||||
storeError ? (
|
||||
<PlaceholderError onRetry={fetchAssetsAndGameTemplates}>
|
||||
<AlertMessage kind="error">
|
||||
<Trans>
|
||||
An error occurred when fetching the store content. Please
|
||||
try again later.
|
||||
</Trans>
|
||||
</AlertMessage>
|
||||
</PlaceholderError>
|
||||
) : publicAssetPacks &&
|
||||
privateAssetPackListingDatas &&
|
||||
privateGameTemplateListingDatas &&
|
||||
bundleListingDatas ? (
|
||||
<AssetsHome
|
||||
ref={assetsHome}
|
||||
publicAssetPacks={publicAssetPacks}
|
||||
privateAssetPackListingDatas={privateAssetPackListingDatas}
|
||||
privateGameTemplateListingDatas={
|
||||
privateGameTemplateListingDatas
|
||||
}
|
||||
bundleListingDatas={bundleListingDatas}
|
||||
onPublicAssetPackSelection={selectPublicAssetPack}
|
||||
onPrivateAssetPackSelection={selectPrivateAssetPack}
|
||||
onPrivateGameTemplateSelection={selectPrivateGameTemplate}
|
||||
onBundleSelection={selectBundle}
|
||||
onCategorySelection={selectShopCategory}
|
||||
openedShopCategory={openedShopCategory}
|
||||
onlyShowAssets={onlyShowAssets}
|
||||
displayPromotions={displayPromotions}
|
||||
onOpenProfile={onOpenProfile}
|
||||
/>
|
||||
) : (
|
||||
<PlaceholderLoader />
|
||||
)
|
||||
) : isOnSearchResultPage ? (
|
||||
<AssetsList
|
||||
publicAssetPacks={
|
||||
assetSwappedObject ? [] : publicAssetPacksSearchResults
|
||||
</Line>
|
||||
</Column>
|
||||
<Line
|
||||
expand
|
||||
noMargin
|
||||
overflow={
|
||||
'hidden' /* Somehow required on Chrome/Firefox to avoid children growing (but not on Safari) */
|
||||
}
|
||||
privateAssetPackListingDatas={
|
||||
assetSwappedObject
|
||||
? []
|
||||
: privateAssetPackListingDatasSearchResults
|
||||
}
|
||||
privateGameTemplateListingDatas={
|
||||
assetSwappedObject
|
||||
? []
|
||||
: privateGameTemplateListingDatasSearchResults
|
||||
}
|
||||
bundleListingDatas={
|
||||
assetSwappedObject ? [] : bundleListingDatasSearchResults
|
||||
}
|
||||
assetShortHeaders={assetShortHeadersSearchResults}
|
||||
ref={assetsList}
|
||||
error={storeError}
|
||||
onOpenDetails={onOpenDetails}
|
||||
onPrivateAssetPackSelection={selectPrivateAssetPack}
|
||||
onPublicAssetPackSelection={selectPublicAssetPack}
|
||||
onPrivateGameTemplateSelection={selectPrivateGameTemplate}
|
||||
onBundleSelection={selectBundle}
|
||||
onFolderSelection={selectFolder}
|
||||
onGoBackToFolderIndex={goBackToFolderIndex}
|
||||
currentPage={shopNavigationState.getCurrentPage()}
|
||||
onlyShowAssets={onlyShowAssets}
|
||||
hideDetails={!!assetSwappedObject && !!minimalUI}
|
||||
/>
|
||||
) : // Do not show the asset details if we're swapping an asset.
|
||||
openedAssetShortHeader && !(assetSwappedObject && minimalUI) ? (
|
||||
<AssetDetails
|
||||
ref={assetDetails}
|
||||
onTagSelection={selectTag}
|
||||
assetShortHeader={openedAssetShortHeader}
|
||||
onOpenDetails={onOpenDetails}
|
||||
onAssetLoaded={() => applyBackScrollPosition(currentPage)}
|
||||
onPrivateAssetPackSelection={selectPrivateAssetPack}
|
||||
onPrivateGameTemplateSelection={selectPrivateGameTemplate}
|
||||
/>
|
||||
) : !!openedPrivateAssetPackListingData ? (
|
||||
<PrivateAssetPackInformationPage
|
||||
privateAssetPackListingData={openedPrivateAssetPackListingData}
|
||||
onAssetPackOpen={selectPrivateAssetPack}
|
||||
onGameTemplateOpen={selectPrivateGameTemplate}
|
||||
onBundleOpen={selectBundle}
|
||||
privateAssetPackListingDatasFromSameCreator={
|
||||
privateAssetPackListingDatasFromSameCreator
|
||||
}
|
||||
/>
|
||||
) : !!openedPrivateGameTemplateListingData ? (
|
||||
<PrivateGameTemplateInformationPage
|
||||
privateGameTemplateListingData={
|
||||
openedPrivateGameTemplateListingData
|
||||
}
|
||||
onCreateWithGameTemplate={() => {
|
||||
onOpenPrivateGameTemplateListingData &&
|
||||
onOpenPrivateGameTemplateListingData(
|
||||
openedPrivateGameTemplateListingData
|
||||
);
|
||||
}}
|
||||
onAssetPackOpen={selectPrivateAssetPack}
|
||||
onGameTemplateOpen={selectPrivateGameTemplate}
|
||||
onBundleOpen={selectBundle}
|
||||
privateGameTemplateListingDatasFromSameCreator={
|
||||
privateGameTemplateListingDatasFromSameCreator
|
||||
}
|
||||
/>
|
||||
) : !!openedBundleListingData ? (
|
||||
<BundleInformationPage
|
||||
bundleListingData={openedBundleListingData}
|
||||
receivedCourses={receivedCourses}
|
||||
onBundleOpen={selectBundle}
|
||||
onGameTemplateOpen={selectPrivateGameTemplate}
|
||||
onAssetPackOpen={selectPrivateAssetPack}
|
||||
onCourseOpen={selectCourse}
|
||||
/>
|
||||
) : null}
|
||||
{canShowFiltersPanel && (
|
||||
<ResponsivePaperOrDrawer
|
||||
onClose={() => setIsFiltersPanelOpen(false)}
|
||||
open={isFiltersPanelOpen}
|
||||
>
|
||||
<ScrollView>
|
||||
<Column>
|
||||
<Column noMargin>
|
||||
<Line alignItems="center">
|
||||
<Tune />
|
||||
<Subheader>
|
||||
<Trans>Object filters</Trans>
|
||||
</Subheader>
|
||||
</Line>
|
||||
</Column>
|
||||
<Line justifyContent="space-between" alignItems="center">
|
||||
<AssetStoreFilterPanel
|
||||
assetSwappedObject={assetSwappedObject}
|
||||
/>
|
||||
</Line>
|
||||
</Column>
|
||||
</ScrollView>
|
||||
</ResponsivePaperOrDrawer>
|
||||
)}
|
||||
</Line>
|
||||
</Column>
|
||||
{isOnHomePage ? (
|
||||
storeError ? (
|
||||
<PlaceholderError onRetry={fetchAssetsAndGameTemplates}>
|
||||
<AlertMessage kind="error">
|
||||
<Trans>
|
||||
An error occurred when fetching the store content.
|
||||
Please try again later.
|
||||
</Trans>
|
||||
</AlertMessage>
|
||||
</PlaceholderError>
|
||||
) : publicAssetPacks &&
|
||||
privateAssetPackListingDatas &&
|
||||
privateGameTemplateListingDatas &&
|
||||
bundleListingDatas ? (
|
||||
<AssetsHome
|
||||
ref={assetsHome}
|
||||
publicAssetPacks={publicAssetPacks}
|
||||
privateAssetPackListingDatas={privateAssetPackListingDatas}
|
||||
privateGameTemplateListingDatas={
|
||||
privateGameTemplateListingDatas
|
||||
}
|
||||
bundleListingDatas={bundleListingDatas}
|
||||
onPublicAssetPackSelection={selectPublicAssetPack}
|
||||
onPrivateAssetPackSelection={selectPrivateAssetPack}
|
||||
onPrivateGameTemplateSelection={selectPrivateGameTemplate}
|
||||
onBundleSelection={selectBundle}
|
||||
onCategorySelection={selectShopCategory}
|
||||
openedShopCategory={openedShopCategory}
|
||||
onlyShowAssets={onlyShowAssets}
|
||||
displayPromotions={displayPromotions}
|
||||
onOpenProfile={onOpenProfile}
|
||||
/>
|
||||
) : (
|
||||
<PlaceholderLoader />
|
||||
)
|
||||
) : isOnSearchResultPage ? (
|
||||
<AssetsList
|
||||
publicAssetPacks={
|
||||
assetSwappedObject ? [] : publicAssetPacksSearchResults
|
||||
}
|
||||
privateAssetPackListingDatas={
|
||||
assetSwappedObject
|
||||
? []
|
||||
: privateAssetPackListingDatasSearchResults
|
||||
}
|
||||
privateGameTemplateListingDatas={
|
||||
assetSwappedObject
|
||||
? []
|
||||
: privateGameTemplateListingDatasSearchResults
|
||||
}
|
||||
bundleListingDatas={
|
||||
assetSwappedObject ? [] : bundleListingDatasSearchResults
|
||||
}
|
||||
assetShortHeaders={assetShortHeadersSearchResults}
|
||||
ref={assetsList}
|
||||
error={storeError}
|
||||
onOpenDetails={onOpenDetails}
|
||||
onPrivateAssetPackSelection={selectPrivateAssetPack}
|
||||
onPublicAssetPackSelection={selectPublicAssetPack}
|
||||
onPrivateGameTemplateSelection={selectPrivateGameTemplate}
|
||||
onBundleSelection={selectBundle}
|
||||
onFolderSelection={selectFolder}
|
||||
onGoBackToFolderIndex={goBackToFolderIndex}
|
||||
currentPage={shopNavigationState.getCurrentPage()}
|
||||
onlyShowAssets={onlyShowAssets}
|
||||
hideDetails={!!assetSwappedObject && !!minimalUI}
|
||||
/>
|
||||
) : // Do not show the asset details if we're swapping an asset.
|
||||
openedAssetShortHeader && !(assetSwappedObject && minimalUI) ? (
|
||||
<AssetDetails
|
||||
ref={assetDetails}
|
||||
onTagSelection={selectTag}
|
||||
assetShortHeader={openedAssetShortHeader}
|
||||
onOpenDetails={onOpenDetails}
|
||||
onAssetLoaded={() => applyBackScrollPosition(currentPage)}
|
||||
onPrivateAssetPackSelection={selectPrivateAssetPack}
|
||||
onPrivateGameTemplateSelection={selectPrivateGameTemplate}
|
||||
/>
|
||||
) : !!openedPrivateAssetPackListingData ? (
|
||||
<PrivateAssetPackInformationPage
|
||||
privateAssetPackListingData={
|
||||
openedPrivateAssetPackListingData
|
||||
}
|
||||
onAssetPackOpen={selectPrivateAssetPack}
|
||||
onGameTemplateOpen={selectPrivateGameTemplate}
|
||||
onBundleOpen={selectBundle}
|
||||
privateAssetPackListingDatasFromSameCreator={
|
||||
privateAssetPackListingDatasFromSameCreator
|
||||
}
|
||||
/>
|
||||
) : !!openedPrivateGameTemplateListingData ? (
|
||||
<PrivateGameTemplateInformationPage
|
||||
privateGameTemplateListingData={
|
||||
openedPrivateGameTemplateListingData
|
||||
}
|
||||
onCreateWithGameTemplate={() => {
|
||||
onOpenPrivateGameTemplateListingData &&
|
||||
onOpenPrivateGameTemplateListingData(
|
||||
openedPrivateGameTemplateListingData
|
||||
);
|
||||
}}
|
||||
onAssetPackOpen={selectPrivateAssetPack}
|
||||
onGameTemplateOpen={selectPrivateGameTemplate}
|
||||
onBundleOpen={selectBundle}
|
||||
privateGameTemplateListingDatasFromSameCreator={
|
||||
privateGameTemplateListingDatasFromSameCreator
|
||||
}
|
||||
/>
|
||||
) : !!openedBundleListingData &&
|
||||
getSubscriptionPlansWithPricingSystems &&
|
||||
getCourseCompletion ? (
|
||||
<BundleInformationPage
|
||||
bundleListingData={openedBundleListingData}
|
||||
noPadding
|
||||
onBack={onBack}
|
||||
onBundleOpen={selectBundle}
|
||||
onGameTemplateOpen={selectPrivateGameTemplate}
|
||||
onAssetPackOpen={selectPrivateAssetPack}
|
||||
onCourseOpen={selectCourse}
|
||||
getSubscriptionPlansWithPricingSystems={
|
||||
getSubscriptionPlansWithPricingSystems
|
||||
}
|
||||
courses={courses}
|
||||
receivedCourses={receivedCourses}
|
||||
getCourseCompletion={getCourseCompletion}
|
||||
/>
|
||||
) : null}
|
||||
{canShowFiltersPanel && (
|
||||
<ResponsivePaperOrDrawer
|
||||
onClose={() => setIsFiltersPanelOpen(false)}
|
||||
open={isFiltersPanelOpen}
|
||||
>
|
||||
<ScrollView>
|
||||
<Column>
|
||||
<Column noMargin>
|
||||
<Line alignItems="center">
|
||||
<Tune />
|
||||
<Subheader>
|
||||
<Trans>Object filters</Trans>
|
||||
</Subheader>
|
||||
</Line>
|
||||
</Column>
|
||||
<Line justifyContent="space-between" alignItems="center">
|
||||
<AssetStoreFilterPanel
|
||||
assetSwappedObject={assetSwappedObject}
|
||||
/>
|
||||
</Line>
|
||||
</Column>
|
||||
</ScrollView>
|
||||
</ResponsivePaperOrDrawer>
|
||||
)}
|
||||
</Line>
|
||||
</Column>
|
||||
)}
|
||||
</I18n>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
@@ -29,6 +29,7 @@ type Props = {|
|
||||
eventsFunctionsExtension: gdEventsFunctionsExtension | null,
|
||||
objectType: string,
|
||||
objectBehaviorsTypes: Array<string>,
|
||||
isChildObject: boolean,
|
||||
open: boolean,
|
||||
onClose: () => void,
|
||||
onChoose: (type: string, defaultName: string) => void,
|
||||
@@ -43,6 +44,7 @@ export default function NewBehaviorDialog({
|
||||
onChoose,
|
||||
objectType,
|
||||
objectBehaviorsTypes,
|
||||
isChildObject,
|
||||
onExtensionInstalled,
|
||||
}: Props) {
|
||||
const [isInstalling, setIsInstalling] = React.useState(false);
|
||||
@@ -224,6 +226,7 @@ export default function NewBehaviorDialog({
|
||||
project={project}
|
||||
objectType={objectType}
|
||||
objectBehaviorsTypes={objectBehaviorsTypes}
|
||||
isChildObject={isChildObject}
|
||||
isInstalling={isInstalling}
|
||||
onInstall={async shortHeader =>
|
||||
onInstallExtension(i18n, shortHeader)
|
||||
|
@@ -71,6 +71,7 @@ type BehaviorConfigurationEditorProps = {|
|
||||
project: gdProject,
|
||||
object: gdObject,
|
||||
behavior: gdBehavior,
|
||||
isChildObject: boolean,
|
||||
resourceManagementProps: ResourceManagementProps,
|
||||
onBehaviorsUpdated: () => void,
|
||||
onChangeBehaviorName: (behavior: gdBehavior, newName: string) => void,
|
||||
@@ -94,6 +95,7 @@ const BehaviorConfigurationEditor = React.forwardRef<
|
||||
project,
|
||||
object,
|
||||
behavior,
|
||||
isChildObject,
|
||||
resourceManagementProps,
|
||||
onBehaviorsUpdated,
|
||||
onChangeBehaviorName,
|
||||
@@ -311,6 +313,7 @@ type UseManageBehaviorsState = {|
|
||||
export const useManageObjectBehaviors = ({
|
||||
project,
|
||||
object,
|
||||
isChildObject,
|
||||
eventsFunctionsExtension,
|
||||
onUpdate,
|
||||
onSizeUpdated,
|
||||
@@ -320,6 +323,7 @@ export const useManageObjectBehaviors = ({
|
||||
}: {
|
||||
project: gdProject,
|
||||
object: gdObject,
|
||||
isChildObject: boolean,
|
||||
eventsFunctionsExtension: gdEventsFunctionsExtension | null,
|
||||
onUpdate: () => void,
|
||||
onSizeUpdated?: ?() => void,
|
||||
@@ -586,6 +590,7 @@ export const useManageObjectBehaviors = ({
|
||||
open
|
||||
objectType={object.getType()}
|
||||
objectBehaviorsTypes={listObjectBehaviorsTypes(object)}
|
||||
isChildObject={isChildObject}
|
||||
onClose={() => setNewBehaviorDialogOpen(false)}
|
||||
onChoose={addBehavior}
|
||||
project={project}
|
||||
@@ -615,6 +620,7 @@ type Props = {|
|
||||
project: gdProject,
|
||||
eventsFunctionsExtension: gdEventsFunctionsExtension | null,
|
||||
object: gdObject,
|
||||
isChildObject: boolean,
|
||||
onUpdateBehaviorsSharedData: () => void,
|
||||
onSizeUpdated?: ?() => void,
|
||||
resourceManagementProps: ResourceManagementProps,
|
||||
@@ -636,6 +642,7 @@ const BehaviorsEditor = (props: Props) => {
|
||||
|
||||
const {
|
||||
object,
|
||||
isChildObject,
|
||||
project,
|
||||
eventsFunctionsExtension,
|
||||
onSizeUpdated,
|
||||
@@ -665,6 +672,7 @@ const BehaviorsEditor = (props: Props) => {
|
||||
} = useManageObjectBehaviors({
|
||||
project,
|
||||
object,
|
||||
isChildObject,
|
||||
eventsFunctionsExtension,
|
||||
onUpdate: forceUpdate,
|
||||
onSizeUpdated,
|
||||
@@ -785,6 +793,7 @@ const BehaviorsEditor = (props: Props) => {
|
||||
key={behaviorName}
|
||||
project={project}
|
||||
object={object}
|
||||
isChildObject={isChildObject}
|
||||
behavior={behavior}
|
||||
copyBehavior={copyBehavior}
|
||||
onRemoveBehavior={removeBehavior}
|
||||
|
102
newIDE/app/src/ChoicesEditor/index.js
Normal file
102
newIDE/app/src/ChoicesEditor/index.js
Normal file
@@ -0,0 +1,102 @@
|
||||
// @flow
|
||||
import { Trans } from '@lingui/macro';
|
||||
import { t } from '@lingui/macro';
|
||||
import * as React from 'react';
|
||||
import {
|
||||
ResponsiveLineStackLayout,
|
||||
LineStackLayout,
|
||||
ColumnStackLayout,
|
||||
} from '../UI/Layout';
|
||||
import { Line } from '../UI/Grid';
|
||||
import SemiControlledTextField from '../UI/SemiControlledTextField';
|
||||
import { IconButton } from '@material-ui/core';
|
||||
import RaisedButton from '../UI/RaisedButton';
|
||||
import Add from '../UI/CustomSvgIcons/Add';
|
||||
import Trash from '../UI/CustomSvgIcons/Trash';
|
||||
|
||||
export type Choice = {|
|
||||
value: string,
|
||||
label: string,
|
||||
|};
|
||||
|
||||
type ChoicesEditorProps = {|
|
||||
choices: Array<Choice>,
|
||||
setChoices: (Array<Choice>) => void,
|
||||
disabled?: boolean,
|
||||
hideLabels?: boolean,
|
||||
|};
|
||||
|
||||
const ChoicesEditor = ({
|
||||
choices,
|
||||
setChoices,
|
||||
disabled,
|
||||
hideLabels,
|
||||
}: ChoicesEditorProps) => {
|
||||
const updateChoices = () => setChoices(choices);
|
||||
|
||||
return (
|
||||
<ResponsiveLineStackLayout>
|
||||
<ColumnStackLayout justifyContent="flex-end" expand noMargin>
|
||||
{choices.map((item, index) => (
|
||||
<LineStackLayout
|
||||
key={index}
|
||||
justifyContent="flex-end"
|
||||
expand
|
||||
alignItems="center"
|
||||
noMargin
|
||||
>
|
||||
<SemiControlledTextField
|
||||
disabled={disabled}
|
||||
commitOnBlur
|
||||
floatingLabelText={<Trans>Value</Trans>}
|
||||
value={item.value}
|
||||
onChange={text => {
|
||||
choices[index].value = text;
|
||||
updateChoices();
|
||||
}}
|
||||
fullWidth
|
||||
/>
|
||||
{!hideLabels && (
|
||||
<SemiControlledTextField
|
||||
disabled={disabled}
|
||||
commitOnBlur
|
||||
floatingLabelText={<Trans>Label displayed in editor</Trans>}
|
||||
value={item.label}
|
||||
onChange={text => {
|
||||
choices[index].label = text;
|
||||
updateChoices();
|
||||
}}
|
||||
fullWidth
|
||||
/>
|
||||
)}
|
||||
<IconButton
|
||||
disabled={disabled}
|
||||
tooltip={t`Delete option`}
|
||||
onClick={() => {
|
||||
choices.splice(index, 1);
|
||||
updateChoices();
|
||||
}}
|
||||
>
|
||||
<Trash />
|
||||
</IconButton>
|
||||
</LineStackLayout>
|
||||
))}
|
||||
|
||||
<Line justifyContent="flex-end" expand>
|
||||
<RaisedButton
|
||||
disabled={disabled}
|
||||
primary
|
||||
onClick={() => {
|
||||
choices.push({ value: 'New Option', label: '' });
|
||||
updateChoices();
|
||||
}}
|
||||
label={<Trans>Add a new option</Trans>}
|
||||
icon={<Add />}
|
||||
/>
|
||||
</Line>
|
||||
</ColumnStackLayout>
|
||||
</ResponsiveLineStackLayout>
|
||||
);
|
||||
};
|
||||
|
||||
export default ChoicesEditor;
|
@@ -96,6 +96,48 @@ const parseVariablePath = (
|
||||
return segments;
|
||||
};
|
||||
|
||||
const parseValueAsObjectOrArray = (value: string): Array<any> | {} | null => {
|
||||
try {
|
||||
const parsedValue = JSON.parse(value);
|
||||
|
||||
if (Array.isArray(parsedValue) || typeof parsedValue === 'object') {
|
||||
return parsedValue;
|
||||
}
|
||||
|
||||
// Value is a primitive, not an object or array.
|
||||
return null;
|
||||
} catch (error) {
|
||||
// Not even a JSON string (probably a primitive or a string).
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
const convertJsObjectToVariable = (value: any, variable: gdVariable) => {
|
||||
if (value === null) {
|
||||
variable.setString('null');
|
||||
} else if (value === undefined) {
|
||||
variable.setString('undefined');
|
||||
} else if (typeof value === 'number') {
|
||||
variable.setValue(value || 0);
|
||||
} else if (typeof value === 'string') {
|
||||
variable.setString(value);
|
||||
} else if (typeof value === 'boolean') {
|
||||
variable.setBool(value);
|
||||
} else if (Array.isArray(value)) {
|
||||
variable.castTo('Array');
|
||||
variable.clearChildren();
|
||||
for (const item of value) {
|
||||
convertJsObjectToVariable(item, variable.pushNew());
|
||||
}
|
||||
} else if (typeof value === 'object') {
|
||||
variable.castTo('Structure');
|
||||
variable.clearChildren();
|
||||
for (const [key, item] of Object.entries(value)) {
|
||||
convertJsObjectToVariable(item, variable.getChild(key));
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export const applyVariableChange = ({
|
||||
variablePath,
|
||||
forcedVariableType,
|
||||
@@ -154,19 +196,35 @@ export const applyVariableChange = ({
|
||||
}
|
||||
}
|
||||
|
||||
const variableType = readOrInferVariableType(forcedVariableType, value);
|
||||
const arrayOrObjectValue = parseValueAsObjectOrArray(value);
|
||||
|
||||
if (variableType === 'String') {
|
||||
variable.setString(value);
|
||||
} else if (variableType === 'Number') {
|
||||
variable.setValue(parseFloat(value));
|
||||
} else if (variableType === 'Boolean') {
|
||||
variable.setBool(value.toLowerCase() === 'true');
|
||||
if (arrayOrObjectValue) {
|
||||
// Value is an object or array.
|
||||
convertJsObjectToVariable(arrayOrObjectValue, variable);
|
||||
|
||||
return {
|
||||
variable,
|
||||
variableType:
|
||||
variable.getType() === gd.Variable.Array ? 'Array' : 'Structure',
|
||||
addedNewVariable,
|
||||
};
|
||||
} else {
|
||||
// Value is a primitive, or not a valid Object/Array in JSON:
|
||||
|
||||
const variableType = readOrInferVariableType(forcedVariableType, value);
|
||||
|
||||
if (variableType === 'String') {
|
||||
variable.setString(value);
|
||||
} else if (variableType === 'Number') {
|
||||
variable.setValue(parseFloat(value));
|
||||
} else if (variableType === 'Boolean') {
|
||||
variable.setBool(value.toLowerCase() === 'true');
|
||||
}
|
||||
|
||||
return {
|
||||
variable,
|
||||
variableType,
|
||||
addedNewVariable,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
variable,
|
||||
variableType,
|
||||
addedNewVariable,
|
||||
};
|
||||
};
|
||||
|
@@ -1,4 +1,5 @@
|
||||
// @flow
|
||||
import { serializeToJSObject } from '../Utils/Serializer';
|
||||
import { applyVariableChange } from './ApplyVariableChange';
|
||||
|
||||
const gd: libGDevelop = global.gd;
|
||||
@@ -395,5 +396,142 @@ describe('applyVariableChange', () => {
|
||||
expect(variable.getType()).toBe(gd.Variable.String);
|
||||
expect(variable.getString()).toBe('123abc');
|
||||
});
|
||||
|
||||
it('should recognize JSON and parse it (object => structure)', () => {
|
||||
const result = applyVariableChange({
|
||||
variablePath: 'testString',
|
||||
forcedVariableType: null,
|
||||
variablesContainer,
|
||||
value:
|
||||
'{"a": 1, "b": "2", "c": [3, 4], "d": {"e": 5, "f": null, "g": true, "h": false}}',
|
||||
});
|
||||
expect(result.variableType).toBe('Structure');
|
||||
|
||||
const variable = variablesContainer.get('testString');
|
||||
expect(variable.getType()).toBe(gd.Variable.Structure);
|
||||
expect(serializeToJSObject(variable)).toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"children": Array [
|
||||
Object {
|
||||
"name": "a",
|
||||
"type": "number",
|
||||
"value": 1,
|
||||
},
|
||||
Object {
|
||||
"name": "b",
|
||||
"type": "string",
|
||||
"value": "2",
|
||||
},
|
||||
Object {
|
||||
"children": Array [
|
||||
Object {
|
||||
"type": "number",
|
||||
"value": 3,
|
||||
},
|
||||
Object {
|
||||
"type": "number",
|
||||
"value": 4,
|
||||
},
|
||||
],
|
||||
"name": "c",
|
||||
"type": "array",
|
||||
},
|
||||
Object {
|
||||
"children": Array [
|
||||
Object {
|
||||
"name": "e",
|
||||
"type": "number",
|
||||
"value": 5,
|
||||
},
|
||||
Object {
|
||||
"name": "f",
|
||||
"type": "string",
|
||||
"value": "null",
|
||||
},
|
||||
Object {
|
||||
"name": "g",
|
||||
"type": "boolean",
|
||||
"value": true,
|
||||
},
|
||||
Object {
|
||||
"name": "h",
|
||||
"type": "boolean",
|
||||
"value": false,
|
||||
},
|
||||
],
|
||||
"name": "d",
|
||||
"type": "structure",
|
||||
},
|
||||
],
|
||||
"type": "structure",
|
||||
}
|
||||
`);
|
||||
});
|
||||
|
||||
it('should recognize JSON and parse it (object => structure)', () => {
|
||||
const result = applyVariableChange({
|
||||
variablePath: 'testString',
|
||||
forcedVariableType: null,
|
||||
variablesContainer,
|
||||
value: '[1, "2", [3, 4], {"e": 5, "f": null, "g": true, "h": false}]',
|
||||
});
|
||||
expect(result.variableType).toBe('Array');
|
||||
|
||||
const variable = variablesContainer.get('testString');
|
||||
expect(variable.getType()).toBe(gd.Variable.Array);
|
||||
expect(serializeToJSObject(variable)).toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"children": Array [
|
||||
Object {
|
||||
"type": "number",
|
||||
"value": 1,
|
||||
},
|
||||
Object {
|
||||
"type": "string",
|
||||
"value": "2",
|
||||
},
|
||||
Object {
|
||||
"children": Array [
|
||||
Object {
|
||||
"type": "number",
|
||||
"value": 3,
|
||||
},
|
||||
Object {
|
||||
"type": "number",
|
||||
"value": 4,
|
||||
},
|
||||
],
|
||||
"type": "array",
|
||||
},
|
||||
Object {
|
||||
"children": Array [
|
||||
Object {
|
||||
"name": "e",
|
||||
"type": "number",
|
||||
"value": 5,
|
||||
},
|
||||
Object {
|
||||
"name": "f",
|
||||
"type": "string",
|
||||
"value": "null",
|
||||
},
|
||||
Object {
|
||||
"name": "g",
|
||||
"type": "boolean",
|
||||
"value": true,
|
||||
},
|
||||
Object {
|
||||
"name": "h",
|
||||
"type": "boolean",
|
||||
"value": false,
|
||||
},
|
||||
],
|
||||
"type": "structure",
|
||||
},
|
||||
],
|
||||
"type": "array",
|
||||
}
|
||||
`);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@@ -2,7 +2,9 @@
|
||||
import { type EventsGenerationResult } from '.';
|
||||
import {
|
||||
editorFunctions,
|
||||
editorFunctionsWithoutProject,
|
||||
type EditorFunction,
|
||||
type EditorFunctionWithoutProject,
|
||||
type EditorCallbacks,
|
||||
type EditorFunctionCall,
|
||||
type EditorFunctionGenericOutput,
|
||||
@@ -10,6 +12,7 @@ import {
|
||||
type AssetSearchAndInstallOptions,
|
||||
type AssetSearchAndInstallResult,
|
||||
type SceneEventsOutsideEditorChanges,
|
||||
type InstancesOutsideEditorChanges,
|
||||
} from '.';
|
||||
|
||||
export type EditorFunctionCallResult =
|
||||
@@ -29,7 +32,7 @@ export type EditorFunctionCallResult =
|
||||
|};
|
||||
|
||||
export type ProcessEditorFunctionCallsOptions = {|
|
||||
project: gdProject,
|
||||
project: gdProject | null,
|
||||
functionCalls: Array<EditorFunctionCall>,
|
||||
editorCallbacks: EditorCallbacks,
|
||||
ignore: boolean,
|
||||
@@ -39,6 +42,9 @@ export type ProcessEditorFunctionCallsOptions = {|
|
||||
onSceneEventsModifiedOutsideEditor: (
|
||||
changes: SceneEventsOutsideEditorChanges
|
||||
) => void,
|
||||
onInstancesModifiedOutsideEditor: (
|
||||
changes: InstancesOutsideEditorChanges
|
||||
) => void,
|
||||
ensureExtensionInstalled: (options: {|
|
||||
extensionName: string,
|
||||
|}) => Promise<void>,
|
||||
@@ -53,6 +59,7 @@ export const processEditorFunctionCalls = async ({
|
||||
editorCallbacks,
|
||||
generateEvents,
|
||||
onSceneEventsModifiedOutsideEditor,
|
||||
onInstancesModifiedOutsideEditor,
|
||||
ignore,
|
||||
ensureExtensionInstalled,
|
||||
searchAndInstallAsset,
|
||||
@@ -74,6 +81,17 @@ export const processEditorFunctionCalls = async ({
|
||||
}
|
||||
|
||||
const name = functionCall.name;
|
||||
if (!project && name !== 'initialize_project') {
|
||||
results.push({
|
||||
status: 'finished',
|
||||
call_id,
|
||||
success: false,
|
||||
output: {
|
||||
message: 'No project opened.',
|
||||
},
|
||||
});
|
||||
continue;
|
||||
}
|
||||
let args;
|
||||
try {
|
||||
try {
|
||||
@@ -117,7 +135,9 @@ export const processEditorFunctionCalls = async ({
|
||||
// Check if the function exists
|
||||
const editorFunction: EditorFunction | null =
|
||||
editorFunctions[name] || null;
|
||||
if (!editorFunction) {
|
||||
const editorFunctionWithoutProject: EditorFunctionWithoutProject | null =
|
||||
editorFunctionsWithoutProject[name] || null;
|
||||
if (!editorFunction && !editorFunctionWithoutProject) {
|
||||
results.push({
|
||||
status: 'finished',
|
||||
call_id,
|
||||
@@ -129,18 +149,42 @@ export const processEditorFunctionCalls = async ({
|
||||
continue;
|
||||
}
|
||||
|
||||
const argumentsWithoutProject = {
|
||||
args,
|
||||
editorCallbacks,
|
||||
generateEvents,
|
||||
onSceneEventsModifiedOutsideEditor,
|
||||
onInstancesModifiedOutsideEditor,
|
||||
ensureExtensionInstalled,
|
||||
searchAndInstallAsset,
|
||||
};
|
||||
|
||||
// Execute the function
|
||||
const result: EditorFunctionGenericOutput = await editorFunction.launchFunction(
|
||||
{
|
||||
project,
|
||||
args,
|
||||
generateEvents,
|
||||
onSceneEventsModifiedOutsideEditor,
|
||||
ensureExtensionInstalled,
|
||||
searchAndInstallAsset,
|
||||
let result: EditorFunctionGenericOutput;
|
||||
if (editorFunction) {
|
||||
if (project) {
|
||||
result = await editorFunction.launchFunction({
|
||||
...argumentsWithoutProject,
|
||||
project,
|
||||
});
|
||||
} else {
|
||||
result = ({
|
||||
success: false,
|
||||
message: `Function ${name} requires a project to be opened before being used.`,
|
||||
}: EditorFunctionGenericOutput);
|
||||
}
|
||||
);
|
||||
const { success, ...output } = result;
|
||||
} else if (editorFunctionWithoutProject) {
|
||||
result = await editorFunctionWithoutProject.launchFunction(
|
||||
argumentsWithoutProject
|
||||
);
|
||||
} else {
|
||||
result = ({
|
||||
success: false,
|
||||
message: `Unknown function with name: ${name}. Please use something else as this seems not supported or existing.`,
|
||||
}: EditorFunctionGenericOutput);
|
||||
}
|
||||
|
||||
const { success, meta, ...output } = result;
|
||||
results.push({
|
||||
status: 'finished',
|
||||
call_id,
|
||||
@@ -148,10 +192,8 @@ export const processEditorFunctionCalls = async ({
|
||||
output,
|
||||
});
|
||||
|
||||
if (success && args) {
|
||||
if (name === 'create_scene' && typeof args.scene_name === 'string') {
|
||||
createdSceneNames.push(args.scene_name);
|
||||
}
|
||||
if (meta && meta.newSceneNames) {
|
||||
createdSceneNames.push(...meta.newSceneNames);
|
||||
}
|
||||
} catch (error) {
|
||||
results.push({
|
||||
|
@@ -32,6 +32,12 @@ export type ExpressionSummary = {|
|
||||
relevantForSceneEvents?: boolean,
|
||||
|};
|
||||
|
||||
export type PropertySummary = {|
|
||||
name: string,
|
||||
description: string,
|
||||
type: string,
|
||||
|};
|
||||
|
||||
export type ObjectSummary = {|
|
||||
name: string,
|
||||
fullName: string,
|
||||
@@ -51,6 +57,17 @@ export type BehaviorSummary = {|
|
||||
expressions: Array<ExpressionSummary>,
|
||||
|};
|
||||
|
||||
export type EffectSummary = {|
|
||||
name: string,
|
||||
fullName: string,
|
||||
description: string,
|
||||
notWorkingForObjects: boolean,
|
||||
onlyWorkingFor2D: boolean,
|
||||
onlyWorkingFor3D: boolean,
|
||||
unique: boolean,
|
||||
properties: Array<PropertySummary>,
|
||||
|};
|
||||
|
||||
export type ExtensionSummary = {|
|
||||
extensionName: string,
|
||||
extensionFullName: string,
|
||||
@@ -60,6 +77,7 @@ export type ExtensionSummary = {|
|
||||
freeExpressions: Array<ExpressionSummary>,
|
||||
objects: { [string]: ObjectSummary },
|
||||
behaviors: { [string]: BehaviorSummary },
|
||||
effects: { [string]: EffectSummary },
|
||||
|};
|
||||
|
||||
const normalizeType = (parameterType: string) => {
|
||||
@@ -102,6 +120,29 @@ const getParameterSummary = (
|
||||
return parameterSummary;
|
||||
};
|
||||
|
||||
const getPropertySummary = (
|
||||
propertyName: string,
|
||||
property: gdPropertyDescriptor
|
||||
) => {
|
||||
return {
|
||||
name: propertyName,
|
||||
description: property.getDescription(),
|
||||
type: property.getType(),
|
||||
};
|
||||
};
|
||||
|
||||
const getPropertiesSummary = (
|
||||
propertiesMetadata: gdMapStringPropertyDescriptor
|
||||
) => {
|
||||
return propertiesMetadata
|
||||
.keys()
|
||||
.toJSArray()
|
||||
.map(propertyName => {
|
||||
const property = propertiesMetadata.get(propertyName);
|
||||
return getPropertySummary(propertyName, property);
|
||||
});
|
||||
};
|
||||
|
||||
export const buildExtensionSummary = ({
|
||||
gd,
|
||||
extension,
|
||||
@@ -111,6 +152,7 @@ export const buildExtensionSummary = ({
|
||||
}): ExtensionSummary => {
|
||||
const objects: { [string]: ObjectSummary } = {};
|
||||
const behaviors: { [string]: BehaviorSummary } = {};
|
||||
const effects: { [string]: EffectSummary } = {};
|
||||
|
||||
const generateInstructionsSummaries = ({
|
||||
instructionsMetadata,
|
||||
@@ -254,6 +296,27 @@ export const buildExtensionSummary = ({
|
||||
|
||||
behaviors[behaviorType] = behaviorSummary;
|
||||
});
|
||||
extension
|
||||
.getExtensionEffectTypes()
|
||||
.toJSArray()
|
||||
.forEach(effectType => {
|
||||
const effectMetadata = extension.getEffectMetadata(effectType);
|
||||
if (gd.MetadataProvider.isBadEffectMetadata(effectMetadata)) {
|
||||
return;
|
||||
}
|
||||
const effectSummary: EffectSummary = {
|
||||
name: effectMetadata.getType(),
|
||||
fullName: effectMetadata.getFullName(),
|
||||
description: effectMetadata.getDescription(),
|
||||
notWorkingForObjects: effectMetadata.isMarkedAsNotWorkingForObjects(),
|
||||
onlyWorkingFor2D: effectMetadata.isMarkedAsOnlyWorkingFor2D(),
|
||||
onlyWorkingFor3D: effectMetadata.isMarkedAsOnlyWorkingFor3D(),
|
||||
unique: effectMetadata.isMarkedAsUnique(),
|
||||
properties: getPropertiesSummary(effectMetadata.getProperties()),
|
||||
};
|
||||
|
||||
effects[effectType] = effectSummary;
|
||||
});
|
||||
|
||||
return {
|
||||
extensionName: extension.getName(),
|
||||
@@ -275,5 +338,6 @@ export const buildExtensionSummary = ({
|
||||
],
|
||||
objects,
|
||||
behaviors,
|
||||
effects,
|
||||
};
|
||||
};
|
||||
|
@@ -104,8 +104,7 @@ export const makeSimplifiedProjectBuilder = (gd: libGDevelop) => {
|
||||
const isCollection = isCollectionVariable(variable);
|
||||
|
||||
if (isCollection) {
|
||||
// Don't diplay children of arrays, and only display the first level of children of structures.
|
||||
if (variable.getType() === gd.Variable.Structure && depth === 0) {
|
||||
if (variable.getType() === gd.Variable.Structure) {
|
||||
return {
|
||||
variableName: name,
|
||||
type: getVariableType(variable),
|
||||
@@ -117,11 +116,20 @@ export const makeSimplifiedProjectBuilder = (gd: libGDevelop) => {
|
||||
return getSimplifiedVariable(childName, childVariable, depth + 1);
|
||||
}),
|
||||
};
|
||||
} else if (variable.getType() === gd.Variable.Array) {
|
||||
return {
|
||||
variableName: name,
|
||||
type: getVariableType(variable),
|
||||
variableChildren: mapFor(0, variable.getChildrenCount(), index => {
|
||||
const childVariable = variable.getAtIndex(index);
|
||||
return getSimplifiedVariable(
|
||||
index.toString(),
|
||||
childVariable,
|
||||
depth + 1
|
||||
);
|
||||
}),
|
||||
};
|
||||
}
|
||||
return {
|
||||
variableName: name,
|
||||
type: getVariableType(variable),
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
@@ -134,7 +142,7 @@ export const makeSimplifiedProjectBuilder = (gd: libGDevelop) => {
|
||||
const getSimplifiedVariablesContainerJson = (
|
||||
container: gdVariablesContainer
|
||||
): Array<SimplifiedVariable> => {
|
||||
return mapFor(0, Math.min(container.count(), 20), (index: number) => {
|
||||
return mapFor(0, container.count(), (index: number) => {
|
||||
const name = container.getNameAt(index);
|
||||
const variable = container.getAt(index);
|
||||
return getSimplifiedVariable(name, variable);
|
||||
|
@@ -135,6 +135,13 @@ describe('SimplifiedProject', () => {
|
||||
},
|
||||
Object {
|
||||
"type": "Array",
|
||||
"variableChildren": Array [
|
||||
Object {
|
||||
"type": "Number",
|
||||
"value": "856.5",
|
||||
"variableName": "0",
|
||||
},
|
||||
],
|
||||
"variableName": "ObjectChild4",
|
||||
},
|
||||
],
|
||||
@@ -460,6 +467,13 @@ describe('SimplifiedProject', () => {
|
||||
},
|
||||
Object {
|
||||
"type": "Array",
|
||||
"variableChildren": Array [
|
||||
Object {
|
||||
"type": "Number",
|
||||
"value": "856.5",
|
||||
"variableName": "0",
|
||||
},
|
||||
],
|
||||
"variableName": "ObjectChild4",
|
||||
},
|
||||
],
|
||||
@@ -812,6 +826,15 @@ describe('SimplifiedProject', () => {
|
||||
},
|
||||
Object {
|
||||
"type": "Structure",
|
||||
"variableChildren": Array [
|
||||
Object {
|
||||
"type": "String",
|
||||
"value": "Hello
|
||||
Multiline
|
||||
World",
|
||||
"variableName": "SubChild1",
|
||||
},
|
||||
],
|
||||
"variableName": "FoldedChild",
|
||||
},
|
||||
],
|
||||
@@ -819,10 +842,45 @@ describe('SimplifiedProject', () => {
|
||||
},
|
||||
Object {
|
||||
"type": "Array",
|
||||
"variableChildren": Array [
|
||||
Object {
|
||||
"type": "String",
|
||||
"value": "String value
|
||||
with Multiline",
|
||||
"variableName": "0",
|
||||
},
|
||||
Object {
|
||||
"type": "Number",
|
||||
"value": "4539.42",
|
||||
"variableName": "1",
|
||||
},
|
||||
Object {
|
||||
"type": "Boolean",
|
||||
"value": "True",
|
||||
"variableName": "2",
|
||||
},
|
||||
],
|
||||
"variableName": "FoldedArray",
|
||||
},
|
||||
Object {
|
||||
"type": "Array",
|
||||
"variableChildren": Array [
|
||||
Object {
|
||||
"type": "String",
|
||||
"value": "PlayerName",
|
||||
"variableName": "0",
|
||||
},
|
||||
Object {
|
||||
"type": "Number",
|
||||
"value": "25",
|
||||
"variableName": "1",
|
||||
},
|
||||
Object {
|
||||
"type": "Boolean",
|
||||
"value": "False",
|
||||
"variableName": "2",
|
||||
},
|
||||
],
|
||||
"variableName": "OtherArray",
|
||||
},
|
||||
],
|
||||
@@ -894,6 +952,7 @@ describe('SimplifiedProject', () => {
|
||||
},
|
||||
},
|
||||
"description": "A fake extension with a fake behavior containing 2 properties.",
|
||||
"effects": Object {},
|
||||
"extensionFullName": "Fake extension with a fake behavior",
|
||||
"extensionName": "FakeBehavior",
|
||||
"freeActions": Array [],
|
||||
|
File diff suppressed because it is too large
Load Diff
@@ -15,7 +15,7 @@ import ElementWithMenu from '../UI/Menu/ElementWithMenu';
|
||||
import SemiControlledTextField from '../UI/SemiControlledTextField';
|
||||
import newNameGenerator from '../Utils/NewNameGenerator';
|
||||
import { ResponsiveLineStackLayout, ColumnStackLayout } from '../UI/Layout';
|
||||
import StringArrayEditor from '../StringArrayEditor';
|
||||
import ChoicesEditor, { type Choice } from '../ChoicesEditor';
|
||||
import ColorField from '../UI/ColorField';
|
||||
import BehaviorTypeSelector from '../BehaviorTypeSelector';
|
||||
import SemiControlledAutoComplete from '../UI/SemiControlledAutoComplete';
|
||||
@@ -124,9 +124,13 @@ const getValidatedPropertyName = (
|
||||
return safeAndUniqueNewName;
|
||||
};
|
||||
|
||||
const getExtraInfoArray = (property: gdNamedPropertyDescriptor) => {
|
||||
const extraInfoVector = property.getExtraInfo();
|
||||
return extraInfoVector.toJSArray();
|
||||
const getChoicesArray = (
|
||||
property: gdNamedPropertyDescriptor
|
||||
): Array<Choice> => {
|
||||
return mapVector(property.getChoices(), choice => ({
|
||||
value: choice.getValue(),
|
||||
label: choice.getLabel(),
|
||||
}));
|
||||
};
|
||||
|
||||
export default function EventsBasedBehaviorPropertiesEditor({
|
||||
@@ -397,17 +401,18 @@ export default function EventsBasedBehaviorPropertiesEditor({
|
||||
[properties, forceUpdate, onPropertiesUpdated]
|
||||
);
|
||||
|
||||
const setChoiceExtraInfo = React.useCallback(
|
||||
const setChoices = React.useCallback(
|
||||
(property: gdNamedPropertyDescriptor) => {
|
||||
return (newExtraInfo: Array<string>) => {
|
||||
const defaultValueIndex = getExtraInfoArray(property).indexOf(
|
||||
property.getValue()
|
||||
return (choices: Array<Choice>) => {
|
||||
property.clearChoices();
|
||||
for (const choice of choices) {
|
||||
property.addChoice(choice.value, choice.label);
|
||||
}
|
||||
property.setValue(
|
||||
getChoicesArray(property).includes(property.getValue())
|
||||
? property.getValue()
|
||||
: ''
|
||||
);
|
||||
const vectorString = new gd.VectorString();
|
||||
newExtraInfo.forEach(item => vectorString.push_back(item));
|
||||
property.setExtraInfo(vectorString);
|
||||
vectorString.delete();
|
||||
property.setValue(newExtraInfo[defaultValueIndex] || '');
|
||||
forceUpdate();
|
||||
};
|
||||
},
|
||||
@@ -954,12 +959,12 @@ export default function EventsBasedBehaviorPropertiesEditor({
|
||||
}}
|
||||
fullWidth
|
||||
>
|
||||
{getExtraInfoArray(property).map(
|
||||
{getChoicesArray(property).map(
|
||||
(choice, index) => (
|
||||
<SelectOption
|
||||
key={index}
|
||||
value={choice}
|
||||
label={choice}
|
||||
value={choice.value}
|
||||
label={choice.value}
|
||||
/>
|
||||
)
|
||||
)}
|
||||
@@ -967,11 +972,9 @@ export default function EventsBasedBehaviorPropertiesEditor({
|
||||
)}
|
||||
</ResponsiveLineStackLayout>
|
||||
{property.getType() === 'Choice' && (
|
||||
<StringArrayEditor
|
||||
extraInfo={getExtraInfoArray(property)}
|
||||
setExtraInfo={setChoiceExtraInfo(
|
||||
property
|
||||
)}
|
||||
<ChoicesEditor
|
||||
choices={getChoicesArray(property)}
|
||||
setChoices={setChoices(property)}
|
||||
/>
|
||||
)}
|
||||
<ResponsiveLineStackLayout noMargin>
|
||||
|
@@ -15,7 +15,7 @@ import ElementWithMenu from '../UI/Menu/ElementWithMenu';
|
||||
import SemiControlledTextField from '../UI/SemiControlledTextField';
|
||||
import newNameGenerator from '../Utils/NewNameGenerator';
|
||||
import { ResponsiveLineStackLayout, ColumnStackLayout } from '../UI/Layout';
|
||||
import StringArrayEditor from '../StringArrayEditor';
|
||||
import ChoicesEditor, { type Choice } from '../ChoicesEditor';
|
||||
import ColorField from '../UI/ColorField';
|
||||
import SemiControlledAutoComplete from '../UI/SemiControlledAutoComplete';
|
||||
import ScrollView, { type ScrollViewInterface } from '../UI/ScrollView';
|
||||
@@ -119,9 +119,13 @@ const getValidatedPropertyName = (
|
||||
return safeAndUniqueNewName;
|
||||
};
|
||||
|
||||
const getExtraInfoArray = (property: gdNamedPropertyDescriptor) => {
|
||||
const extraInfoVector = property.getExtraInfo();
|
||||
return extraInfoVector.toJSArray();
|
||||
const getChoicesArray = (
|
||||
property: gdNamedPropertyDescriptor
|
||||
): Array<Choice> => {
|
||||
return mapVector(property.getChoices(), choice => ({
|
||||
value: choice.getValue(),
|
||||
label: choice.getLabel(),
|
||||
}));
|
||||
};
|
||||
|
||||
export default function EventsBasedObjectPropertiesEditor({
|
||||
@@ -404,17 +408,18 @@ export default function EventsBasedObjectPropertiesEditor({
|
||||
[eventsBasedObject, forceUpdate, onPropertiesUpdated]
|
||||
);
|
||||
|
||||
const setChoiceExtraInfo = React.useCallback(
|
||||
const setChoices = React.useCallback(
|
||||
(property: gdNamedPropertyDescriptor) => {
|
||||
return (newExtraInfo: Array<string>) => {
|
||||
const defaultValueIndex = getExtraInfoArray(property).indexOf(
|
||||
property.getValue()
|
||||
return (choices: Array<Choice>) => {
|
||||
property.clearChoices();
|
||||
for (const choice of choices) {
|
||||
property.addChoice(choice.value, choice.label);
|
||||
}
|
||||
property.setValue(
|
||||
getChoicesArray(property).includes(property.getValue())
|
||||
? property.getValue()
|
||||
: ''
|
||||
);
|
||||
const vectorString = new gd.VectorString();
|
||||
newExtraInfo.forEach(item => vectorString.push_back(item));
|
||||
property.setExtraInfo(vectorString);
|
||||
vectorString.delete();
|
||||
property.setValue(newExtraInfo[defaultValueIndex] || '');
|
||||
forceUpdate();
|
||||
};
|
||||
},
|
||||
@@ -877,12 +882,12 @@ export default function EventsBasedObjectPropertiesEditor({
|
||||
}}
|
||||
fullWidth
|
||||
>
|
||||
{getExtraInfoArray(property).map(
|
||||
{getChoicesArray(property).map(
|
||||
(choice, index) => (
|
||||
<SelectOption
|
||||
key={index}
|
||||
value={choice}
|
||||
label={choice}
|
||||
value={choice.value}
|
||||
label={choice.label}
|
||||
/>
|
||||
)
|
||||
)}
|
||||
@@ -906,11 +911,9 @@ export default function EventsBasedObjectPropertiesEditor({
|
||||
)}
|
||||
</ResponsiveLineStackLayout>
|
||||
{property.getType() === 'Choice' && (
|
||||
<StringArrayEditor
|
||||
extraInfo={getExtraInfoArray(property)}
|
||||
setExtraInfo={setChoiceExtraInfo(
|
||||
property
|
||||
)}
|
||||
<ChoicesEditor
|
||||
choices={getChoicesArray(property)}
|
||||
setChoices={setChoices(property)}
|
||||
/>
|
||||
)}
|
||||
<ResponsiveLineStackLayout noMargin>
|
||||
|
@@ -113,11 +113,12 @@ export default function EventsBasedObjectEditor({
|
||||
}}
|
||||
/>
|
||||
<Checkbox
|
||||
label={<Trans>Has animations</Trans>}
|
||||
checked={eventsBasedObject.isAnimatable()}
|
||||
label={<Trans>Expand inner area with parent</Trans>}
|
||||
checked={eventsBasedObject.isInnerAreaFollowingParentSize()}
|
||||
onCheck={(e, checked) => {
|
||||
eventsBasedObject.markAsAnimatable(checked);
|
||||
eventsBasedObject.markAsInnerAreaFollowingParentSize(checked);
|
||||
onChange();
|
||||
onEventsBasedObjectChildrenEdited(eventsBasedObject);
|
||||
}}
|
||||
/>
|
||||
<Checkbox
|
||||
@@ -129,12 +130,11 @@ export default function EventsBasedObjectEditor({
|
||||
}}
|
||||
/>
|
||||
<Checkbox
|
||||
label={<Trans>Expand inner area with parent</Trans>}
|
||||
checked={eventsBasedObject.isInnerAreaFollowingParentSize()}
|
||||
label={<Trans>Has animations (JavaScript only)</Trans>}
|
||||
checked={eventsBasedObject.isAnimatable()}
|
||||
onCheck={(e, checked) => {
|
||||
eventsBasedObject.markAsInnerAreaFollowingParentSize(checked);
|
||||
eventsBasedObject.markAsAnimatable(checked);
|
||||
onChange();
|
||||
onEventsBasedObjectChildrenEdited(eventsBasedObject);
|
||||
}}
|
||||
/>
|
||||
{isDev && (
|
||||
|
@@ -553,12 +553,20 @@ export const EventsFunctionPropertiesEditor = ({
|
||||
}}
|
||||
/>
|
||||
{eventsFunction.isAsync() && (
|
||||
<AlertMessage kind="info">
|
||||
<AlertMessage
|
||||
kind="info"
|
||||
renderRightButton={() => (
|
||||
<HelpButton
|
||||
helpPagePath={'/functions/asynchronous-functions'}
|
||||
/>
|
||||
)}
|
||||
>
|
||||
<Trans>
|
||||
This is an asynchronous action, meaning that the actions
|
||||
and sub-events following it will wait for it to end.
|
||||
Don't forget to use the action "End asynchronous
|
||||
function" to mark the end of the action.
|
||||
and sub-events following it will wait for it to end. You
|
||||
should use other async actions like "wait" to schedule
|
||||
your actions and don't forget to use the action "End
|
||||
asynchronous function" to mark the end of the action.
|
||||
</Trans>
|
||||
</AlertMessage>
|
||||
)}
|
||||
|
@@ -9,7 +9,7 @@ import SemiControlledTextField from '../../UI/SemiControlledTextField';
|
||||
import ObjectTypeSelector from '../../ObjectTypeSelector';
|
||||
import BehaviorTypeSelector from '../../BehaviorTypeSelector';
|
||||
import { ColumnStackLayout, ResponsiveLineStackLayout } from '../../UI/Layout';
|
||||
import StringArrayEditor from '../../StringArrayEditor';
|
||||
import ChoicesEditor, { type Choice } from '../../ChoicesEditor';
|
||||
import useForceUpdate from '../../Utils/UseForceUpdate';
|
||||
import ResourceTypeSelectField from './ResourceTypeSelectField';
|
||||
|
||||
@@ -295,11 +295,18 @@ export default function ValueTypeEditor({
|
||||
)}
|
||||
</ResponsiveLineStackLayout>
|
||||
{valueTypeMetadata.getName() === 'stringWithSelector' && (
|
||||
<StringArrayEditor
|
||||
<ChoicesEditor
|
||||
disabled={disabled}
|
||||
extraInfo={getExtraInfoArray(valueTypeMetadata)}
|
||||
setExtraInfo={(newExtraInfo: Array<string>) => {
|
||||
valueTypeMetadata.setExtraInfo(JSON.stringify(newExtraInfo));
|
||||
choices={getExtraInfoArray(valueTypeMetadata).map(value => ({
|
||||
value,
|
||||
label: '',
|
||||
}))}
|
||||
hideLabels
|
||||
setChoices={(choices: Array<Choice>) => {
|
||||
// TODO Handle labels (and search "choice label")
|
||||
valueTypeMetadata.setExtraInfo(
|
||||
JSON.stringify(choices.map(choice => choice.value))
|
||||
);
|
||||
forceUpdate();
|
||||
onTypeUpdated();
|
||||
}}
|
||||
|
@@ -1,9 +1,6 @@
|
||||
// @flow
|
||||
import { t } from '@lingui/macro';
|
||||
|
||||
import * as React from 'react';
|
||||
import classNames from 'classnames';
|
||||
import TextField, { type TextFieldInterface } from '../../../UI/TextField';
|
||||
import { rgbToHex } from '../../../Utils/ColorTransformer';
|
||||
import {
|
||||
largeSelectedArea,
|
||||
@@ -22,6 +19,15 @@ const gd: libGDevelop = global.gd;
|
||||
|
||||
const commentTextStyle = {
|
||||
width: '100%',
|
||||
fontSize: 'inherit',
|
||||
fontFamily: '-apple-system, BlinkMacSystemFont, sans-serif',
|
||||
padding: 0,
|
||||
backgroundColor: 'transparent',
|
||||
outline: 0,
|
||||
border: 0,
|
||||
|
||||
// Big enough to have an empty text be the same size as an empty textarea.
|
||||
lineHeight: '1.5em',
|
||||
};
|
||||
|
||||
const styles = {
|
||||
@@ -30,9 +36,9 @@ const styles = {
|
||||
flexWrap: 'wrap',
|
||||
padding: 5,
|
||||
overflow: 'hidden',
|
||||
minHeight: '2.1em',
|
||||
minHeight: '2.4em',
|
||||
},
|
||||
commentTextField: { ...commentTextStyle, fontSize: 'inherit' },
|
||||
commentTextField: { ...commentTextStyle, minHeight: '0', resize: 'none' },
|
||||
commentSpan: {
|
||||
...commentTextStyle,
|
||||
alignItems: 'center',
|
||||
@@ -55,8 +61,7 @@ export default class CommentEvent extends React.Component<
|
||||
editingPreviousValue: null,
|
||||
};
|
||||
|
||||
_selectable: ?HTMLSpanElement;
|
||||
_textField: ?TextFieldInterface;
|
||||
_textField: ?HTMLTextAreaElement;
|
||||
|
||||
edit = () => {
|
||||
if (this.state.editing) return;
|
||||
@@ -67,8 +72,11 @@ export default class CommentEvent extends React.Component<
|
||||
editingPreviousValue: commentEvent.getComment(),
|
||||
},
|
||||
() => {
|
||||
if (this._textField) {
|
||||
this._textField.focus({ caretPosition: 'end' });
|
||||
const textField = this._textField;
|
||||
if (textField) {
|
||||
textField.focus();
|
||||
textField.selectionStart = textField.value.length;
|
||||
textField.selectionEnd = textField.value.length;
|
||||
}
|
||||
// Wait for the change to be applied on the DOM before calling onUpdate,
|
||||
// so that the height of the event is updated.
|
||||
@@ -77,15 +85,12 @@ export default class CommentEvent extends React.Component<
|
||||
);
|
||||
};
|
||||
|
||||
onChange = (e: any, text: string) => {
|
||||
onChange = (e: any) => {
|
||||
const commentEvent = gd.asCommentEvent(this.props.event);
|
||||
commentEvent.setComment(text);
|
||||
commentEvent.setComment(e.target.value);
|
||||
|
||||
this.forceUpdate(() => {
|
||||
// Wait for the change to be applied on the DOM before calling onUpdate,
|
||||
// so that the height of the event is updated.
|
||||
this.props.onUpdate();
|
||||
});
|
||||
this._autoResizeTextArea();
|
||||
this.forceUpdate();
|
||||
};
|
||||
|
||||
endEditing = () => {
|
||||
@@ -114,6 +119,22 @@ export default class CommentEvent extends React.Component<
|
||||
.replace(/\n/g, '<br>');
|
||||
};
|
||||
|
||||
_autoResizeTextArea = () => {
|
||||
if (this._textField) {
|
||||
const previousHeight = this._textField.style.height;
|
||||
this._textField.style.height = 'auto';
|
||||
this._textField.style.height = this._textField.scrollHeight + 'px';
|
||||
|
||||
if (previousHeight !== this._textField.style.height) {
|
||||
this.props.onUpdate(); // Notify the parent that the height has changed.
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
componentDidUpdate() {
|
||||
this._autoResizeTextArea();
|
||||
}
|
||||
|
||||
render() {
|
||||
const commentEvent = gd.asCommentEvent(this.props.event);
|
||||
|
||||
@@ -149,31 +170,27 @@ export default class CommentEvent extends React.Component<
|
||||
id={`${this.props.idPrefix}-comment`}
|
||||
>
|
||||
{this.state.editing ? (
|
||||
<TextField
|
||||
multiline
|
||||
margin="none"
|
||||
<textarea
|
||||
ref={textField => (this._textField = textField)}
|
||||
value={commentEvent.getComment()}
|
||||
translatableHintText={t`<Enter comment>`}
|
||||
placeholder="..."
|
||||
onBlur={this.endEditing}
|
||||
onChange={this.onChange}
|
||||
style={styles.commentTextField}
|
||||
inputStyle={{
|
||||
color: textColor,
|
||||
padding: 0,
|
||||
}}
|
||||
fullWidth
|
||||
style={{ ...styles.commentTextField, color: textColor }}
|
||||
id="comment-title"
|
||||
onKeyDown={event => {
|
||||
if (shouldCloseOrCancel(event) || shouldSubmit(event)) {
|
||||
this.endEditing();
|
||||
}
|
||||
}}
|
||||
underlineShow={false}
|
||||
rows={
|
||||
/* Ensure the textarea resize down to 1 line when no text or just a single line is entered. */
|
||||
1
|
||||
}
|
||||
spellCheck="false"
|
||||
/>
|
||||
) : (
|
||||
<span
|
||||
ref={selectable => (this._selectable = selectable)}
|
||||
className={classNames({
|
||||
[selectableArea]: true,
|
||||
[disabledText]: this.props.disabled,
|
||||
|
@@ -428,6 +428,7 @@ const InstructionEditorDialog = ({
|
||||
open={newBehaviorDialogOpen}
|
||||
objectType={chosenObject.getType()}
|
||||
objectBehaviorsTypes={listObjectBehaviorsTypes(chosenObject)}
|
||||
isChildObject={!scope.layout}
|
||||
onClose={() => setNewBehaviorDialogOpen(false)}
|
||||
onChoose={addBehavior}
|
||||
onExtensionInstalled={extensionName => {
|
||||
|
@@ -19,7 +19,10 @@ import {
|
||||
} from '../../Utils/GDevelopServices/Project';
|
||||
import PlaceholderLoader from '../../UI/PlaceholderLoader';
|
||||
import PlaceholderError from '../../UI/PlaceholderError';
|
||||
import { getUserPublicProfilesByIds } from '../../Utils/GDevelopServices/User';
|
||||
import {
|
||||
getUserPublicProfilesByIds,
|
||||
type UserPublicProfileByIds,
|
||||
} from '../../Utils/GDevelopServices/User';
|
||||
import Dialog, { DialogPrimaryButton } from '../../UI/Dialog';
|
||||
import FlatButton from '../../UI/FlatButton';
|
||||
import LeftLoader from '../../UI/LeftLoader';
|
||||
@@ -84,9 +87,10 @@ const InviteHome = ({ cloudProjectId }: Props) => {
|
||||
| 'unexpected'
|
||||
| null
|
||||
>(null);
|
||||
const [userPublicProfileByIds, setUserPublicProfileByIds] = React.useState(
|
||||
{}
|
||||
);
|
||||
const [
|
||||
userPublicProfileByIds,
|
||||
setUserPublicProfileByIds,
|
||||
] = React.useState<UserPublicProfileByIds>({});
|
||||
const [
|
||||
showCollaboratorAddDialog,
|
||||
setShowCollaboratorAddDialog,
|
||||
@@ -312,6 +316,7 @@ const InviteHome = ({ cloudProjectId }: Props) => {
|
||||
<ColumnStackLayout expand noMargin>
|
||||
<UserLine
|
||||
username={profile.username}
|
||||
fullName={profile.fullName}
|
||||
email={profile.email}
|
||||
level={currentUserLevel}
|
||||
/>
|
||||
@@ -365,6 +370,7 @@ const InviteHome = ({ cloudProjectId }: Props) => {
|
||||
projectUserAcls.map(projectUserAcl => (
|
||||
<UserLine
|
||||
username={getCollaboratorUsername(projectUserAcl.userId)}
|
||||
fullName={null}
|
||||
email={projectUserAcl.email}
|
||||
level={projectUserAcl.level}
|
||||
onDelete={() => {
|
||||
|
@@ -1,4 +1,5 @@
|
||||
// @flow
|
||||
import Window from '../Utils/Window';
|
||||
import { getIDEVersion } from '../Version';
|
||||
|
||||
type FileSet =
|
||||
@@ -48,10 +49,9 @@ export const findGDJS = (
|
||||
// run `newIDE/web-app/scripts/deploy-GDJS-Runtime` script.
|
||||
let gdjsRoot = `https://resources.gdevelop-app.com/GDJS-${getIDEVersion()}`;
|
||||
|
||||
// If you want to test your local changes to the game engine on the local web-app,
|
||||
// run `npx serve --cors -p 5001` (or another CORS enabled http server on port 5001)
|
||||
// in `newIDE/app/resources/GDJS` and uncomment this line:
|
||||
// gdjsRoot = `http://localhost:5001`;
|
||||
if (Window.isDev()) {
|
||||
gdjsRoot = `http://localhost:5002`;
|
||||
}
|
||||
|
||||
return Promise.all(
|
||||
filesToDownload[fileSet].map(relativeFilePath => {
|
||||
|
@@ -113,7 +113,7 @@ export const getExtraInstructionInformation = (type: string): ?Hint => {
|
||||
if (type === 'TextObject::Text::SetFontSize') {
|
||||
return {
|
||||
kind: 'warning',
|
||||
message: t`This action will create a new texture and re-render the text each time it is called, which is expensive and can reduce performances. Prefer to avoid changing a lot the character size of a text.`,
|
||||
message: t`This action will create a new texture and re-render the text each time it is called, which is expensive and can reduce performance. Avoid changing the character size of text frequently.`,
|
||||
};
|
||||
}
|
||||
if (type === 'PlayMusicCanal' || type === 'PlayMusic') {
|
||||
|
@@ -17,6 +17,7 @@ import { type CourseChapter } from '../../Utils/GDevelopServices/Asset';
|
||||
import { type GamesList } from '../../GameDashboard/UseGamesList';
|
||||
import { type GamesPlatformFrameTools } from './HomePage/PlaySection/UseGamesPlatformFrame';
|
||||
import { type ObjectWithContext } from '../../ObjectsList/EnumerateObjects';
|
||||
import { type CreateProjectResult } from '../../Utils/UseCreateProject';
|
||||
|
||||
export type EditorContainerExtraProps = {|
|
||||
// Events function extension editor
|
||||
@@ -37,6 +38,10 @@ export type SceneEventsOutsideEditorChanges = {|
|
||||
newOrChangedAiGeneratedEventIds: Set<string>,
|
||||
|};
|
||||
|
||||
export type InstancesOutsideEditorChanges = {|
|
||||
scene: gdLayout,
|
||||
|};
|
||||
|
||||
export type RenderEditorContainerProps = {|
|
||||
isActive: boolean,
|
||||
projectItemName: ?string,
|
||||
@@ -155,8 +160,10 @@ export type RenderEditorContainerProps = {|
|
||||
newProjectSetup: NewProjectSetup,
|
||||
i18n: I18nType,
|
||||
isQuickCustomization?: boolean
|
||||
) => Promise<void>,
|
||||
onCreateEmptyProject: (newProjectSetup: NewProjectSetup) => Promise<void>,
|
||||
) => Promise<CreateProjectResult>,
|
||||
onCreateEmptyProject: (
|
||||
newProjectSetup: NewProjectSetup
|
||||
) => Promise<CreateProjectResult>,
|
||||
onOpenTemplateFromTutorial: (tutorialId: string) => Promise<void>,
|
||||
onOpenTemplateFromCourseChapter: (
|
||||
CourseChapter,
|
||||
@@ -179,11 +186,14 @@ export type RenderEditorContainerProps = {|
|
||||
) => void,
|
||||
onSceneObjectsDeleted: (scene: gdLayout) => void,
|
||||
|
||||
onInstancesModifiedOutsideEditor: (
|
||||
changes: InstancesOutsideEditorChanges
|
||||
) => void,
|
||||
|
||||
// Events editing
|
||||
onSceneEventsModifiedOutsideEditor: (
|
||||
changes: SceneEventsOutsideEditorChanges
|
||||
) => void,
|
||||
|
||||
onExtractAsExternalLayout: (name: string) => void,
|
||||
onExtractAsEventBasedObject: (
|
||||
extensionName: string,
|
||||
|
@@ -4,6 +4,7 @@ import {
|
||||
type RenderEditorContainerProps,
|
||||
type RenderEditorContainerPropsWithRef,
|
||||
type SceneEventsOutsideEditorChanges,
|
||||
type InstancesOutsideEditorChanges,
|
||||
} from './BaseEditor';
|
||||
import { prepareInstancesEditorSettings } from '../../InstancesEditor/InstancesEditorSettings';
|
||||
import {
|
||||
@@ -115,6 +116,10 @@ export class CustomObjectEditorContainer extends React.Component<RenderEditorCon
|
||||
// No thing to be done.
|
||||
}
|
||||
|
||||
onInstancesModifiedOutsideEditor(changes: InstancesOutsideEditorChanges) {
|
||||
// No thing to be done.
|
||||
}
|
||||
|
||||
saveUiSettings = () => {
|
||||
// const layout = this.getCustomObject();
|
||||
// const editor = this.editor;
|
||||
|
@@ -7,6 +7,7 @@ import {
|
||||
type RenderEditorContainerProps,
|
||||
type RenderEditorContainerPropsWithRef,
|
||||
type SceneEventsOutsideEditorChanges,
|
||||
type InstancesOutsideEditorChanges,
|
||||
} from './BaseEditor';
|
||||
import SubscriptionChecker, {
|
||||
type SubscriptionCheckerInterface,
|
||||
@@ -66,6 +67,10 @@ export class DebuggerEditorContainer extends React.Component<
|
||||
// No thing to be done.
|
||||
}
|
||||
|
||||
onInstancesModifiedOutsideEditor(changes: InstancesOutsideEditorChanges) {
|
||||
// No thing to be done.
|
||||
}
|
||||
|
||||
// To be updated, see https://reactjs.org/docs/react-component.html#unsafe_componentwillreceiveprops.
|
||||
UNSAFE_componentWillReceiveProps() {
|
||||
this._checkUserHasSubscription();
|
||||
|
@@ -6,6 +6,7 @@ import {
|
||||
type RenderEditorContainerProps,
|
||||
type RenderEditorContainerPropsWithRef,
|
||||
type SceneEventsOutsideEditorChanges,
|
||||
type InstancesOutsideEditorChanges,
|
||||
} from './BaseEditor';
|
||||
import { ProjectScopedContainersAccessor } from '../../InstructionOrExpression/EventsScope';
|
||||
import { type ObjectWithContext } from '../../ObjectsList/EnumerateObjects';
|
||||
@@ -68,6 +69,10 @@ export class EventsEditorContainer extends React.Component<RenderEditorContainer
|
||||
}
|
||||
}
|
||||
|
||||
onInstancesModifiedOutsideEditor(changes: InstancesOutsideEditorChanges) {
|
||||
// No thing to be done.
|
||||
}
|
||||
|
||||
getLayout(): ?gdLayout {
|
||||
const { project, projectItemName } = this.props;
|
||||
if (
|
||||
|
@@ -5,6 +5,7 @@ import {
|
||||
type RenderEditorContainerProps,
|
||||
type RenderEditorContainerPropsWithRef,
|
||||
type SceneEventsOutsideEditorChanges,
|
||||
type InstancesOutsideEditorChanges,
|
||||
} from './BaseEditor';
|
||||
import { type ObjectWithContext } from '../../ObjectsList/EnumerateObjects';
|
||||
|
||||
@@ -55,6 +56,10 @@ export class EventsFunctionsExtensionEditorContainer extends React.Component<Ren
|
||||
// No thing to be done.
|
||||
}
|
||||
|
||||
onInstancesModifiedOutsideEditor(changes: InstancesOutsideEditorChanges) {
|
||||
// No thing to be done.
|
||||
}
|
||||
|
||||
shouldComponentUpdate(nextProps: RenderEditorContainerProps) {
|
||||
// We stop updates when the component is inactive.
|
||||
// If it's active, was active or becoming active again we let update propagate.
|
||||
|
@@ -8,6 +8,7 @@ import {
|
||||
type RenderEditorContainerProps,
|
||||
type RenderEditorContainerPropsWithRef,
|
||||
type SceneEventsOutsideEditorChanges,
|
||||
type InstancesOutsideEditorChanges,
|
||||
} from './BaseEditor';
|
||||
import ExternalPropertiesDialog, {
|
||||
type ExternalProperties,
|
||||
@@ -99,6 +100,10 @@ export class ExternalEventsEditorContainer extends React.Component<
|
||||
// No thing to be done.
|
||||
}
|
||||
|
||||
onInstancesModifiedOutsideEditor(changes: InstancesOutsideEditorChanges) {
|
||||
// No thing to be done.
|
||||
}
|
||||
|
||||
getExternalEvents(): ?gdExternalEvents {
|
||||
const { project, projectItemName } = this.props;
|
||||
if (!project || !projectItemName) return null;
|
||||
|
@@ -13,6 +13,7 @@ import {
|
||||
type RenderEditorContainerProps,
|
||||
type RenderEditorContainerPropsWithRef,
|
||||
type SceneEventsOutsideEditorChanges,
|
||||
type InstancesOutsideEditorChanges,
|
||||
} from './BaseEditor';
|
||||
import ExternalPropertiesDialog, {
|
||||
type ExternalProperties,
|
||||
@@ -157,6 +158,16 @@ export class ExternalLayoutEditorContainer extends React.Component<
|
||||
// No thing to be done.
|
||||
}
|
||||
|
||||
onInstancesModifiedOutsideEditor(changes: InstancesOutsideEditorChanges) {
|
||||
if (changes.scene !== this.getLayout()) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.editor) {
|
||||
this.editor.onInstancesModifiedOutsideEditor();
|
||||
}
|
||||
}
|
||||
|
||||
getExternalLayout(): ?gdExternalLayout {
|
||||
const { project, projectItemName } = this.props;
|
||||
if (!project || !projectItemName) return null;
|
||||
|
@@ -50,6 +50,7 @@ import {
|
||||
import { useProjectsListFor } from './utils';
|
||||
import { deleteCloudProject } from '../../../../Utils/GDevelopServices/Project';
|
||||
import { getDefaultRegisterGameProperties } from '../../../../Utils/UseGameAndBuildsManager';
|
||||
import { type CreateProjectResult } from '../../../../Utils/UseCreateProject';
|
||||
|
||||
const getExampleItemsColumns = (
|
||||
windowSize: WindowSizeType,
|
||||
@@ -83,7 +84,7 @@ type Props = {|
|
||||
newProjectSetup: NewProjectSetup,
|
||||
i18n: I18nType,
|
||||
isQuickCustomization?: boolean
|
||||
) => Promise<void>,
|
||||
) => Promise<CreateProjectResult>,
|
||||
onSelectPrivateGameTemplateListingData: (
|
||||
privateGameTemplateListingData: PrivateGameTemplateListingData
|
||||
) => void,
|
||||
|
@@ -1,303 +0,0 @@
|
||||
// @flow
|
||||
|
||||
import * as React from 'react';
|
||||
import { I18n } from '@lingui/react';
|
||||
import SectionContainer, { SectionRow } from '../SectionContainer';
|
||||
import { Column, Line } from '../../../../UI/Grid';
|
||||
import BundlePageHeader from './BundlePageHeader';
|
||||
import { BundleStoreContext } from '../../../../AssetStore/Bundles/BundleStoreContext';
|
||||
import PlaceholderLoader from '../../../../UI/PlaceholderLoader';
|
||||
import type { CourseCompletion } from '../UseCourses';
|
||||
import {
|
||||
getBundle,
|
||||
type Bundle,
|
||||
type Course,
|
||||
} from '../../../../Utils/GDevelopServices/Asset';
|
||||
import {
|
||||
type PrivateAssetPackListingData,
|
||||
type BundleListingData,
|
||||
type PrivateGameTemplateListingData,
|
||||
type CourseListingData,
|
||||
} from '../../../../Utils/GDevelopServices/Shop';
|
||||
import { type SubscriptionPlanWithPricingSystems } from '../../../../Utils/GDevelopServices/Usage';
|
||||
import { extractGDevelopApiErrorStatusAndCode } from '../../../../Utils/GDevelopServices/Errors';
|
||||
import { Trans } from '@lingui/macro';
|
||||
import AlertMessage from '../../../../UI/AlertMessage';
|
||||
import {
|
||||
getProductsIncludedInBundle,
|
||||
getProductsIncludedInBundleTiles,
|
||||
} from '../../../../AssetStore/ProductPageHelper';
|
||||
import { PrivateGameTemplateStoreContext } from '../../../../AssetStore/PrivateGameTemplates/PrivateGameTemplateStoreContext';
|
||||
import { AssetStoreContext } from '../../../../AssetStore/AssetStoreContext';
|
||||
import AuthenticatedUserContext from '../../../../Profile/AuthenticatedUserContext';
|
||||
import { GridList, GridListTile } from '@material-ui/core';
|
||||
import { LARGE_WIDGET_SIZE } from '../CardWidget';
|
||||
import {
|
||||
useResponsiveWindowSize,
|
||||
type WindowSizeType,
|
||||
} from '../../../../UI/Responsive/ResponsiveWindowMeasurer';
|
||||
import Text from '../../../../UI/Text';
|
||||
import CourseStoreContext from '../../../../Course/CourseStoreContext';
|
||||
import CourseCard from './CourseCard';
|
||||
|
||||
const getColumns = (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 cellSpacing = 10;
|
||||
const MAX_COLUMNS = getColumns('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,
|
||||
overflow: 'hidden',
|
||||
width: `calc(100% + ${cellSpacing}px)`, // This is needed to compensate for the `margin: -5px` added by MUI related to spacing.
|
||||
},
|
||||
};
|
||||
|
||||
type Props = {|
|
||||
bundleListingData: BundleListingData,
|
||||
onBack: () => void,
|
||||
getSubscriptionPlansWithPricingSystems: () => Array<SubscriptionPlanWithPricingSystems> | null,
|
||||
onBundleOpen: BundleListingData => void,
|
||||
onGameTemplateOpen: PrivateGameTemplateListingData => void,
|
||||
onAssetPackOpen: (
|
||||
privateAssetPackListingData: PrivateAssetPackListingData
|
||||
) => void,
|
||||
onCourseOpen: CourseListingData => void,
|
||||
courses: ?Array<Course>,
|
||||
receivedCourses: ?Array<Course>,
|
||||
getCourseCompletion: (courseId: string) => CourseCompletion | null,
|
||||
|};
|
||||
|
||||
const BundlePage = ({
|
||||
bundleListingData,
|
||||
onBack,
|
||||
getSubscriptionPlansWithPricingSystems,
|
||||
onAssetPackOpen,
|
||||
onGameTemplateOpen,
|
||||
onBundleOpen,
|
||||
onCourseOpen,
|
||||
courses,
|
||||
receivedCourses,
|
||||
getCourseCompletion,
|
||||
}: Props) => {
|
||||
const { windowSize, isLandscape } = useResponsiveWindowSize();
|
||||
const { bundleListingDatas } = React.useContext(BundleStoreContext); // If archived, should use the one passed.
|
||||
const { privateGameTemplateListingDatas } = React.useContext(
|
||||
PrivateGameTemplateStoreContext
|
||||
);
|
||||
const { privateAssetPackListingDatas } = React.useContext(AssetStoreContext);
|
||||
const { listedCourses } = React.useContext(CourseStoreContext);
|
||||
const {
|
||||
receivedBundles,
|
||||
receivedGameTemplates,
|
||||
receivedAssetPacks,
|
||||
} = React.useContext(AuthenticatedUserContext);
|
||||
const [bundle, setBundle] = React.useState<?Bundle>(null);
|
||||
const [errorText, setErrorText] = React.useState<?React.Node>(null);
|
||||
|
||||
const courseAndTheirListingDataIncludedInBundle = React.useMemo(
|
||||
(): Array<{|
|
||||
course: Course,
|
||||
courseListingData: CourseListingData,
|
||||
|}> | null => {
|
||||
if (!bundle || !bundleListingData || !courses) return null;
|
||||
const productListingDatasInBundle = getProductsIncludedInBundle({
|
||||
productListingData: bundleListingData,
|
||||
productListingDatas: [...(listedCourses || [])],
|
||||
});
|
||||
|
||||
if (!productListingDatasInBundle) return null;
|
||||
// $FlowIgnore - Flow doesn't understand that we have filtered the products to only include courses.
|
||||
const courseListingDatasInBundle: CourseListingData[] = productListingDatasInBundle.filter(
|
||||
productListingData => productListingData.productType === 'COURSE'
|
||||
);
|
||||
|
||||
return (courseListingDatasInBundle || [])
|
||||
.map(courseListingData => {
|
||||
const course = courses.find(
|
||||
course => course.id === courseListingData.id
|
||||
);
|
||||
if (!course) return null;
|
||||
return {
|
||||
course,
|
||||
courseListingData,
|
||||
};
|
||||
})
|
||||
.filter(Boolean);
|
||||
},
|
||||
[bundle, bundleListingData, listedCourses, courses]
|
||||
);
|
||||
|
||||
const productsExceptCoursesIncludedInBundleTiles = React.useMemo(
|
||||
() =>
|
||||
bundle && bundleListingData
|
||||
? getProductsIncludedInBundleTiles({
|
||||
product: bundle,
|
||||
productListingDatas: [
|
||||
...(bundleListingDatas || []),
|
||||
...(privateGameTemplateListingDatas || []),
|
||||
...(privateAssetPackListingDatas || []),
|
||||
],
|
||||
productListingData: bundleListingData,
|
||||
receivedProducts: [
|
||||
...(receivedBundles || []),
|
||||
...(receivedGameTemplates || []),
|
||||
...(receivedAssetPacks || []),
|
||||
],
|
||||
onPrivateAssetPackOpen: onAssetPackOpen,
|
||||
onPrivateGameTemplateOpen: onGameTemplateOpen,
|
||||
onBundleOpen,
|
||||
onCourseOpen,
|
||||
})
|
||||
: null,
|
||||
[
|
||||
bundle,
|
||||
bundleListingDatas,
|
||||
privateGameTemplateListingDatas,
|
||||
privateAssetPackListingDatas,
|
||||
receivedBundles,
|
||||
receivedGameTemplates,
|
||||
receivedAssetPacks,
|
||||
bundleListingData,
|
||||
onAssetPackOpen,
|
||||
onGameTemplateOpen,
|
||||
onBundleOpen,
|
||||
onCourseOpen,
|
||||
]
|
||||
);
|
||||
|
||||
React.useEffect(
|
||||
() => {
|
||||
(async () => {
|
||||
try {
|
||||
const bundle = await getBundle(bundleListingData.id);
|
||||
|
||||
setBundle(bundle);
|
||||
} 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>
|
||||
);
|
||||
}
|
||||
}
|
||||
})();
|
||||
},
|
||||
[bundleListingData.id]
|
||||
);
|
||||
|
||||
if (errorText) {
|
||||
return (
|
||||
<SectionContainer flexBody backAction={onBack}>
|
||||
<SectionRow expand>
|
||||
<Line alignItems="center" justifyContent="center" expand>
|
||||
<AlertMessage kind="error">{errorText}</AlertMessage>
|
||||
</Line>
|
||||
</SectionRow>
|
||||
</SectionContainer>
|
||||
);
|
||||
}
|
||||
|
||||
if (!bundleListingData || !bundle) {
|
||||
return (
|
||||
<SectionContainer flexBody>
|
||||
<SectionRow expand>
|
||||
<PlaceholderLoader />
|
||||
</SectionRow>
|
||||
</SectionContainer>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<I18n>
|
||||
{({ i18n }) => (
|
||||
<SectionContainer
|
||||
applyTopSpacingAsMarginOnChildrenContainer
|
||||
backAction={onBack}
|
||||
>
|
||||
<Column noOverflowParent noMargin>
|
||||
<BundlePageHeader
|
||||
bundleListingData={bundleListingData}
|
||||
bundle={bundle}
|
||||
getSubscriptionPlansWithPricingSystems={
|
||||
getSubscriptionPlansWithPricingSystems
|
||||
}
|
||||
/>
|
||||
</Column>
|
||||
{courseAndTheirListingDataIncludedInBundle &&
|
||||
courseAndTheirListingDataIncludedInBundle.length > 0 && (
|
||||
<Line>
|
||||
<GridList
|
||||
cols={getColumns(windowSize, isLandscape)}
|
||||
style={styles.grid}
|
||||
cellHeight="auto"
|
||||
spacing={cellSpacing}
|
||||
>
|
||||
{courseAndTheirListingDataIncludedInBundle.map(
|
||||
({ course, courseListingData }) => {
|
||||
const completion = getCourseCompletion(course.id);
|
||||
return (
|
||||
<GridListTile key={course.id}>
|
||||
<CourseCard
|
||||
course={course}
|
||||
courseListingData={courseListingData}
|
||||
completion={completion}
|
||||
onClick={() => {
|
||||
onCourseOpen(courseListingData);
|
||||
}}
|
||||
/>
|
||||
</GridListTile>
|
||||
);
|
||||
}
|
||||
)}
|
||||
</GridList>
|
||||
</Line>
|
||||
)}
|
||||
{productsExceptCoursesIncludedInBundleTiles && (
|
||||
<>
|
||||
<Line>
|
||||
<Text size="block-title">
|
||||
<Trans>Also included in this bundle</Trans>
|
||||
</Text>
|
||||
</Line>
|
||||
<Line>
|
||||
<GridList
|
||||
cols={getColumns(windowSize, isLandscape)}
|
||||
cellHeight="auto"
|
||||
spacing={cellSpacing}
|
||||
style={styles.grid}
|
||||
>
|
||||
{productsExceptCoursesIncludedInBundleTiles}
|
||||
</GridList>
|
||||
</Line>
|
||||
</>
|
||||
)}
|
||||
</SectionContainer>
|
||||
)}
|
||||
</I18n>
|
||||
);
|
||||
};
|
||||
|
||||
export default BundlePage;
|
@@ -1,388 +0,0 @@
|
||||
// @flow
|
||||
|
||||
import * as React from 'react';
|
||||
import { Trans } from '@lingui/macro';
|
||||
import { I18n } from '@lingui/react';
|
||||
import { type Bundle } from '../../../../Utils/GDevelopServices/Asset';
|
||||
import { type BundleListingData } from '../../../../Utils/GDevelopServices/Shop';
|
||||
import { SectionRow } from '../SectionContainer';
|
||||
import Paper from '../../../../UI/Paper';
|
||||
import Text from '../../../../UI/Text';
|
||||
import { Column, Line } from '../../../../UI/Grid';
|
||||
import {
|
||||
ColumnStackLayout,
|
||||
LineStackLayout,
|
||||
ResponsiveLineStackLayout,
|
||||
} from '../../../../UI/Layout';
|
||||
import GDevelopThemeContext from '../../../../UI/Theme/GDevelopThemeContext';
|
||||
import { useResponsiveWindowSize } from '../../../../UI/Responsive/ResponsiveWindowMeasurer';
|
||||
import { selectMessageByLocale } from '../../../../Utils/i18n/MessageByLocale';
|
||||
import { renderProductPrice } from '../../../../AssetStore/ProductPriceTag';
|
||||
import {
|
||||
getProductsIncludedInBundle,
|
||||
getUserProductPurchaseUsageType,
|
||||
PurchaseProductButtons,
|
||||
} from '../../../../AssetStore/ProductPageHelper';
|
||||
import { shouldUseAppStoreProduct } from '../../../../Utils/AppStorePurchases';
|
||||
import { Divider } from '@material-ui/core';
|
||||
import AuthenticatedUserContext from '../../../../Profile/AuthenticatedUserContext';
|
||||
import { BundleStoreContext } from '../../../../AssetStore/Bundles/BundleStoreContext';
|
||||
import { sendBundleBuyClicked } from '../../../../Utils/Analytics/EventSender';
|
||||
import BundlePurchaseDialog from '../../../../AssetStore/Bundles/BundlePurchaseDialog';
|
||||
import RedemptionCodesDialog from '../../../../RedemptionCode/RedemptionCodesDialog';
|
||||
import { renderEstimatedTotalPriceFormatted } from '../../../../AssetStore/Bundles/Utils';
|
||||
import { PrivateGameTemplateStoreContext } from '../../../../AssetStore/PrivateGameTemplates/PrivateGameTemplateStoreContext';
|
||||
import {
|
||||
CreditsPackageStoreContext,
|
||||
getCreditsAmountFromId,
|
||||
} from '../../../../AssetStore/CreditsPackages/CreditsPackageStoreContext';
|
||||
import { AssetStoreContext } from '../../../../AssetStore/AssetStoreContext';
|
||||
import CourseStoreContext from '../../../../Course/CourseStoreContext';
|
||||
import SecureCheckout from '../../../../AssetStore/SecureCheckout/SecureCheckout';
|
||||
import {
|
||||
getPlanIcon,
|
||||
getPlanInferredNameFromId,
|
||||
} from '../../../../Profile/Subscription/PlanCard';
|
||||
import FlatButton from '../../../../UI/FlatButton';
|
||||
import Coin from '../../../../Credits/Icons/Coin';
|
||||
import { type SubscriptionPlanWithPricingSystems } from '../../../../Utils/GDevelopServices/Usage';
|
||||
import { formatDurationOfRedemptionCode } from '../../../../RedemptionCode/Utils';
|
||||
|
||||
const styles = {
|
||||
title: { overflowWrap: 'anywhere', textWrap: 'wrap' },
|
||||
image: { width: 300, aspectRatio: '16 / 9' },
|
||||
imageContainer: {
|
||||
display: 'flex',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
position: 'relative',
|
||||
borderRadius: 8,
|
||||
overflow: 'hidden',
|
||||
},
|
||||
discountedPrice: { textDecoration: 'line-through', opacity: 0.7 },
|
||||
coinIcon: {
|
||||
width: 13,
|
||||
height: 13,
|
||||
position: 'relative',
|
||||
top: -1,
|
||||
},
|
||||
};
|
||||
|
||||
const ResponsiveDivider = () => {
|
||||
const { isMobile, isMediumScreen } = useResponsiveWindowSize();
|
||||
return isMobile || isMediumScreen ? (
|
||||
<Column noMargin>
|
||||
<Divider orientation="horizontal" />
|
||||
</Column>
|
||||
) : (
|
||||
<Line noMargin>
|
||||
<Divider orientation="vertical" />
|
||||
</Line>
|
||||
);
|
||||
};
|
||||
|
||||
type Props = {|
|
||||
bundleListingData: BundleListingData,
|
||||
bundle: Bundle,
|
||||
getSubscriptionPlansWithPricingSystems: () => Array<SubscriptionPlanWithPricingSystems> | null,
|
||||
simulateAppStoreProduct?: boolean,
|
||||
|};
|
||||
|
||||
const BundlePageHeader = ({
|
||||
bundle,
|
||||
bundleListingData,
|
||||
getSubscriptionPlansWithPricingSystems,
|
||||
simulateAppStoreProduct,
|
||||
}: Props) => {
|
||||
const gdevelopTheme = React.useContext(GDevelopThemeContext);
|
||||
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 { receivedBundles, bundlePurchases } = authenticatedUser;
|
||||
const [
|
||||
purchasingBundleListingData,
|
||||
setPurchasingBundleListingData,
|
||||
] = React.useState<?BundleListingData>(null);
|
||||
const { isMobile, isMediumScreen } = useResponsiveWindowSize();
|
||||
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 productListingDatasIncludedInBundle = React.useMemo(
|
||||
() =>
|
||||
bundleListingData &&
|
||||
bundleListingDatas &&
|
||||
privateGameTemplateListingDatas &&
|
||||
privateAssetPackListingDatas &&
|
||||
listedCourses &&
|
||||
creditsPackageListingDatas
|
||||
? getProductsIncludedInBundle({
|
||||
productListingDatas: [
|
||||
...bundleListingDatas,
|
||||
...privateGameTemplateListingDatas,
|
||||
...privateAssetPackListingDatas,
|
||||
...listedCourses,
|
||||
...creditsPackageListingDatas,
|
||||
],
|
||||
productListingData: bundleListingData,
|
||||
})
|
||||
: null,
|
||||
[
|
||||
bundleListingData,
|
||||
bundleListingDatas,
|
||||
privateGameTemplateListingDatas,
|
||||
privateAssetPackListingDatas,
|
||||
listedCourses,
|
||||
creditsPackageListingDatas,
|
||||
]
|
||||
);
|
||||
|
||||
const subscriptionPlansWithPricingSystems = getSubscriptionPlansWithPricingSystems();
|
||||
|
||||
const redemptionCodesIncludedInBundle = React.useMemo(
|
||||
() =>
|
||||
bundleListingData
|
||||
? bundleListingData.includedRedemptionCodes || []
|
||||
: null,
|
||||
[bundleListingData]
|
||||
);
|
||||
|
||||
const includedCreditsAmount = React.useMemo(
|
||||
() =>
|
||||
(bundleListingData.includedListableProducts || [])
|
||||
.filter(product => product.productType === 'CREDIT_PACKAGE')
|
||||
.reduce(
|
||||
(total, product) => total + getCreditsAmountFromId(product.productId),
|
||||
0
|
||||
),
|
||||
[bundleListingData]
|
||||
);
|
||||
|
||||
const onClickBuy = React.useCallback(
|
||||
async () => {
|
||||
if (!bundle) return;
|
||||
if (isAlreadyReceived) {
|
||||
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]
|
||||
);
|
||||
|
||||
return (
|
||||
<I18n>
|
||||
{({ i18n }) => (
|
||||
<>
|
||||
<SectionRow>
|
||||
<Paper background="dark" variant="outlined" style={{ padding: 16 }}>
|
||||
<ColumnStackLayout noMargin>
|
||||
<ResponsiveLineStackLayout
|
||||
noMargin
|
||||
alignItems="center"
|
||||
justifyContent="flex-start"
|
||||
forceMobileLayout={isMediumScreen}
|
||||
expand
|
||||
>
|
||||
<div style={styles.imageContainer}>
|
||||
<img
|
||||
src={bundle.previewImageUrls[0]}
|
||||
style={styles.image}
|
||||
alt=""
|
||||
/>
|
||||
</div>
|
||||
<ColumnStackLayout expand justifyContent="flex-start">
|
||||
<Text size="title" noMargin style={styles.title}>
|
||||
{selectMessageByLocale(i18n, bundle.nameByLocale)}
|
||||
</Text>
|
||||
<Line noMargin>
|
||||
<Text noMargin>
|
||||
{selectMessageByLocale(
|
||||
i18n,
|
||||
bundle.longDescriptionByLocale
|
||||
)}
|
||||
</Text>
|
||||
</Line>
|
||||
</ColumnStackLayout>
|
||||
</ResponsiveLineStackLayout>
|
||||
<ResponsiveLineStackLayout
|
||||
expand
|
||||
justifyContent="space-between"
|
||||
forceMobileLayout={isMediumScreen}
|
||||
>
|
||||
{redemptionCodesIncludedInBundle &&
|
||||
redemptionCodesIncludedInBundle.length > 0 && (
|
||||
<ColumnStackLayout noMargin expand>
|
||||
{redemptionCodesIncludedInBundle.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 && (
|
||||
<Column justifyContent="center" expand noMargin>
|
||||
<LineStackLayout noMargin alignItems="center">
|
||||
<Coin style={styles.coinIcon} />
|
||||
<Text>
|
||||
<Trans>
|
||||
{includedCreditsAmount} credits included
|
||||
</Trans>
|
||||
</Text>
|
||||
</LineStackLayout>
|
||||
</Column>
|
||||
)}
|
||||
<ResponsiveDivider />
|
||||
</ResponsiveLineStackLayout>
|
||||
{!isAlreadyReceived && (
|
||||
<Paper background="medium" style={{ padding: 16 }}>
|
||||
{!!bundleListingData && (
|
||||
<ResponsiveLineStackLayout
|
||||
justifyContent="space-between"
|
||||
noMargin
|
||||
>
|
||||
{!isMobile && !isMediumScreen && (
|
||||
<Column noMargin justifyContent="center">
|
||||
<LineStackLayout noMargin>
|
||||
<Text noMargin color="secondary">
|
||||
<span style={styles.discountedPrice}>
|
||||
{renderEstimatedTotalPriceFormatted({
|
||||
i18n,
|
||||
bundleListingData,
|
||||
productListingDatasIncludedInBundle,
|
||||
redemptionCodesIncludedInBundle,
|
||||
subscriptionPlansWithPricingSystems,
|
||||
})}
|
||||
</span>
|
||||
</Text>
|
||||
<div
|
||||
style={{
|
||||
color: gdevelopTheme.text.color.secondary,
|
||||
}}
|
||||
>
|
||||
{renderProductPrice({
|
||||
i18n,
|
||||
productListingData: bundleListingData,
|
||||
usageType: 'default',
|
||||
})}
|
||||
</div>
|
||||
</LineStackLayout>
|
||||
</Column>
|
||||
)}
|
||||
<ResponsiveLineStackLayout
|
||||
noMargin
|
||||
forceMobileLayout={isMediumScreen}
|
||||
>
|
||||
{!shouldUseOrSimulateAppStoreProduct && (
|
||||
<SecureCheckout />
|
||||
)}
|
||||
<PurchaseProductButtons
|
||||
i18n={i18n}
|
||||
productListingData={bundleListingData}
|
||||
selectedUsageType="default"
|
||||
onUsageTypeChange={() => {}}
|
||||
simulateAppStoreProduct={simulateAppStoreProduct}
|
||||
isAlreadyReceived={isAlreadyReceived}
|
||||
onClickBuy={onClickBuy}
|
||||
onClickBuyWithCredits={() => {}}
|
||||
/>
|
||||
</ResponsiveLineStackLayout>
|
||||
</ResponsiveLineStackLayout>
|
||||
)}
|
||||
</Paper>
|
||||
)}
|
||||
</ColumnStackLayout>
|
||||
</Paper>
|
||||
</SectionRow>
|
||||
{!!purchasingBundleListingData && (
|
||||
<BundlePurchaseDialog
|
||||
bundleListingData={purchasingBundleListingData}
|
||||
usageType="default"
|
||||
onClose={() => setPurchasingBundleListingData(null)}
|
||||
/>
|
||||
)}
|
||||
{isRedemptionCodesDialogOpen && (
|
||||
<RedemptionCodesDialog
|
||||
onClose={() => setIsRedemptionCodesDialogOpen(false)}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</I18n>
|
||||
);
|
||||
};
|
||||
|
||||
export default BundlePageHeader;
|
@@ -131,6 +131,7 @@ type Props = {|
|
||||
course: ?Course,
|
||||
courseListingData: ?CourseListingData,
|
||||
onClick?: () => void,
|
||||
discountedPrice?: boolean,
|
||||
|};
|
||||
|
||||
const CourseCard = ({
|
||||
@@ -138,6 +139,7 @@ const CourseCard = ({
|
||||
course,
|
||||
courseListingData,
|
||||
onClick,
|
||||
discountedPrice,
|
||||
}: Props) => {
|
||||
const gdevelopTheme = React.useContext(GDevelopThemeContext);
|
||||
const specializationConfig = getSpecializationConfig(
|
||||
@@ -255,6 +257,7 @@ const CourseCard = ({
|
||||
usageType: 'default',
|
||||
showBothPrices: 'column',
|
||||
owned: !course.isLocked,
|
||||
discountedPrice,
|
||||
})}
|
||||
</Line>
|
||||
</div>
|
||||
|
@@ -39,6 +39,7 @@ import CoursePageHeader from './CoursePageHeader';
|
||||
import Window from '../../../../Utils/Window';
|
||||
import AuthenticatedUserContext from '../../../../Profile/AuthenticatedUserContext';
|
||||
import { RatingBanner } from './RatingBanner';
|
||||
import { selectMessageByLocale } from '../../../../Utils/i18n/MessageByLocale';
|
||||
|
||||
const styles = {
|
||||
desktopContainer: { display: 'flex', gap: 16 },
|
||||
@@ -353,8 +354,20 @@ const CoursePage = ({
|
||||
</AlertMessage>
|
||||
</Line>
|
||||
)}
|
||||
{course.introByLocale && (
|
||||
<Line>
|
||||
<AlertMessage kind="info" background="light">
|
||||
{selectMessageByLocale(i18n, course.introByLocale)}
|
||||
</AlertMessage>
|
||||
</Line>
|
||||
)}
|
||||
{courseChapters.map((chapter: CourseChapter, index) => (
|
||||
<ColumnStackLayout expand noOverflowParent noMargin>
|
||||
<ColumnStackLayout
|
||||
expand
|
||||
noOverflowParent
|
||||
noMargin
|
||||
key={chapter.id}
|
||||
>
|
||||
{chapter.videoUrl ? (
|
||||
<VideoBasedCourseChapterView
|
||||
chapterIndex={index}
|
||||
|
@@ -276,9 +276,6 @@ const CoursePageHeader = ({
|
||||
noMargin
|
||||
forceMobileLayout={isMediumScreen}
|
||||
>
|
||||
{!shouldUseOrSimulateAppStoreProduct && (
|
||||
<SecureCheckout />
|
||||
)}
|
||||
<PurchaseProductButtons
|
||||
i18n={i18n}
|
||||
productListingData={courseListingData}
|
||||
@@ -295,6 +292,9 @@ const CoursePageHeader = ({
|
||||
onWillBuyWithCredits(i18n)
|
||||
}
|
||||
/>
|
||||
{!shouldUseOrSimulateAppStoreProduct && (
|
||||
<SecureCheckout />
|
||||
)}
|
||||
</ResponsiveLineStackLayout>
|
||||
</ResponsiveLineStackLayout>
|
||||
)}
|
||||
|
@@ -129,7 +129,11 @@ const CoursesPage = ({
|
||||
</SectionRow>
|
||||
{!hidePremiumProducts && (
|
||||
<SectionRow>
|
||||
<BundlePreviewBanner onDisplayBundle={onSelectBundle} />
|
||||
<BundlePreviewBanner
|
||||
onDisplayBundle={onSelectBundle}
|
||||
category="starter"
|
||||
i18n={i18n}
|
||||
/>
|
||||
</SectionRow>
|
||||
)}
|
||||
{courses && listedCourses && courses.length > numberOfItemsOnOneRow && (
|
||||
@@ -141,28 +145,72 @@ const CoursesPage = ({
|
||||
cellHeight="auto"
|
||||
spacing={ITEMS_SPACING * 2}
|
||||
>
|
||||
{courses.slice(numberOfItemsOnOneRow).map(course => {
|
||||
const completion = getCourseCompletion(course.id);
|
||||
const courseListingData = listedCourses.find(
|
||||
listedCourse => listedCourse.id === course.id
|
||||
);
|
||||
return (
|
||||
<GridListTile key={course.id}>
|
||||
<CourseCard
|
||||
course={course}
|
||||
courseListingData={courseListingData}
|
||||
completion={completion}
|
||||
onClick={() => {
|
||||
onSelectCourse(course.id);
|
||||
}}
|
||||
/>
|
||||
</GridListTile>
|
||||
);
|
||||
})}
|
||||
{courses
|
||||
.slice(numberOfItemsOnOneRow, 2 * numberOfItemsOnOneRow)
|
||||
.map(course => {
|
||||
const completion = getCourseCompletion(course.id);
|
||||
const courseListingData = listedCourses.find(
|
||||
listedCourse => listedCourse.id === course.id
|
||||
);
|
||||
return (
|
||||
<GridListTile key={course.id}>
|
||||
<CourseCard
|
||||
course={course}
|
||||
courseListingData={courseListingData}
|
||||
completion={completion}
|
||||
onClick={() => {
|
||||
onSelectCourse(course.id);
|
||||
}}
|
||||
/>
|
||||
</GridListTile>
|
||||
);
|
||||
})}
|
||||
</GridList>
|
||||
</Line>
|
||||
</SectionRow>
|
||||
)}
|
||||
{!hidePremiumProducts && (
|
||||
<SectionRow>
|
||||
<BundlePreviewBanner
|
||||
onDisplayBundle={onSelectBundle}
|
||||
category="premium"
|
||||
i18n={i18n}
|
||||
/>
|
||||
</SectionRow>
|
||||
)}
|
||||
{courses &&
|
||||
listedCourses &&
|
||||
courses.length > 2 * numberOfItemsOnOneRow && (
|
||||
<SectionRow>
|
||||
<Line>
|
||||
<GridList
|
||||
cols={numberOfItemsOnOneRow}
|
||||
style={styles.grid}
|
||||
cellHeight="auto"
|
||||
spacing={ITEMS_SPACING * 2}
|
||||
>
|
||||
{courses.slice(2 * numberOfItemsOnOneRow).map(course => {
|
||||
const completion = getCourseCompletion(course.id);
|
||||
const courseListingData = listedCourses.find(
|
||||
listedCourse => listedCourse.id === course.id
|
||||
);
|
||||
return (
|
||||
<GridListTile key={course.id}>
|
||||
<CourseCard
|
||||
course={course}
|
||||
courseListingData={courseListingData}
|
||||
completion={completion}
|
||||
onClick={() => {
|
||||
onSelectCourse(course.id);
|
||||
}}
|
||||
/>
|
||||
</GridListTile>
|
||||
);
|
||||
})}
|
||||
</GridList>
|
||||
</Line>
|
||||
</SectionRow>
|
||||
)}
|
||||
</SectionContainer>
|
||||
)}
|
||||
</I18n>
|
||||
|
@@ -240,7 +240,11 @@ const MainPage = ({
|
||||
</SectionRow>
|
||||
{!hidePremiumProducts && (
|
||||
<SectionRow>
|
||||
<BundlePreviewBanner onDisplayBundle={onSelectBundle} />
|
||||
<BundlePreviewBanner
|
||||
onDisplayBundle={onSelectBundle}
|
||||
category="starter"
|
||||
i18n={i18n}
|
||||
/>
|
||||
</SectionRow>
|
||||
)}
|
||||
<SectionRow>
|
||||
@@ -252,7 +256,7 @@ const MainPage = ({
|
||||
>
|
||||
<Column noMargin>
|
||||
<Text size="section-title">
|
||||
<Trans>In-app tutorials</Trans>
|
||||
<Trans>Free in-app tutorials</Trans>
|
||||
</Text>
|
||||
</Column>
|
||||
<Column noMargin>
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user