Compare commits

..

28 Commits

Author SHA1 Message Date
Davy Hélard
55fc686a5b Fix expression default type. 2022-10-29 22:02:37 +02:00
Davy Hélard
5580189f88 Remove unused imports. 2022-10-29 20:49:29 +02:00
Davy Hélard
9a6d1d6d32 Remove useless lines added by mistake. 2022-10-29 19:42:54 +02:00
Davy Hélard
6c9739c01d Fix object parameter extraInfo declaration. 2022-10-29 19:41:07 +02:00
Davy Hélard
cd3c997b28 Review changes: more renaming 2022-10-29 17:53:15 +02:00
Davy Hélard
8064c4de57 Review changes: renaming 2022-10-29 14:54:09 +02:00
Davy Hélard
8666851f54 Documentation. 2022-10-29 13:40:53 +02:00
Davy Hélard
6d568b2f2c Fix type conversion to expressionType. 2022-10-29 13:40:52 +02:00
Davy Hélard
180d4318aa Add ValueTypeEditor to edit type definitions. 2022-10-29 13:40:52 +02:00
Davy Hélard
65a57f86da Allow to define type extraInfo for operands. 2022-10-29 13:40:52 +02:00
Davy Hélard
6a3e7f9c58 Define expression types with ValueTypeMetadata. 2022-10-29 13:40:52 +02:00
Davy Hélard
74f1d571ba Extract ValueTypeMetadata from ParameterMetadata. 2022-10-29 13:40:51 +02:00
Davy Hélard
a7cb3fc5a2 Add some comment about parameter types. 2022-10-29 13:40:51 +02:00
Davy Hélard
08d3c3323a Move out changes to mapFor. 2022-10-29 13:40:51 +02:00
Davy Hélard
fec603b811 Format 2022-10-29 13:40:51 +02:00
Davy Hélard
d68affc117 Add tests for shiftSentenceParamIndexes 2022-10-29 13:40:50 +02:00
Davy Hélard
77cd6c44d6 Review change: memory leak in test. 2022-10-29 13:40:50 +02:00
Davy Hélard
2b4c8813e4 Review changes 2022-10-29 13:40:50 +02:00
Davy Hélard
2ef9266ec4 Disable the "Add parameter" button for ActionWithOperator. 2022-10-29 13:40:49 +02:00
Davy Hélard
ceba6cf739 Split function types and returned types in 2 drop-down lists. 2022-10-29 13:40:49 +02:00
Davy Hélard
18f2085de7 Better disable parameters in UI. 2022-10-29 13:40:49 +02:00
Davy Hélard
b586fb87ed Show the right parameters in the editor. 2022-10-29 13:40:49 +02:00
Davy Hélard
8aed02ab17 Allow event extensions to define an action with an operator. 2022-10-29 13:40:48 +02:00
Davy Hélard
8c383fc448 Cleanup stories imports. 2022-10-29 13:40:48 +02:00
Davy Hélard
af3a2016f2 Fix flow. 2022-10-29 13:40:48 +02:00
Davy Hélard
7a20161794 Use the icon of expressions. 2022-10-29 13:40:47 +02:00
Davy Hélard
0feb4ef321 Put operator and operand first. 2022-10-29 13:40:47 +02:00
Davy Hélard
5cbcd16523 Allow event extensions to define a condition with an operator from an expression. 2022-10-29 13:40:47 +02:00
181 changed files with 2641 additions and 2693 deletions

View File

@@ -670,18 +670,6 @@ gd::String EventsCodeGenerator::GenerateActionsListCode(
return outputCode;
}
const gd::String EventsCodeGenerator::GenerateRelationalOperatorCodes(const gd::String &operatorString) {
if (operatorString == "=") {
return "==";
}
if (operatorString != "<" && operatorString != ">" &&
operatorString != "<=" && operatorString != ">=" && operatorString != "!=") {
cout << "Warning: Bad relational operator: Set to == by default." << endl;
return "==";
}
return operatorString;
}
gd::String EventsCodeGenerator::GenerateParameterCodes(
const gd::Expression& parameter,
const gd::ParameterMetadata& metadata,
@@ -706,7 +694,14 @@ gd::String EventsCodeGenerator::GenerateParameterCodes(
argOutput =
GenerateObject(parameter.GetPlainString(), metadata.GetType(), context);
} else if (metadata.GetType() == "relationalOperator") {
argOutput += GenerateRelationalOperatorCodes(parameter.GetPlainString());
auto parameterString = parameter.GetPlainString();
argOutput += parameterString == "=" ? "==" : parameterString;
if (argOutput != "==" && argOutput != "<" && argOutput != ">" &&
argOutput != "<=" && argOutput != ">=" && argOutput != "!=") {
cout << "Warning: Bad relational operator: Set to == by default." << endl;
argOutput = "==";
}
argOutput = "\"" + argOutput + "\"";
} else if (metadata.GetType() == "operator") {
argOutput += parameter.GetPlainString();

View File

@@ -481,9 +481,6 @@ class GD_CORE_API EventsCodeGenerator {
*/
size_t GenerateSingleUsageUniqueIdForEventsList();
virtual const gd::String GenerateRelationalOperatorCodes(
const gd::String& operatorString);
protected:
/**
* \brief Generate the code for a single parameter.

View File

@@ -81,7 +81,7 @@ void GD_CORE_API BuiltinExtensionsImplementer::ImplementsAdvancedExtension(
.AddExpression(
"GetArgumentAsNumber",
_("Get function parameter value"),
_("Get function parameter (also called \"argument\") value."),
_("Get function parameter (also called \"argument\") value"),
"",
"res/function16.png")
.AddParameter("functionParameterName", "Parameter name");
@@ -90,34 +90,10 @@ void GD_CORE_API BuiltinExtensionsImplementer::ImplementsAdvancedExtension(
.AddStrExpression(
"GetArgumentAsString",
_("Get function parameter text"),
_("Get function parameter (also called \"argument\") text."),
_("Get function parameter (also called \"argument\") text "),
"",
"res/function16.png")
.AddParameter("functionParameterName", "Parameter name");
extension
.AddCondition(
"CompareArgumentAsNumber",
_("Compare function parameter value"),
_("Compare function parameter (also called \"argument\") value."),
_("Parameter _PARAM0_"),
"",
"res/function32.png",
"res/function16.png")
.AddParameter("functionParameterName", "Parameter name")
.UseStandardRelationalOperatorParameters("number");
extension
.AddCondition(
"CompareArgumentAsString",
_("Compare function parameter text"),
_("Compare function parameter (also called \"argument\") text."),
_("Parameter _PARAM0_"),
"",
"res/function32.png",
"res/function16.png")
.AddParameter("functionParameterName", "Parameter name")
.UseStandardRelationalOperatorParameters("string");
}
} // namespace gd

View File

@@ -96,7 +96,7 @@ BuiltinExtensionsImplementer::ImplementsCommonConversionsExtension(
"JSONToVariableStructure",
_("Convert JSON to a scene variable"),
_("Parse a JSON object and store it into a scene variable"),
_("Convert JSON string _PARAM0_ and store it into variable _PARAM1_"),
_("Parse JSON string _PARAM0_ and store it into variable _PARAM1_"),
"",
"res/actions/net24.png",
"res/actions/net.png")
@@ -108,7 +108,7 @@ BuiltinExtensionsImplementer::ImplementsCommonConversionsExtension(
.AddAction("JSONToGlobalVariableStructure",
_("Convert JSON to global variable"),
_("Parse a JSON object and store it into a global variable"),
_("Convert JSON string _PARAM0_ and store it into global "
_("Parse JSON string _PARAM0_ and store it into global "
"variable _PARAM1_"),
"",
"res/actions/net24.png",

View File

@@ -324,19 +324,6 @@ void GD_CORE_API BuiltinExtensionsImplementer::ImplementsSpriteExtension(
"res/conditions/animation24.png",
"res/conditions/animation.png")
.AddParameter("object", _("Object"), "Sprite")
.MarkAsSimple()
.SetHidden();
obj.AddCondition("AnimationEnded2",
_("Animation finished"),
_("Check if the animation being played by the Sprite object "
"is finished."),
_("The animation of _PARAM0_ is finished"),
_("Animations and images"),
"res/conditions/animation24.png",
"res/conditions/animation.png")
.AddParameter("object", _("Object"), "Sprite")
.MarkAsSimple();

View File

@@ -236,6 +236,7 @@ class GD_CORE_API BehaviorMetadata {
}
const gd::String& GetName() const;
#if defined(GD_IDE_ONLY)
const gd::String& GetFullName() const { return fullname; }
const gd::String& GetDefaultName() const { return defaultName; }
const gd::String& GetDescription() const { return description; }
@@ -256,21 +257,7 @@ class GD_CORE_API BehaviorMetadata {
* \note An empty string means the base object, so any object.
*/
const gd::String& GetObjectType() const { return objectType; }
/**
* Check if the behavior is private - it can't be used outside of its
* extension.
*/
bool IsPrivate() const { return isPrivate; }
/**
* Set that the behavior is private - it can't be used outside of its
* extension.
*/
BehaviorMetadata &SetPrivate() {
isPrivate = true;
return *this;
}
#endif
/**
* \brief Return the associated gd::Behavior, handling behavior contents.
@@ -328,7 +315,6 @@ class GD_CORE_API BehaviorMetadata {
gd::String group;
gd::String iconFilename;
gd::String objectType;
bool isPrivate = false;
// TODO: Nitpicking: convert these to std::unique_ptr to clarify ownership.
std::shared_ptr<gd::Behavior> instance;

View File

@@ -15,12 +15,9 @@ ParameterMetadata::ParameterMetadata() : codeOnly(false) {}
void ParameterMetadata::SerializeTo(SerializerElement& element) const {
valueTypeMetadata.SerializeTo(element);
element.SetAttribute("description", description);
if (!longDescription.empty()) {
element.SetAttribute("longDescription", longDescription);
}
if (codeOnly) {
element.SetAttribute("codeOnly", codeOnly);
}
element.SetAttribute("longDescription", longDescription);
element.SetAttribute("codeOnly", codeOnly);
element.SetAttribute("defaultValue", defaultValue);
element.SetAttribute("name", name);
}
@@ -29,6 +26,7 @@ void ParameterMetadata::UnserializeFrom(const SerializerElement& element) {
description = element.GetStringAttribute("description");
longDescription = element.GetStringAttribute("longDescription");
codeOnly = element.GetBoolAttribute("codeOnly");
defaultValue = element.GetStringAttribute("defaultValue");
name = element.GetStringAttribute("name");
}

View File

@@ -144,15 +144,13 @@ class GD_CORE_API ParameterMetadata {
/**
* \brief Get the default value for the parameter.
*/
const gd::String &GetDefaultValue() const {
return valueTypeMetadata.GetDefaultValue();
}
const gd::String &GetDefaultValue() const { return defaultValue; }
/**
* \brief Set the default value, if the parameter is optional.
*/
ParameterMetadata &SetDefaultValue(const gd::String &defaultValue_) {
valueTypeMetadata.SetDefaultValue(defaultValue_);
defaultValue = defaultValue_;
return *this;
}
@@ -238,6 +236,8 @@ class GD_CORE_API ParameterMetadata {
private:
gd::ValueTypeMetadata valueTypeMetadata; ///< Parameter type
gd::String longDescription; ///< Long description shown in the editor.
gd::String defaultValue; ///< Used as a default value in editor or if an
///< optional parameter is empty.
gd::String name; ///< The name of the parameter to be used in code
///< generation. Optional.
};

View File

@@ -18,16 +18,12 @@ EventsBasedBehavior::EventsBasedBehavior()
void EventsBasedBehavior::SerializeTo(SerializerElement& element) const {
AbstractEventsBasedEntity::SerializeTo(element);
element.SetAttribute("objectType", objectType);
if (isPrivate) {
element.SetBoolAttribute("private", isPrivate);
}
}
void EventsBasedBehavior::UnserializeFrom(gd::Project& project,
const SerializerElement& element) {
AbstractEventsBasedEntity::UnserializeFrom(project, element);
objectType = element.GetStringAttribute("objectType");
isPrivate = element.GetBoolAttribute("private");
}
} // namespace gd

View File

@@ -73,21 +73,6 @@ class GD_CORE_API EventsBasedBehavior: public AbstractEventsBasedEntity {
return *this;
}
/**
* \brief Check if the behavior is private - it can't be used outside of its
* extension.
*/
bool IsPrivate() { return isPrivate; }
/**
* \brief Set that the behavior is private - it can't be used outside of its
* extension.
*/
EventsBasedBehavior& SetPrivate(bool _isPrivate) {
isPrivate = _isPrivate;
return *this;
}
void SerializeTo(SerializerElement& element) const override;
void UnserializeFrom(gd::Project& project,
@@ -95,7 +80,6 @@ class GD_CORE_API EventsBasedBehavior: public AbstractEventsBasedEntity {
private:
gd::String objectType;
bool isPrivate = false;
};
} // namespace gd

View File

@@ -59,19 +59,11 @@ const std::vector<gd::ParameterMetadata>& EventsFunction::GetParametersForEvents
void EventsFunction::SerializeTo(SerializerElement& element) const {
element.SetAttribute("name", name);
element.SetAttribute("fullName", fullName);
if (!description.empty()) {
element.SetAttribute("description", description);
}
element.SetAttribute("description", description);
element.SetAttribute("sentence", sentence);
if (!group.empty()) {
element.SetAttribute("group", group);
}
if (!getterName.empty()) {
element.SetAttribute("getterName", getterName);
}
if (isPrivate) {
element.SetBoolAttribute("private", isPrivate);
}
element.SetAttribute("group", group);
element.SetAttribute("getterName", getterName);
element.SetBoolAttribute("private", isPrivate);
events.SerializeTo(element.AddChild("events"));
gd::String functionTypeStr = "Action";

View File

@@ -62,6 +62,16 @@ describe('gdjs.AnchorRuntimeBehavior', function () {
return object;
}
function getAnchorBehavior(object) {
const behavior = object.getBehavior(anchorBehaviorName);
if (!(behavior instanceof gdjs.AnchorRuntimeBehavior)) {
throw new Error(
'Expected behavior to be an instance of gdjs.AnchorBehavior'
);
}
return behavior;
}
describe('(anchor horizontal edge)', function () {
['rightEdgeAnchor', 'leftEdgeAnchor'].forEach((objectEdge) => {
it(`anchors the ${objectEdge} edge of object to window left (fixed)`, function () {

View File

@@ -239,14 +239,14 @@ describe('gdjs.LinksManager', function () {
manager.removeAllLinksOf(object1A);
manager.removeAllLinksOf(object1A);
{
const { pickedSomething } = pickObjectsLinkedTo(
const { pickedSomething, objectsLists } = pickObjectsLinkedTo(
object1A,
Hashtable.newFrom({ obj2: [object2A, object2B, object2C] })
);
expect(pickedSomething).to.be(false);
}
{
const { pickedSomething } = pickObjectsLinkedTo(
const { pickedSomething, objectsLists } = pickObjectsLinkedTo(
object2A,
Hashtable.newFrom({ obj1: [object1A, object1B, object1C] })
);

View File

@@ -648,19 +648,6 @@ void DeclarePlatformBehaviorExtension(gd::PlatformExtension& extension) {
.UseStandardOperatorParameters("number")
.MarkAsAdvanced();
aut.AddScopedAction("FollowCurrentPlatform",
_("Follow the floor"),
_("Move the object to follow the platform it's currently"
"on if any. This action allows to avoid the 1-frame "
"delay induced by the automatic following."),
_("Move _PARAM0_ to follow the floor"),
_(""),
"CppPlatform/Extensions/platformerobjecticon.png",
"CppPlatform/Extensions/platformerobjecticon.png")
.AddParameter("object", _("Object"))
.AddParameter("behavior", _("Behavior"), "PlatformerObjectBehavior")
.MarkAsAdvanced();
aut.AddCondition("CurrentSpeed",
_("Current horizontal speed"),
_("Compare the current horizontal speed of the object "

View File

@@ -131,14 +131,10 @@ class PlatformBehaviorJsExtension : public gd::PlatformExtension {
"getCurrentJumpSpeed");
autExpressions["CurrentJumpSpeed"].SetFunctionName("getCurrentJumpSpeed");
autActions["PlatformBehavior::SetCanJump"].SetFunctionName("setCanJump");
autActions["PlatformBehavior::PlatformerObjectBehavior::SetCanNotAirJump"]
.SetFunctionName("setCanNotAirJump");
autActions["PlatformBehavior::PlatformerObjectBehavior::AbortJump"]
.SetFunctionName("abortJump");
autActions
["PlatformBehavior::PlatformerObjectBehavior::FollowCurrentPlatform"]
.SetFunctionName("followCurrentPlatformIfAny");
autConditions["PlatformBehavior::CanJump"].SetFunctionName("canJump");
autActions["PlatformBehavior::PlatformerObjectBehavior::SetCanNotAirJump"].SetFunctionName("setCanNotAirJump");
autActions["PlatformBehavior::PlatformerObjectBehavior::AbortJump"].SetFunctionName("abortJump");
autConditions["PlatformBehavior::CanJump"].SetFunctionName(
"canJump");
autActions["PlatformBehavior::SimulateLeftKey"].SetFunctionName(
"simulateLeftKey");
autActions["PlatformBehavior::SimulateRightKey"].SetFunctionName(

View File

@@ -1143,14 +1143,6 @@ namespace gdjs {
return this._gravity;
}
/**
* Get maximum angle of a slope for the Platformer Object to run on it as a floor.
* @returns the slope maximum angle, in degrees.
*/
getSlopeMaxAngle(): float {
return this._slopeMaxAngle;
}
/**
* Get the maximum falling speed of the Platformer Object.
* @returns The maximum falling speed.
@@ -1592,17 +1584,6 @@ namespace gdjs {
this._currentFallSpeed !== 0
);
}
followCurrentPlatformIfAny(): boolean {
let hasMoved = false;
if (this.isOnFloor()) {
const timeDelta = this.owner.getElapsedTime() / 1000;
const hasMovedX = this._onFloor.followCurrentPlatformOnX(timeDelta);
const hasMovedY = this._onFloor.followCurrentPlatformOnY(timeDelta);
hasMoved = hasMovedX || hasMovedY;
}
return hasMoved;
}
}
/**
@@ -1677,11 +1658,6 @@ namespace gdjs {
}
beforeUpdatingObstacles(timeDelta: float) {
this.followCurrentPlatformOnY(timeDelta);
}
followCurrentPlatformOnY(timeDelta: float): boolean {
let hasMoved = false;
const object = this._behavior.owner;
//Stick the object to the floor if its height has changed.
if (this._oldHeight !== object.getHeight()) {
@@ -1690,7 +1666,6 @@ namespace gdjs {
object.getHeight() +
(object.getY() - object.getDrawableY())
);
hasMoved = true;
}
// Directly follow the floor movement on the Y axis by moving the character.
// For the X axis, we follow the floor movement using `_requestedDeltaX`
@@ -1711,18 +1686,14 @@ namespace gdjs {
// and the platform can go out of the spatial search rectangle
// even though they are next to each other, which means
// that the character will fall.
const floorY = this._floorPlatform!.owner.getY();
const deltaY = floorY - this._floorLastY;
const deltaY = this._floorPlatform!.owner.getY() - this._floorLastY;
if (
deltaY !== 0 &&
Math.abs(deltaY) <=
Math.abs(this._behavior._maxFallingSpeed * timeDelta)
) {
object.setY(object.getY() + deltaY);
this._floorLastY = floorY;
hasMoved = true;
}
return hasMoved;
}
checkTransitionBeforeX() {
@@ -1760,29 +1731,11 @@ namespace gdjs {
beforeMovingX() {
const behavior = this._behavior;
// Shift the object according to the floor movement.
const floorX = this._floorPlatform!.owner.getX();
const deltaX = floorX - this._floorLastX;
behavior._requestedDeltaX += deltaX;
this._floorLastX = floorX;
behavior._requestedDeltaX +=
this._floorPlatform!.owner.getX() - this._floorLastX;
// See `beforeUpdatingObstacles` for the logic for the Y axis.
}
followCurrentPlatformOnX(timeDelta: float): boolean {
let hasMoved = false;
const object = this._behavior.owner;
// Shift the object according to the floor movement.
const floorX = this._floorPlatform!.owner.getX();
const deltaX = floorX - this._floorLastX;
if (deltaX !== 0) {
console.log(deltaX);
object.setX(object.getX() + deltaX);
this._floorLastX = floorX;
hasMoved = true;
}
return hasMoved;
}
checkTransitionBeforeY(timeDelta: float) {
const behavior = this._behavior;
// Go on a ladder

View File

@@ -404,9 +404,7 @@ describe('gdjs.PlatformerObjectRuntimeBehavior', function () {
expect(object.getY()).to.be(-30); // -30 = -10 (platform y) + -20 (object height)
// Make the platform under the character feet smaller.
runtimeScene.renderAndStepWithEventsFunction(1000 / 60, () => {
object.setCustomWidthAndHeight(object.getWidth(), 9);
});
object.setCustomWidthAndHeight(object.getWidth(), 9);
runtimeScene.renderAndStep(1000 / 60);
expect(object.getBehavior('auto1').isFalling()).to.be(false);
expect(object.getBehavior('auto1').isFallingWithoutJumping()).to.be(
@@ -681,11 +679,18 @@ describe('gdjs.PlatformerObjectRuntimeBehavior', function () {
expect(object.getY()).to.be.within(140.6297999, 140.6298001);
// Move the platform by 6 pixels to the right.
for (let index = 0; index < 6; index++) {
runtimeScene.renderAndStepWithEventsFunction(1000 / 60, () => {
platform.setX(platform.getX() + 1);
});
}
platform.setX(platform.getX() + 1);
runtimeScene.renderAndStep(1000 / 60);
platform.setX(platform.getX() + 1);
runtimeScene.renderAndStep(1000 / 60);
platform.setX(platform.getX() + 1);
runtimeScene.renderAndStep(1000 / 60);
platform.setX(platform.getX() + 1);
runtimeScene.renderAndStep(1000 / 60);
platform.setX(platform.getX() + 1);
runtimeScene.renderAndStep(1000 / 60);
platform.setX(platform.getX() + 1);
runtimeScene.renderAndStep(1000 / 60);
// Ensure the object followed the platform on the X axis.
// If the floating point errors caused oscillations between two Y positions,
@@ -694,9 +699,6 @@ describe('gdjs.PlatformerObjectRuntimeBehavior', function () {
expect(object.getBehavior('auto1').isFalling()).to.be(false);
expect(object.getBehavior('auto1').isOnFloor()).to.be(true);
expect(object.getY()).to.be.within(140.6297999, 140.6298001);
// The floor following has a 1-frame delay.
expect(object.getX()).to.be(5);
runtimeScene.renderAndStep(1000 / 60);
expect(object.getX()).to.be(6);
});
});

View File

@@ -59,18 +59,13 @@ describe('gdjs.PlatformerObjectRuntimeBehavior', function () {
// Check that the object follow the platform, even if the
// movement is less than one pixel.
runtimeScene.renderAndStepWithEventsFunction(1000 / 60, () => {
platform.setX(platform.getX() + 0.12);
});
runtimeScene.renderAndStepWithEventsFunction(1000 / 60, () => {
platform.setX(platform.getX() + 0.12);
});
runtimeScene.renderAndStepWithEventsFunction(1000 / 60, () => {
platform.setX(platform.getX() + 0.12);
});
// The floor following has a 1-frame delay.
expect(object.getX()).to.be(0.24);
platform.setX(platform.getX() + 0.12);
runtimeScene.renderAndStep(1000 / 60);
platform.setX(platform.getX() + 0.12);
runtimeScene.renderAndStep(1000 / 60);
platform.setX(platform.getX() + 0.12);
runtimeScene.renderAndStep(1000 / 60);
expect(object.getX()).to.be(0.36);
});
@@ -245,28 +240,22 @@ describe('gdjs.PlatformerObjectRuntimeBehavior', function () {
// Check that the object follow the platform, even if the
// movement is less than one pixel.
for (let i = 0; i < 5; ++i) {
const previousPlatformY = platform.getY();
runtimeScene.renderAndStepWithEventsFunction(1000 / 60, () => {
platform.setPosition(
platform.getX() + deltaX,
platform.getY() + deltaY
);
});
platform.setPosition(
platform.getX() + deltaX,
platform.getY() + deltaY
);
runtimeScene.renderAndStep(1000 / 60);
expect(object.getBehavior('auto1').isOnFloor()).to.be(true);
expect(object.getBehavior('auto1').isFalling()).to.be(false);
expect(object.getBehavior('auto1').isMoving()).to.be(false);
// The object follow the platform
// The rounding error is probably due to a separate call.
// TODO Try to make it exact or find why
// The floor following has a 1-frame delay.
expect(object.getY()).to.be.within(
previousPlatformY - object.getHeight() - epsilon,
previousPlatformY - object.getHeight() + epsilon
platform.getY() - object.getHeight() - epsilon,
platform.getY() - object.getHeight() + epsilon
);
}
// The floor following has a 1-frame delay.
expect(object.getX()).to.be(0 + 4 * deltaX);
runtimeScene.renderAndStep(1000 / 60);
expect(object.getX()).to.be(0 + 5 * deltaX);
});
});
@@ -389,26 +378,20 @@ describe('gdjs.PlatformerObjectRuntimeBehavior', function () {
// Check that the object follow the platform, even if the
// movement is less than one pixel.
for (let i = 0; i < 5; ++i) {
const previousPlatformY = platform.getY();
runtimeScene.renderAndStepWithEventsFunction(1000 / 60, () => {
platform.setPosition(
platform.getX() + deltaX,
platform.getY() + deltaY
);
});
platform.setPosition(
platform.getX() + deltaX,
platform.getY() + deltaY
);
runtimeScene.renderAndStep(1000 / 60);
expect(object.getBehavior('auto1').isOnFloor()).to.be(true);
expect(object.getBehavior('auto1').isFalling()).to.be(false);
expect(object.getBehavior('auto1').isMoving()).to.be(false);
// The object must not be inside the platform or it gets stuck
// The floor following has a 1-frame delay.
expect(object.getY()).to.be.within(
previousPlatformY - object.getHeight() - epsilon,
previousPlatformY - object.getHeight() + epsilon
platform.getY() - object.getHeight() - epsilon,
platform.getY() - object.getHeight() + epsilon
);
}
// The floor following has a 1-frame delay.
expect(object.getX()).to.be(0 + 4 * deltaX);
runtimeScene.renderAndStep(1000 / 60);
expect(object.getX()).to.be(0 + 5 * deltaX);
});
});

View File

@@ -7,7 +7,7 @@
},
properties: { windowWidth: 800, windowHeight: 600 },
});
const runtimeScene = new gdjs.TestRuntimeScene(runtimeGame);
const runtimeScene = new gdjs.RuntimeScene(runtimeGame);
runtimeScene.loadFromScene({
layers: [{ name: '', visibility: true, effects: [] }],
variables: [],

View File

@@ -111,6 +111,7 @@ describe('gdjs.TextInputRuntimeObject (using a PixiJS RuntimeGame with DOM eleme
const {
runtimeScene,
gameDomElementContainer,
object,
} = await setupObjectAndGetDomElementContainer();
expect(gameDomElementContainer.querySelector('input')).not.to.be(null);

View File

@@ -170,21 +170,19 @@ namespace gdjs {
// Position the input on the container on top of the canvas.
workingPoint[0] = canvasLeft;
workingPoint[1] = canvasTop;
runtimeGameRenderer.convertCanvasToDomElementContainerCoords(
workingPoint,
const topLeftPageCoordinates = runtimeGameRenderer.convertCanvasToDomElementContainerCoords(
workingPoint
);
const pageLeft = workingPoint[0];
const pageTop = workingPoint[1];
const pageLeft = topLeftPageCoordinates[0];
const pageTop = topLeftPageCoordinates[1];
workingPoint[0] = canvasRight;
workingPoint[1] = canvasBottom;
runtimeGameRenderer.convertCanvasToDomElementContainerCoords(
workingPoint,
const bottomRightPageCoordinates = runtimeGameRenderer.convertCanvasToDomElementContainerCoords(
workingPoint
);
const pageRight = workingPoint[0];
const pageBottom = workingPoint[1];
const pageRight = bottomRightPageCoordinates[0];
const pageBottom = bottomRightPageCoordinates[1];
const widthInContainer = pageRight - pageLeft;
const heightInContainer = pageBottom - pageTop;

View File

@@ -1,4 +1,6 @@
namespace gdjs {
const logger = new gdjs.Logger('Text input object');
const supportedInputTypes = [
'text',
'email',

View File

@@ -124,7 +124,7 @@ describe('gdjs.TileMapCollisionMaskRuntimeObject', function () {
index < 200 && tileMap._collisionTileMap.getDimensionX() === 0;
index++
) {
await delay(100);
await delay(25);
}
if (tileMap._collisionTileMap.getDimensionX() === 0) {
throw new Error('Timeout reading the tile map JSON file.');

View File

@@ -1,5 +1,7 @@
/// <reference path="helper/TileMapHelper.d.ts" />
namespace gdjs {
const logger = new gdjs.Logger('Tilemap object');
/**
* An object that handle hitboxes for a tile map.
* @extends gdjs.RuntimeObject
@@ -180,7 +182,7 @@ namespace gdjs {
updateHitBoxes(): void {
this.updateTransformation();
// Update the RuntimeObject hitboxes attribute.
for (const _ of this._collisionTileMap.getAllHitboxes(
for (const hitboxes of this._collisionTileMap.getAllHitboxes(
this._collisionMaskTag
)) {
// RuntimeObject.hitBoxes contains the same polygons instances as the

View File

@@ -1,6 +1,8 @@
/// <reference path="helper/TileMapHelper.d.ts" />
/// <reference path="pixi-tilemap/dist/pixi-tilemap.d.ts" />
namespace gdjs {
const logger = new gdjs.Logger('Tilemap object');
/**
* The PIXI.js renderer for the Tile map runtime object.
*

View File

@@ -2,6 +2,7 @@
namespace gdjs {
import PIXI = GlobalPIXIModule.PIXI;
const logger = new gdjs.Logger('Tilemap object');
/**
* Displays a Tilemap object (mapeditor.org supported).
*/

View File

@@ -24,6 +24,8 @@ namespace gdjs {
private _angle: float = 0;
//Attributes used when moving
private _x: float = 0;
private _y: float = 0;
private _xVelocity: float = 0;
private _yVelocity: float = 0;
private _angularSpeed: float = 0;

View File

@@ -121,62 +121,6 @@ AdvancedExtension::AdvancedExtension() {
"eventsFunctionContext.getArgument(" +
parameterNameCode + ") : \"\")";
});
GetAllConditions()["CompareArgumentAsNumber"]
.GetCodeExtraInformation()
.SetCustomCodeGenerator([](gd::Instruction &instruction,
gd::EventsCodeGenerator &codeGenerator,
gd::EventsCodeGenerationContext &context) {
gd::String parameterNameCode =
gd::ExpressionCodeGenerator::GenerateExpressionCode(
codeGenerator, context, "string",
instruction.GetParameter(0).GetPlainString());
gd::String operatorCode = codeGenerator.GenerateRelationalOperatorCodes(
instruction.GetParameter(1).GetPlainString());
gd::String operandCode =
gd::ExpressionCodeGenerator::GenerateExpressionCode(
codeGenerator, context, "number",
instruction.GetParameter(2).GetPlainString());
gd::String resultingBoolean =
codeGenerator.GenerateBooleanFullName("conditionTrue", context) +
".val";
return resultingBoolean + " = ((typeof eventsFunctionContext !== 'undefined' ? "
"Number(eventsFunctionContext.getArgument(" +
parameterNameCode + ")) || 0 : 0) " + operatorCode + " " +
operandCode + ");\n";
});
GetAllConditions()["CompareArgumentAsString"]
.GetCodeExtraInformation()
.SetCustomCodeGenerator([](gd::Instruction &instruction,
gd::EventsCodeGenerator &codeGenerator,
gd::EventsCodeGenerationContext &context) {
gd::String parameterNameCode =
gd::ExpressionCodeGenerator::GenerateExpressionCode(
codeGenerator, context, "string",
instruction.GetParameter(0).GetPlainString());
gd::String operatorCode = codeGenerator.GenerateRelationalOperatorCodes(
instruction.GetParameter(1).GetPlainString());
gd::String operandCode =
gd::ExpressionCodeGenerator::GenerateExpressionCode(
codeGenerator, context, "string",
instruction.GetParameter(2).GetPlainString());
gd::String resultingBoolean =
codeGenerator.GenerateBooleanFullName("conditionTrue", context) +
".val";
return resultingBoolean + " = ((typeof eventsFunctionContext !== 'undefined' ? "
"\"\" + eventsFunctionContext.getArgument(" +
parameterNameCode + ") : \"\") " + operatorCode + " " +
operandCode + ");\n";
});
}
} // namespace gdjs
} // namespace gdjs

View File

@@ -55,24 +55,40 @@ CommonInstructionsExtension::CommonInstructionsExtension() {
codeGenerator,
context,
"number",
instruction.GetParameter(0).GetPlainString());
gd::String operatorCode = codeGenerator.GenerateRelationalOperatorCodes(
instruction.GetParameter(1).GetPlainString());
instruction.GetParameters()[0].GetPlainString());
gd::String value2Code =
gd::ExpressionCodeGenerator::GenerateExpressionCode(
codeGenerator,
context,
"number",
instruction.GetParameter(2).GetPlainString());
instruction.GetParameters()[2].GetPlainString());
gd::String resultingBoolean =
codeGenerator.GenerateBooleanFullName("conditionTrue", context) +
".val";
return resultingBoolean + " = (" + value1Code + " " + operatorCode +
" " + value2Code + ");\n";
if (instruction.GetParameters()[1].GetPlainString() == "=" ||
instruction.GetParameters()[1].GetPlainString().empty())
return resultingBoolean + " = (" + value1Code + " == " + value2Code +
");\n";
else if (instruction.GetParameters()[1].GetPlainString() == ">")
return resultingBoolean + " = (" + value1Code + " > " + value2Code +
");\n";
else if (instruction.GetParameters()[1].GetPlainString() == "<")
return resultingBoolean + " = (" + value1Code + " < " + value2Code +
");\n";
else if (instruction.GetParameters()[1].GetPlainString() == "<=")
return resultingBoolean + " = (" + value1Code + " <= " + value2Code +
");\n";
else if (instruction.GetParameters()[1].GetPlainString() == ">=")
return resultingBoolean + " = (" + value1Code + " >= " + value2Code +
");\n";
else if (instruction.GetParameters()[1].GetPlainString() == "!=")
return resultingBoolean + " = (" + value1Code + " != " + value2Code +
");\n";
return gd::String("");
});
GetAllConditions()["BuiltinCommonInstructions::CompareNumbers"]
.codeExtraInformation = GetAllConditions()["Egal"].codeExtraInformation;
@@ -86,24 +102,27 @@ CommonInstructionsExtension::CommonInstructionsExtension() {
codeGenerator,
context,
"string",
instruction.GetParameter(0).GetPlainString());
gd::String operatorCode = codeGenerator.GenerateRelationalOperatorCodes(
instruction.GetParameter(1).GetPlainString());
instruction.GetParameters()[0].GetPlainString());
gd::String value2Code =
gd::ExpressionCodeGenerator::GenerateExpressionCode(
codeGenerator,
context,
"string",
instruction.GetParameter(2).GetPlainString());
instruction.GetParameters()[2].GetPlainString());
gd::String resultingBoolean =
codeGenerator.GenerateBooleanFullName("conditionTrue", context) +
".val";
return resultingBoolean + " = (" + value1Code + " " + operatorCode +
" " + value2Code + ");\n";
if (instruction.GetParameters()[1].GetPlainString() == "=")
return resultingBoolean + " = (" + value1Code + " == " + value2Code +
");\n";
else if (instruction.GetParameters()[1].GetPlainString() == "!=")
return resultingBoolean + " = (" + value1Code + " != " + value2Code +
");\n";
return gd::String("");
});
GetAllConditions()["BuiltinCommonInstructions::CompareStrings"]
.codeExtraInformation =

View File

@@ -46,7 +46,6 @@ SpriteExtension::SpriteExtension() {
spriteConditions["Direction"].SetFunctionName("getDirectionOrAngle");
spriteConditions["Sprite"].SetFunctionName("getAnimationFrame");
spriteConditions["AnimationEnded"].SetFunctionName("hasAnimationEnded");
spriteConditions["AnimationEnded2"].SetFunctionName("hasAnimationEnded2");
spriteActions["PauseAnimation"].SetFunctionName("pauseAnimation");
spriteActions["PlayAnimation"].SetFunctionName("playAnimation");
spriteConditions["AnimStopped"].SetFunctionName("animationPaused");

View File

@@ -265,6 +265,7 @@ namespace gdjs {
cy *= absScaleY;
// Rotation
const oldX = x;
const angleInRadians = (this.angle / 180) * Math.PI;
const cosValue = Math.cos(angleInRadians);
const sinValue = Math.sin(angleInRadians);

View File

@@ -5,6 +5,9 @@
*/
namespace gdjs {
const logger = new gdjs.Logger('CustomRuntimeObject');
const setupWarningLogger = new gdjs.Logger(
'CustomRuntimeObject (setup warnings)'
);
/**
* The instance container of a custom object, containing instances of objects rendered on screen.
@@ -197,8 +200,12 @@ namespace gdjs {
*/
_updateObjectsPreRender() {
const allInstancesList = this.getAdhocListOfAllInstances();
for (let i = 0, len = allInstancesList.length; i < len; ++i) {
const object = allInstancesList[i];
for (
let i = 0, len = this.getAdhocListOfAllInstances().length;
i < len;
++i
) {
const object = this.getAdhocListOfAllInstances()[i];
const rendererObject = object.getRendererObject();
if (rendererObject) {
rendererObject.visible = !object.isHidden();
@@ -215,7 +222,7 @@ namespace gdjs {
// to see what is rendered).
if (this._debugDrawEnabled) {
this._debuggerRenderer.renderDebugDraw(
allInstancesList,
this.getAdhocListOfAllInstances(),
this._debugDrawShowHiddenInstances,
this._debugDrawShowPointsNames,
this._debugDrawShowCustomPoints

View File

@@ -5,6 +5,9 @@
*/
namespace gdjs {
const logger = new gdjs.Logger('RuntimeInstanceContainer');
const setupWarningLogger = new gdjs.Logger(
'RuntimeInstanceContainer (setup warnings)'
);
/**
* A container of object instances rendered on screen.

View File

@@ -6,6 +6,8 @@
namespace gdjs {
export namespace evtTools {
export namespace network {
const logger = new gdjs.Logger('Network requests');
/**
* Send an asynchronous request to the specified URL, with the specified (text)
* body, method and contentType (defaults to `application/x-www-form-urlencoded`).

View File

@@ -18,7 +18,6 @@ namespace gdjs {
* @memberOf gdjs
*/
export namespace evtTools {
// @ts-ignore - This variable is unused on purpose.
const thisIsUnusedButEnsureTheNamespaceIsDeclared = true;
}

View File

@@ -409,22 +409,20 @@ namespace gdjs {
* Convert a point from the canvas coordinates to the dom element container coordinates.
*
* @param canvasCoords The point in the canvas coordinates.
* @param result The point to return.
* @returns The point in the dom element container coordinates.
*/
convertCanvasToDomElementContainerCoords(
canvasCoords: FloatPoint,
result: FloatPoint
canvasCoords: FloatPoint
): FloatPoint {
const pageCoords = result || [0, 0];
const pageCoords: FloatPoint = [0, 0];
const gameResolutionWidth = this._game.getGameResolutionWidth();
const canvasWidth = this._canvasWidth || 1;
const gameResolutionHeight = this._game.getGameResolutionHeight();
const canvasHeight = this._canvasHeight || 1;
// Handle the fact that the game is stretched to fill the canvas.
pageCoords[0] =
(canvasCoords[0] * this._canvasWidth) /
this._game.getGameResolutionWidth();
pageCoords[1] =
(canvasCoords[1] * this._canvasHeight) /
this._game.getGameResolutionHeight();
pageCoords[0] = (canvasCoords[0] * canvasWidth) / gameResolutionWidth;
pageCoords[1] = (canvasCoords[1] * canvasHeight) / gameResolutionHeight;
return pageCoords;
}
@@ -713,6 +711,7 @@ namespace gdjs {
e.preventDefault();
if (e.changedTouches) {
for (let i = 0; i < e.changedTouches.length; ++i) {
const pos = getEventPosition(e.changedTouches[i]);
manager.onTouchEnd(e.changedTouches[i].identifier);
}
}

View File

@@ -393,6 +393,8 @@ namespace gdjs {
const windowInnerHeight = gdjs.RuntimeGameRenderer.getWindowInnerHeight();
// Enlarge either the width or the eight to fill the inner window space.
let width = this._gameResolutionWidth;
let height = this._gameResolutionHeight;
if (this._resizeMode === 'adaptWidth') {
this._gameResolutionWidth =
(this._gameResolutionHeight * windowInnerWidth) /

View File

@@ -471,19 +471,14 @@ namespace gdjs {
];
const oldFrame = this._currentFrame;
const elapsedTime = this.getElapsedTime() / 1000;
this._frameElapsedTime += this._animationPaused
? 0
: elapsedTime * this._animationSpeedScale;
if (
!direction.loop &&
this._currentFrame >= direction.frames.length - 1 &&
this._frameElapsedTime > direction.timeBetweenFrames
) {
// *Optimization*: Animation is finished, don't change the current frame
// and compute nothing more.
//*Optimization*: Animation is finished, don't change the current frame
//and compute nothing more.
if (!direction.loop && this._currentFrame >= direction.frames.length) {
} else {
const elapsedTime = this.getElapsedTime() / 1000;
this._frameElapsedTime += this._animationPaused
? 0
: elapsedTime * this._animationSpeedScale;
if (this._frameElapsedTime > direction.timeBetweenFrames) {
const count = Math.floor(
this._frameElapsedTime / direction.timeBetweenFrames
@@ -755,10 +750,7 @@ namespace gdjs {
}
/**
* @deprecated
* Return true if animation has ended.
* Prefer using hasAnimationEnded2. This method returns true as soon as
* the animation enters the last frame, not at the end of the last frame.
*/
hasAnimationEnded(): boolean {
if (
@@ -777,33 +769,6 @@ namespace gdjs {
return this._currentFrame === direction.frames.length - 1;
}
/**
* Return true if animation has ended.
* The animation had ended if:
* - it's not configured as a loop;
* - the current frame is the last frame;
* - the last frame has been displayed long enough.
*/
hasAnimationEnded2(): boolean {
if (
this._currentAnimation >= this._animations.length ||
this._currentDirection >=
this._animations[this._currentAnimation].directions.length
) {
return true;
}
const direction = this._animations[this._currentAnimation].directions[
this._currentDirection
];
if (direction.loop) {
return false;
}
return (
this._currentFrame === direction.frames.length - 1 &&
this._frameElapsedTime > direction.timeBetweenFrames
);
}
animationPaused() {
return this._animationPaused;
}

View File

@@ -567,14 +567,12 @@ describe('gdjs.CustomRuntimeObject', function () {
/** @type {gdjs.CustomRuntimeObject} */
let topCustomObject;
/** @type {gdjs.RuntimeObject} */
// @ts-ignore - we do not use this variable
let topLeftSprite;
/** @type {gdjs.RuntimeObject} */
let topRightSprite;
/** @type {gdjs.CustomRuntimeObject} */
let bottomCustomObject;
/** @type {gdjs.RuntimeObject} */
// @ts-ignore - we do not use this variable
let bottomLeftSprite;
/** @type {gdjs.RuntimeObject} */
let bottomRightSprite;

View File

@@ -1,23 +0,0 @@
/**
* A RuntimeScene that allows to test events side effects.
*/
gdjs.TestRuntimeScene = class TestRuntimeScene extends gdjs.RuntimeScene {
/**
* @param {gdjs.RuntimeGame} runtimeGame
*/
constructor(runtimeGame) {
super(runtimeGame);
}
/**
* @param {float} elapsedTime
* @param {() => void} eventsFunction
*/
renderAndStepWithEventsFunction(elapsedTime, eventsFunction) {
const runtimeScene = this;
this._eventsFunction = (runtimeScene) => eventsFunction();
this.renderAndStep(elapsedTime);
this._eventsFunction = null;
}
};

View File

@@ -1579,9 +1579,6 @@ interface BehaviorMetadata {
[Ref] BehaviorMetadata SetObjectType([Const] DOMString objectType);
[Const, Ref] DOMString GetObjectType();
boolean IsPrivate();
[Ref] BehaviorMetadata SetPrivate();
[Ref] Behavior Get();
BehaviorsSharedData GetSharedDataInstance();
@@ -2355,8 +2352,6 @@ interface EventsBasedBehavior {
[Const, Ref] DOMString GetFullName();
[Ref] EventsBasedBehavior SetObjectType([Const] DOMString fullName);
[Const, Ref] DOMString GetObjectType();
[Ref] EventsBasedBehavior SetPrivate(boolean isPrivate);
boolean IsPrivate();
[Ref] EventsFunctionsContainer GetEventsFunctions();
[Ref] NamedPropertyDescriptorsList GetPropertyDescriptors();

View File

@@ -28,8 +28,6 @@ declare class gdBehaviorMetadata {
addRequiredFile(resourceFile: string): gdBehaviorMetadata;
setObjectType(objectType: string): gdBehaviorMetadata;
getObjectType(): string;
isPrivate(): boolean;
setPrivate(): gdBehaviorMetadata;
get(): gdBehavior;
getSharedDataInstance(): gdBehaviorsSharedData;
delete(): void;

View File

@@ -9,8 +9,6 @@ declare class gdEventsBasedBehavior {
getFullName(): string;
setObjectType(fullName: string): gdEventsBasedBehavior;
getObjectType(): string;
setPrivate(isPrivate: boolean): gdEventsBasedBehavior;
isPrivate(): boolean;
getEventsFunctions(): gdEventsFunctionsContainer;
getPropertyDescriptors(): gdNamedPropertyDescriptorsList;
serializeTo(element: gdSerializerElement): void;

File diff suppressed because it is too large Load Diff

View File

@@ -57,7 +57,7 @@
"node-require-function": "^1.2.0",
"pixi-simple-gesture": "github:4ian/pixi-simple-gesture#v0.3.3",
"pixi.js-legacy": "^6.1.2",
"posthog-js": "^1.34.0",
"posthog-js": "^1.20.4",
"prop-types": "^15.5.10",
"randomcolor": "^0.5.3",
"raven-js": "^3.19.1",
@@ -70,7 +70,7 @@
"react-dom": "16.14.0",
"react-error-boundary": "^1.2.0",
"react-json-view": "^1.16.1",
"react-markdown": "^6.0.3",
"react-markdown": "^4.0.6",
"react-measure": "2.3.0",
"react-monaco-editor": "^0.18.0",
"react-mosaic-component": "github:4ian/react-mosaic#v3.1.0",

View File

@@ -35,7 +35,6 @@ import { BoxSearchResults } from '../UI/Search/BoxSearchResults';
import Link from '../UI/Link';
import PrivateAssetsAuthorizationContext from './PrivateAssets/PrivateAssetsAuthorizationContext';
import AuthorizedAssetImage from './PrivateAssets/AuthorizedAssetImage';
import { MarkdownText } from '../UI/MarkdownText';
const FIXED_HEIGHT = 250;
const FIXED_WIDTH = 300;
@@ -411,9 +410,7 @@ export const AssetDetails = ({
</Trans>
)}
</Text>
<Text size="body" displayInlineAsSpan>
<MarkdownText source={asset.description} allowParagraphs />
</Text>
<Text size="body">{asset.description}</Text>
</React.Fragment>
) : error ? (
<PlaceholderError onRetry={loadAsset}>

View File

@@ -5,8 +5,6 @@ import {
type Environment,
getPublicAsset,
isPixelArt,
isPublicAssetResourceUrl,
extractFilenameWithExtensionFromPublicAssetResourceUrl,
} from '../Utils/GDevelopServices/Asset';
import newNameGenerator from '../Utils/NewNameGenerator';
import { unserializeFromJSObject } from '../Utils/Serializer';
@@ -74,23 +72,16 @@ export const installResource = (
// Check if the resource that must be installed is already present. Use the "origin"
// of the resource (if present), otherwise for compatibility we use the URL.
const resourceFileUrl: string = serializedResource.file;
const resourceOriginRawName: string = serializedResource.origin
const resourceOriginName: string = serializedResource.origin
? serializedResource.origin.name
: '';
// We clean up the name of the resource, to avoid having a resource with a name
// too long (for instance, a resource with a SHA for public assets).
const resourceOriginCleanedName: string = isPublicAssetResourceUrl(
resourceFileUrl
)
? extractFilenameWithExtensionFromPublicAssetResourceUrl(resourceFileUrl)
: resourceOriginRawName;
const resourceOriginIdentifier: string = serializedResource.origin
? serializedResource.origin.identifier
: '';
const existingResourceNameFromSameOrigin =
resourceOriginCleanedName && resourceOriginIdentifier
resourceOriginName && resourceOriginIdentifier
? resourcesManager.getResourceNameWithOrigin(
resourceOriginCleanedName,
resourceOriginName,
resourceOriginIdentifier
)
: '';
@@ -127,7 +118,7 @@ export const installResource = (
newResource.setSmooth(
project.getScaleMode() !== 'nearest' && !isPixelArt(asset)
);
newResource.setOrigin(resourceOriginCleanedName, resourceOriginIdentifier);
newResource.setOrigin(resourceOriginName, resourceOriginIdentifier);
resourcesManager.addResource(newResource);
newResource.delete();

View File

@@ -1,6 +1,5 @@
// @flow
import { t, Trans } from '@lingui/macro';
import { type I18n as I18nType } from '@lingui/core';
import * as React from 'react';
import Dialog from '../UI/Dialog';
import FlatButton from '../UI/FlatButton';
@@ -39,7 +38,6 @@ import Window from '../Utils/Window';
import PrivateAssetsAuthorizationContext from './PrivateAssets/PrivateAssetsAuthorizationContext';
import { isPrivateAsset } from '../Utils/GDevelopServices/Asset';
import useAlertDialog from '../UI/Alert/useAlertDialog';
import { translateExtensionCategory } from '../Utils/Extension/ExtensionCategories';
const isDev = Window.isDev();
const ObjectListItem = ({
@@ -84,7 +82,6 @@ type Props = {|
onCreateNewObject: (type: string) => void,
onObjectAddedFromAsset: gdObject => void,
canInstallPrivateAsset: () => boolean,
i18n: I18nType,
|};
export default function NewObjectDialog({
@@ -99,7 +96,6 @@ export default function NewObjectDialog({
onCreateNewObject,
onObjectAddedFromAsset,
canInstallPrivateAsset,
i18n,
}: Props) {
const {
setNewObjectDialogDefaultTab,
@@ -117,10 +113,7 @@ export default function NewObjectDialog({
() => {
const objectsByCategory = {};
allObjectMetadata.forEach(objectMetadata => {
const category = translateExtensionCategory(
objectMetadata.categoryFullName,
i18n
);
const category = objectMetadata.categoryFullName;
objectsByCategory[category] = [
...(objectsByCategory[category] || []),
objectMetadata,
@@ -128,7 +121,7 @@ export default function NewObjectDialog({
});
return objectsByCategory;
},
[allObjectMetadata, i18n]
[allObjectMetadata]
);
React.useEffect(() => setNewObjectDialogDefaultTab(currentTab), [

View File

@@ -13,7 +13,11 @@ import PriceTag, { formatPrice } from '../../UI/PriceTag';
import FlatButton from '../../UI/FlatButton';
import AlertMessage from '../../UI/AlertMessage';
import PlaceholderLoader from '../../UI/PlaceholderLoader';
import { ResponsiveLineStackLayout, LineStackLayout } from '../../UI/Layout';
import {
ColumnStackLayout,
ResponsiveLineStackLayout,
LineStackLayout,
} from '../../UI/Layout';
import { Column, Line } from '../../UI/Grid';
import {
getUserPublicProfile,
@@ -28,7 +32,6 @@ import ResponsiveImagesGallery from '../../UI/ResponsiveImagesGallery';
import { useResponsiveWindowWidth } from '../../UI/Reponsive/ResponsiveWindowMeasurer';
import RaisedButton from '../../UI/RaisedButton';
import { sendAssetPackBuyClicked } from '../../Utils/Analytics/EventSender';
import { MarkdownText } from '../../UI/MarkdownText';
const sortedContentType = [
'sprite',
@@ -205,7 +208,7 @@ const PrivateAssetPackDialog = ({
variant="outlined"
style={{ padding: windowWidth === 'small' ? 20 : 30 }}
>
<Column noMargin>
<ColumnStackLayout noMargin useLargeSpacer>
<Line
noMargin
expand
@@ -215,12 +218,7 @@ const PrivateAssetPackDialog = ({
<PriceTag value={prices[0].value} />
{getBuyButton(i18n)}
</Line>
<Text size="body2" displayInlineAsSpan>
<MarkdownText
source={assetPack.longDescription}
allowParagraphs
/>
</Text>
<Text noMargin>{assetPack.longDescription}</Text>
<ResponsiveLineStackLayout noMargin noColumnMargin>
<Column noMargin expand>
<Text size="sub-title">
@@ -279,7 +277,7 @@ const PrivateAssetPackDialog = ({
</LineStackLayout>
</Column>
</ResponsiveLineStackLayout>
</Column>
</ColumnStackLayout>
</Paper>
</Column>
</ResponsiveLineStackLayout>

View File

@@ -14,7 +14,6 @@ type Props = {|
value: string,
onChange: string => void,
disabled?: boolean,
eventsFunctionsExtension?: gdEventsFunctionsExtension,
|};
type State = {|
behaviorMetadata: Array<EnumeratedBehaviorMetadata>,
@@ -27,8 +26,7 @@ export default class BehaviorTypeSelector extends React.Component<
state = {
behaviorMetadata: enumerateBehaviorsMetadata(
this.props.project.getCurrentPlatform(),
this.props.project,
this.props.eventsFunctionsExtension
this.props.project
),
};

View File

@@ -15,8 +15,7 @@ export type EnumeratedBehaviorMetadata = {|
export const enumerateBehaviorsMetadata = (
platform: gdPlatform,
project: gdProject,
eventsFunctionsExtension?: gdEventsFunctionsExtension
project: gdProject
): Array<EnumeratedBehaviorMetadata> => {
const extensionsList = platform.getAllPlatformExtensions();
@@ -31,12 +30,6 @@ export const enumerateBehaviorsMetadata = (
behaviorType,
behaviorMetadata: extension.getBehaviorMetadata(behaviorType),
}))
.filter(
({ behaviorMetadata }) =>
!behaviorMetadata.isPrivate() ||
(eventsFunctionsExtension &&
extension.getName() === eventsFunctionsExtension.getName())
)
.map(({ behaviorType, behaviorMetadata }) => ({
extension,
behaviorMetadata,

View File

@@ -82,7 +82,6 @@ const BehaviorListItem = ({
type Props = {|
project: gdProject,
eventsFunctionsExtension?: gdEventsFunctionsExtension,
objectType: string,
objectBehaviorsTypes: Array<string>,
open: boolean,
@@ -92,7 +91,6 @@ type Props = {|
export default function NewBehaviorDialog({
project,
eventsFunctionsExtension,
open,
onClose,
onChoose,
@@ -121,17 +119,13 @@ export default function NewBehaviorDialog({
);
const platform = project.getCurrentPlatform();
const behaviorsMetadata: Array<EnumeratedBehaviorMetadata> = React.useMemo(
const behaviorMetadata: Array<EnumeratedBehaviorMetadata> = React.useMemo(
() => {
return project && platform
? enumerateBehaviorsMetadata(
platform,
project,
eventsFunctionsExtension
)
? enumerateBehaviorsMetadata(platform, project)
: [];
},
[project, platform, eventsFunctionsExtension, extensionInstallTime] // eslint-disable-line react-hooks/exhaustive-deps
[project, platform, extensionInstallTime] // eslint-disable-line react-hooks/exhaustive-deps
);
const shouldAutofocusSearchbar = useShouldAutofocusSearchbar();
@@ -150,7 +144,7 @@ export default function NewBehaviorDialog({
const deprecatedBehaviorsInformation = getDeprecatedBehaviorsInformation();
const filteredBehaviorMetadata = filterEnumeratedBehaviorMetadata(
behaviorsMetadata,
behaviorMetadata,
searchText
);
const behaviors = filteredBehaviorMetadata.filter(

View File

@@ -38,14 +38,12 @@ const gd: libGDevelop = global.gd;
type Props = {|
project: gdProject,
eventsFunctionsExtension?: gdEventsFunctionsExtension,
object: gdObject,
onUpdateBehaviorsSharedData: () => void,
onSizeUpdated?: ?() => void,
resourceSources: Array<ResourceSource>,
onChooseResource: ChooseResourceFunction,
resourceExternalEditors: Array<ResourceExternalEditor>,
onBehaviorsUpdated?: () => void,
|};
const BehaviorsEditor = (props: Props) => {
@@ -53,7 +51,7 @@ const BehaviorsEditor = (props: Props) => {
false
);
const { object, project, eventsFunctionsExtension } = props;
const { object, project } = props;
const allBehaviorNames = object.getAllBehaviorNames().toJSArray();
const forceUpdate = useForceUpdate();
@@ -78,7 +76,6 @@ const BehaviorsEditor = (props: Props) => {
forceUpdate();
if (props.onSizeUpdated) props.onSizeUpdated();
props.onUpdateBehaviorsSharedData();
if (props.onBehaviorsUpdated) props.onBehaviorsUpdated();
};
const onChangeBehaviorName = (behavior: gdBehavior, newName: string) => {
@@ -91,7 +88,6 @@ const BehaviorsEditor = (props: Props) => {
if (object.hasBehaviorNamed(newName)) return;
object.renameBehavior(behavior.getName(), newName);
forceUpdate();
if (props.onBehaviorsUpdated) props.onBehaviorsUpdated();
};
const onRemoveBehavior = (behaviorName: string) => {
@@ -114,7 +110,6 @@ const BehaviorsEditor = (props: Props) => {
dependentBehaviors.forEach(name => object.removeBehavior(name));
if (props.onSizeUpdated) props.onSizeUpdated();
}
if (props.onBehaviorsUpdated) props.onBehaviorsUpdated();
};
return (
@@ -305,7 +300,6 @@ const BehaviorsEditor = (props: Props) => {
onClose={() => setNewBehaviorDialogOpen(false)}
onChoose={addBehavior}
project={project}
eventsFunctionsExtension={eventsFunctionsExtension}
/>
)}
</Column>

View File

@@ -163,7 +163,8 @@ export class CodeEditor extends React.Component<Props, State> {
// (so no need to scroll horizontally
// on small code editors) or at 80 columns max
// (as a good practice).
wordWrap: 'on',
wordWrap: 'bounded',
wordWrapColumn: 80,
}}
/>
)}

View File

@@ -346,19 +346,6 @@ export default class EventsBasedBehaviorPropertiesEditor extends React.Component
disabled={false}
/>
)}
{property.getType() === 'Color' && (
<ColorField
floatingLabelText={<Trans>Default value</Trans>}
disableAlpha
fullWidth
color={property.getValue()}
onChange={color => {
property.setValue(color);
this.forceUpdate();
this.props.onPropertiesUpdated();
}}
/>
)}
{property.getType() === 'Choice' && (
<SelectField
floatingLabelText={<Trans>Default value</Trans>}
@@ -388,6 +375,19 @@ export default class EventsBasedBehaviorPropertiesEditor extends React.Component
setExtraInfo={this._setChoiceExtraInfo(property)}
/>
)}
{property.getType() === 'Color' && (
<ColorField
floatingLabelText={<Trans>Color</Trans>}
disableAlpha
fullWidth
color={property.getValue()}
onChange={color => {
property.setValue(color);
this.forceUpdate();
this.props.onPropertiesUpdated();
}}
/>
)}
<ResponsiveLineStackLayout noMargin>
<SemiControlledTextField
commitOnBlur

View File

@@ -22,8 +22,6 @@ import {
unserializeFromJSObject,
} from '../Utils/Serializer';
import { type UnsavedChanges } from '../MainFrame/UnsavedChangesContext';
import Tooltip from '@material-ui/core/Tooltip';
import VisibilityOffIcon from '@material-ui/icons/VisibilityOff';
const EVENTS_BASED_BEHAVIOR_CLIPBOARD_KIND = 'Events Based Behavior';
@@ -31,29 +29,8 @@ const styles = {
listContainer: {
flex: 1,
},
tooltip: { marginRight: 5, verticalAlign: 'bottom' },
};
const renderEventsBehaviorLabel = (
eventsBasedBehavior: gdEventsBasedBehavior
) =>
eventsBasedBehavior.isPrivate() ? (
<>
<Tooltip
title={
<Trans>This behavior won't be visible in the events editor.</Trans>
}
>
<VisibilityOffIcon fontSize="small" style={styles.tooltip} />
</Tooltip>
<span title={eventsBasedBehavior.getName()}>
{eventsBasedBehavior.getName()}
</span>
</>
) : (
eventsBasedBehavior.getName()
);
type State = {|
renamedEventsBasedBehavior: ?gdEventsBasedBehavior,
searchText: string,
@@ -184,11 +161,6 @@ export default class EventsBasedBehaviorsList extends React.Component<
this.forceUpdateList();
};
_togglePrivate = (eventsBasedBehavior: gdEventsBasedBehavior) => {
eventsBasedBehavior.setPrivate(!eventsBasedBehavior.isPrivate());
this.forceUpdate();
};
forceUpdateList = () => {
this._onEventsBasedBehaviorModified();
if (this.sortableList) this.sortableList.forceUpdateGrid();
@@ -268,12 +240,6 @@ export default class EventsBasedBehaviorsList extends React.Component<
askForConfirmation: true,
}),
},
{
label: eventsBasedBehavior.isPrivate()
? i18n._(t`Make public`)
: i18n._(t`Make private`),
click: () => this._togglePrivate(eventsBasedBehavior),
},
{
type: 'separator',
},
@@ -349,7 +315,6 @@ export default class EventsBasedBehaviorsList extends React.Component<
height={height}
onAddNewItem={this._addNewEventsBasedBehavior}
addNewItemLabel={<Trans>Add a new behavior</Trans>}
renderItemLabel={renderEventsBehaviorLabel}
getItemName={getEventsBasedBehaviorName}
selectedItems={
selectedEventsBasedBehavior

View File

@@ -192,7 +192,7 @@ export default class EventBasedObjectChildrenEditor extends React.Component<
};
render() {
const { eventsBasedObject, project, eventsFunctionsExtension } = this.props;
const { eventsBasedObject, project } = this.props;
// TODO EBO When adding an object, filter the object types to excludes
// object that depend (transitively) on this object to avoid cycles.
@@ -260,7 +260,6 @@ export default class EventBasedObjectChildrenEditor extends React.Component<
object={this.state.editedObjectWithContext.object}
initialTab={this.state.editedObjectInitialTab}
project={project}
eventsFunctionsExtension={eventsFunctionsExtension}
resourceSources={[]}
resourceExternalEditors={[]}
onChooseResource={() => Promise.resolve([])}

View File

@@ -8,7 +8,6 @@ import TextField from '../UI/TextField';
import SemiControlledTextField from '../UI/SemiControlledTextField';
import { Tabs, Tab } from '../UI/Tabs';
import DismissableAlertMessage from '../UI/DismissableAlertMessage';
import AlertMessage from '../UI/AlertMessage';
import EventsBasedObjectPropertiesEditor from './EventsBasedObjectPropertiesEditor';
import EventBasedObjectChildrenEditor from './EventBasedObjectChildrenEditor';
import { ColumnStackLayout } from '../UI/Layout';
@@ -70,13 +69,6 @@ export default class EventsBasedObjectEditor extends React.Component<
<Line expand useFullHeight>
{currentTab === 'configuration' && (
<ColumnStackLayout expand>
<AlertMessage kind="warning">
<Trans>
The custom object editor is at a very early stage. A lot of
features are missing or broken. Extensions written with it may
no longer work in future GDevelop releases.
</Trans>
</AlertMessage>
<DismissableAlertMessage
identifier="events-based-object-explanation"
kind="info"

View File

@@ -202,67 +202,54 @@ export const ExtensionOptionsEditor = ({
eventsFunctionsExtension.setCategory(category);
forceUpdate();
}}
// TODO Sort by translated value.
dataSource={[
{
text: '',
value: 'General',
translatableValue: 'General',
},
{
text: 'Ads',
value: 'Ads',
translatableValue: 'Ads',
},
{
text: 'Visual effect',
value: 'Visual effect',
translatableValue: 'Visual effect',
},
{
text: 'Audio',
value: 'Audio',
translatableValue: 'Audio',
},
{
text: 'Advanced',
value: 'Advanced',
translatableValue: 'Advanced',
},
{
text: 'Camera',
value: 'Camera',
translatableValue: 'Camera',
},
{
text: 'Input',
value: 'Input',
translatableValue: 'Input',
},
{
text: 'Game mechanic',
value: 'Game mechanic',
translatableValue: 'Game mechanic',
},
{
text: 'Movement',
value: 'Movement',
translatableValue: 'Movement',
},
{
text: 'Network',
value: 'Network',
translatableValue: 'Network',
},
{
text: 'Third-party',
value: 'Third-party',
translatableValue: 'Third-party',
},
{
text: 'User interface',
value: 'User interface',
translatableValue: 'User interface',
},
]}
/>

View File

@@ -46,9 +46,12 @@ import { type UnsavedChanges } from '../MainFrame/UnsavedChangesContext';
import PreferencesContext from '../MainFrame/Preferences/PreferencesContext';
import { ParametersIndexOffsets } from '../EventsFunctionsExtensionsLoader';
import { sendEventsExtractedAsFunction } from '../Utils/Analytics/EventSender';
import Window from '../Utils/Window';
import { type OnFetchNewlyAddedResourcesFunction } from '../ProjectsStorage/ResourceFetcher';
const gd: libGDevelop = global.gd;
const isDev = Window.isDev();
type Props = {|
project: gdProject,
eventsFunctionsExtension: gdEventsFunctionsExtension,
@@ -93,9 +96,9 @@ type State = {|
// The event based object editor is hidden in releases
// because it's not handled by GDJS.
const getInitialMosaicEditorNodes = (showEventBasedObjectsEditor: boolean) => ({
const initialMosaicEditorNodes = {
direction: 'row',
first: showEventBasedObjectsEditor
first: isDev
? {
direction: 'column',
first: 'free-functions-list',
@@ -120,7 +123,7 @@ const getInitialMosaicEditorNodes = (showEventBasedObjectsEditor: boolean) => ({
splitPercentage: 25,
},
splitPercentage: 25,
});
};
export default class EventsFunctionsExtensionEditor extends React.Component<
Props,
@@ -1421,7 +1424,6 @@ export default class EventsFunctionsExtensionEditor extends React.Component<
{({
getDefaultEditorMosaicNode,
setDefaultEditorMosaicNode,
getShowEventBasedObjectsEditor,
}) => (
<EditorMosaic
ref={editorMosaic => (this._editorMosaic = editorMosaic)}
@@ -1433,28 +1435,21 @@ export default class EventsFunctionsExtensionEditor extends React.Component<
)
}
initialNodes={
// "objects-list" must only appear when the setting is enabled
// and reciprocally.
getShowEventBasedObjectsEditor() ===
// "objects-list" must only appear in dev mode.
isDev ===
mosaicContainsNode(
getDefaultEditorMosaicNode(
'events-functions-extension-editor'
) ||
getInitialMosaicEditorNodes(
getShowEventBasedObjectsEditor()
),
) || initialMosaicEditorNodes,
'objects-list'
)
? getDefaultEditorMosaicNode(
'events-functions-extension-editor'
) ||
getInitialMosaicEditorNodes(
getShowEventBasedObjectsEditor()
)
) || initialMosaicEditorNodes
: // Force the mosaic to reset to default.
getInitialMosaicEditorNodes(
getShowEventBasedObjectsEditor()
)
// It contains "objects-list" only
// in dev mode.
initialMosaicEditorNodes
}
/>
)}

View File

@@ -67,7 +67,7 @@ export const declareBehaviorMetadata = (
extension: gdPlatformExtension,
eventsBasedBehavior: gdEventsBasedBehavior
): gdBehaviorMetadata => {
const behaviorMetadata = extension
return extension
.addEventsBasedBehavior(
eventsBasedBehavior.getName(),
eventsBasedBehavior.getFullName() || eventsBasedBehavior.getName(),
@@ -76,10 +76,6 @@ export const declareBehaviorMetadata = (
getExtensionIconUrl(extension)
)
.setObjectType(eventsBasedBehavior.getObjectType());
if (eventsBasedBehavior.isPrivate()) behaviorMetadata.setPrivate();
return behaviorMetadata;
};
/**
@@ -91,19 +87,12 @@ export const declareObjectMetadata = (
extension: gdPlatformExtension,
eventsBasedObject: gdEventsBasedObject
): gdObjectMetadata => {
const objectMetadata = extension
.addEventsBasedObject(
eventsBasedObject.getName(),
eventsBasedObject.getFullName() || eventsBasedObject.getName(),
eventsBasedObject.getDescription(),
getExtensionIconUrl(extension)
)
// TODO Change the metadata model to only set a category on the extension.
// If an extension has behavior or object across several categories,
// we can assume it's not scoped correctly.
// Note: We shouldn't rely on gdPlatformExtension but this line will
// be removed soon.
.setCategoryFullName(extension.getCategory());
const objectMetadata = extension.addEventsBasedObject(
eventsBasedObject.getName(),
eventsBasedObject.getFullName() || eventsBasedObject.getName(),
eventsBasedObject.getDescription(),
getExtensionIconUrl(extension)
);
// TODO EBO Use full type to identify object to avoid collision.
// Objects are identified by their name alone.
@@ -199,8 +188,8 @@ export const declareObjectMetadata = (
i18n._('Flip the object horizontally'),
i18n._('Flip horizontally _PARAM0_: _PARAM1_'),
i18n._('Effects'),
'res/actions/flipX24.png',
'res/actions/flipX.png'
'res/actions/flipX24_black.png',
'res/actions/flipX_black.png'
)
.addParameter('object', i18n._('Object'), objectType)
.addParameter('yesorno', i18n._('Activate flipping'))
@@ -215,8 +204,8 @@ export const declareObjectMetadata = (
i18n._('Flip the object vertically'),
i18n._('Flip vertically _PARAM0_: _PARAM1_'),
i18n._('Effects'),
'res/actions/flipY24.png',
'res/actions/flipY.png'
'res/actions/flipY24_black.png',
'res/actions/flipY_black.png'
)
.addParameter('object', i18n._('Object'), objectType)
.addParameter('yesorno', i18n._('Activate flipping'))
@@ -231,8 +220,8 @@ export const declareObjectMetadata = (
i18n._('Check if the object is horizontally flipped'),
i18n._('_PARAM0_ is horizontally flipped'),
i18n._('Effects'),
'res/actions/flipX24.png',
'res/actions/flipX.png'
'res/actions/flipX24_black.png',
'res/actions/flipX_black.png'
)
.addParameter('object', i18n._('Object'), objectType)
.getCodeExtraInformation()
@@ -245,8 +234,8 @@ export const declareObjectMetadata = (
i18n._('Check if the object is vertically flipped'),
i18n._('_PARAM0_ is vertically flipped'),
i18n._('Effects'),
'res/actions/flipY24.png',
'res/actions/flipY.png'
'res/actions/flipY24_black.png',
'res/actions/flipY_black.png'
)
.addParameter('object', i18n._('Object'), objectType)
.getCodeExtraInformation()
@@ -428,14 +417,12 @@ export const shiftSentenceParamIndexes = (
sentence: string,
offset: number
): string => {
const parameterIndexesStrings = sentence.match(/_PARAM\d+_/g);
const parameterIndexesStrings = sentence.match(/(?<=_PARAM)(\d+)(?=_)/g);
if (!parameterIndexesStrings) {
return sentence;
}
const parameterIndexes = parameterIndexesStrings.map(indexString =>
Number.parseInt(
indexString.substring('_PARAM'.length, indexString.length - '_'.length)
)
Number.parseInt(indexString)
);
const sentenceElements = sentence.split(/_PARAM\d+_/);
let shiftedSentence = '';
@@ -696,30 +683,7 @@ export const declareObjectInstructionOrExpressionMetadata = (
type gdInstructionOrExpressionMetadata =
| gdInstructionMetadata
| gdExpressionMetadata
| gdMultipleInstructionMetadata;
const convertPropertyTypeToValueType = (propertyType: string): string => {
switch (propertyType) {
case 'Number':
return 'number';
case 'Boolean':
return 'boolean';
case 'Color':
return 'color';
case 'Choice':
return 'stringWithSelector';
case 'String':
default:
return 'string';
}
};
const getStringifiedExtraInfo = (property: gdPropertyDescriptor) => {
return property.getType() === 'Choice'
? JSON.stringify(property.getExtraInfo().toJSArray())
: '';
};
| gdExpressionMetadata;
/**
* Declare the instructions (actions/conditions) and expressions for the
@@ -766,37 +730,176 @@ export const declareBehaviorPropertiesInstructionAndExpressions = (
mapVector(eventsBasedBehavior.getPropertyDescriptors(), property => {
const propertyType = property.getType();
if (propertyType === 'Behavior') {
// Required behaviors don't need accessors and mutators.
return;
}
const propertyName = property.getName();
const getterName = gd.BehaviorCodeGenerator.getBehaviorPropertyGetterName(
propertyName
);
const setterName = gd.BehaviorCodeGenerator.getBehaviorPropertySetterName(
propertyName
);
const propertyLabel = i18n._(
t`${property.getLabel() || propertyName} property`
);
addObjectAndBehaviorParameters(
behaviorMetadata.addExpressionAndConditionAndAction(
convertPropertyTypeToValueType(propertyType),
gd.EventsBasedBehavior.getPropertyExpressionName(propertyName),
propertyLabel,
i18n._(t`the value of ${propertyLabel}`),
i18n._(t`the value of ${propertyLabel}`),
eventsBasedBehavior.getFullName() || eventsBasedBehavior.getName(),
getExtensionIconUrl(extension)
if (propertyType === 'String' || propertyType === 'Choice') {
addObjectAndBehaviorParameters(
behaviorMetadata.addStrExpression(
gd.EventsBasedBehavior.getPropertyExpressionName(propertyName),
propertyLabel,
propertyLabel,
eventsBasedBehavior.getFullName() || eventsBasedBehavior.getName(),
getExtensionIconUrl(extension)
)
)
)
.useStandardParameters(
convertPropertyTypeToValueType(propertyType),
getStringifiedExtraInfo(property)
.getCodeExtraInformation()
.setFunctionName(getterName);
addObjectAndBehaviorParameters(
behaviorMetadata.addScopedCondition(
gd.EventsBasedBehavior.getPropertyConditionName(propertyName),
propertyLabel,
i18n._(t`Compare the content of ${propertyLabel}`),
i18n._(t`the property ${propertyName}`),
eventsBasedBehavior.getFullName() || eventsBasedBehavior.getName(),
getExtensionIconUrl(extension),
getExtensionIconUrl(extension)
)
)
.setFunctionName(
gd.BehaviorCodeGenerator.getBehaviorPropertySetterName(propertyName)
.useStandardRelationalOperatorParameters('string')
.getCodeExtraInformation()
.setFunctionName(getterName);
addObjectAndBehaviorParameters(
behaviorMetadata.addScopedAction(
gd.EventsBasedBehavior.getPropertyActionName(propertyName),
propertyLabel,
i18n._(t`Change the content of ${propertyLabel}`),
i18n._(t`the property ${propertyName}`),
eventsBasedBehavior.getFullName() || eventsBasedBehavior.getName(),
getExtensionIconUrl(extension),
getExtensionIconUrl(extension)
)
)
.setGetter(
gd.BehaviorCodeGenerator.getBehaviorPropertyGetterName(propertyName)
);
.useStandardOperatorParameters('string')
.getCodeExtraInformation()
.setFunctionName(setterName)
.setManipulatedType('string')
.setGetter(getterName);
} else if (propertyType === 'Number') {
addObjectAndBehaviorParameters(
behaviorMetadata.addExpression(
gd.EventsBasedBehavior.getPropertyExpressionName(propertyName),
propertyLabel,
propertyLabel,
eventsBasedBehavior.getFullName() || eventsBasedBehavior.getName(),
getExtensionIconUrl(extension)
)
)
.getCodeExtraInformation()
.setFunctionName(getterName);
addObjectAndBehaviorParameters(
behaviorMetadata.addScopedCondition(
gd.EventsBasedBehavior.getPropertyConditionName(propertyName),
propertyLabel,
i18n._(t`Compare the value of ${propertyLabel}`),
i18n._(t`the property ${propertyName}`),
eventsBasedBehavior.getFullName() || eventsBasedBehavior.getName(),
getExtensionIconUrl(extension),
getExtensionIconUrl(extension)
)
)
.useStandardRelationalOperatorParameters('number')
.getCodeExtraInformation()
.setFunctionName(getterName);
addObjectAndBehaviorParameters(
behaviorMetadata.addScopedAction(
gd.EventsBasedBehavior.getPropertyActionName(propertyName),
propertyLabel,
i18n._(t`Change the value of ${propertyLabel}`),
i18n._(t`the property ${propertyName}`),
eventsBasedBehavior.getFullName() || eventsBasedBehavior.getName(),
getExtensionIconUrl(extension),
getExtensionIconUrl(extension)
)
)
.useStandardOperatorParameters('number')
.getCodeExtraInformation()
.setFunctionName(setterName)
.setGetter(getterName);
} else if (propertyType === 'Boolean') {
addObjectAndBehaviorParameters(
behaviorMetadata.addScopedCondition(
gd.EventsBasedBehavior.getPropertyConditionName(propertyName),
propertyLabel,
i18n._(t`Check the value of ${propertyLabel}`),
i18n._(t`Property ${propertyName} of _PARAM0_ is true`),
eventsBasedBehavior.getFullName() || eventsBasedBehavior.getName(),
getExtensionIconUrl(extension),
getExtensionIconUrl(extension)
)
)
.getCodeExtraInformation()
.setFunctionName(getterName);
addObjectAndBehaviorParameters(
behaviorMetadata.addScopedAction(
gd.EventsBasedBehavior.getPropertyActionName(propertyName),
propertyLabel,
i18n._(t`Update the value of ${propertyLabel}`),
i18n._(t`Set property ${propertyName} of _PARAM0_ to _PARAM2_`),
eventsBasedBehavior.getFullName() || eventsBasedBehavior.getName(),
getExtensionIconUrl(extension),
getExtensionIconUrl(extension)
)
)
.addParameter('yesorno', i18n._(t`New value to set`), '', false)
.getCodeExtraInformation()
.setFunctionName(setterName);
} else if (propertyType === 'Color') {
addObjectAndBehaviorParameters(
behaviorMetadata.addScopedCondition(
gd.EventsBasedBehavior.getPropertyConditionName(propertyName),
propertyLabel,
i18n._(t`Check the color of ${propertyLabel}`),
i18n._(t`Color ${propertyName}`),
eventsBasedBehavior.getFullName() || eventsBasedBehavior.getName(),
getExtensionIconUrl(extension),
getExtensionIconUrl(extension)
)
)
.useStandardRelationalOperatorParameters('string')
.getCodeExtraInformation()
.setFunctionName(getterName);
addObjectAndBehaviorParameters(
behaviorMetadata.addScopedAction(
gd.EventsBasedBehavior.getPropertyActionName(propertyName),
propertyLabel,
i18n._(t`Update the color of ${propertyLabel}`),
i18n._(t`Change color ${propertyName} of _PARAM0_ to _PARAM2_`),
eventsBasedBehavior.getFullName() || eventsBasedBehavior.getName(),
getExtensionIconUrl(extension),
getExtensionIconUrl(extension)
)
)
.addParameter('color', i18n._(t`New color to set`), '', false)
.getCodeExtraInformation()
.setFunctionName(setterName);
addObjectAndBehaviorParameters(
behaviorMetadata.addStrExpression(
gd.EventsBasedBehavior.getPropertyExpressionName(propertyName),
propertyLabel,
propertyLabel,
eventsBasedBehavior.getFullName() || eventsBasedBehavior.getName(),
getExtensionIconUrl(extension)
)
)
.getCodeExtraInformation()
.setFunctionName(getterName);
}
});
};
@@ -830,36 +933,6 @@ export const declareObjectPropertiesInstructionAndExpressions = (
return instructionOrExpression;
};
mapVector(eventsBasedObject.getPropertyDescriptors(), property => {
const propertyType = property.getType();
const propertyName = property.getName();
const propertyLabel = i18n._(
t`${property.getLabel() || propertyName} property`
);
addObjectParameter(
objectMetadata.addExpressionAndConditionAndAction(
convertPropertyTypeToValueType(propertyType),
gd.EventsBasedObject.getPropertyExpressionName(propertyName),
propertyLabel,
i18n._(t`the value of ${propertyLabel}`),
i18n._(t`the value of ${propertyLabel}`),
eventsBasedObject.getFullName() || eventsBasedObject.getName(),
getExtensionIconUrl(extension)
)
)
.useStandardParameters(
convertPropertyTypeToValueType(propertyType),
getStringifiedExtraInfo(property)
)
.setFunctionName(
gd.BehaviorCodeGenerator.getBehaviorPropertySetterName(propertyName)
)
.setGetter(
gd.BehaviorCodeGenerator.getBehaviorPropertyGetterName(propertyName)
);
});
mapVector(eventsBasedObject.getPropertyDescriptors(), property => {
const propertyType = property.getType();
const propertyName = property.getName();
@@ -980,7 +1053,7 @@ export const declareObjectPropertiesInstructionAndExpressions = (
gd.EventsBasedObject.getPropertyActionName(propertyName),
propertyLabel,
i18n._(t`Update the value of ${propertyLabel}`),
i18n._(t`Set property ${propertyName} of _PARAM0_ to _PARAM1_`),
i18n._(t`Set property ${propertyName} of _PARAM0_ to _PARAM2_`),
eventsBasedObject.getFullName() || eventsBasedObject.getName(),
getExtensionIconUrl(extension),
getExtensionIconUrl(extension)
@@ -1010,7 +1083,7 @@ export const declareObjectPropertiesInstructionAndExpressions = (
gd.EventsBasedObject.getPropertyActionName(propertyName),
propertyLabel,
i18n._(t`Update the color of ${propertyLabel}`),
i18n._(t`Change color ${propertyName} of _PARAM0_ to _PARAM1_`),
i18n._(t`Change color ${propertyName} of _PARAM0_ to _PARAM2_`),
eventsBasedObject.getFullName() || eventsBasedObject.getName(),
getExtensionIconUrl(extension),
getExtensionIconUrl(extension)

View File

@@ -31,7 +31,6 @@ const styles = {
listContainer: {
flex: 1,
},
tooltip: { marginRight: 5, verticalAlign: 'bottom' },
};
export type EventsFunctionCreationParameters = {|
@@ -42,12 +41,11 @@ export type EventsFunctionCreationParameters = {|
const renderEventsFunctionLabel = (eventsFunction: gdEventsFunction) =>
eventsFunction.isPrivate() ? (
<>
<Tooltip
title={
<Trans>This function won't be visible in the events editor.</Trans>
}
>
<VisibilityOffIcon fontSize="small" style={styles.tooltip} />
<Tooltip title="This function won't be visible in the events editor">
<VisibilityOffIcon
fontSize="small"
style={{ marginRight: 5, verticalAlign: 'bottom' }}
/>
</Tooltip>
<span title={eventsFunction.getName()}>{eventsFunction.getName()}</span>
</>

View File

@@ -42,9 +42,6 @@ const styles = {
color: '#d4d4d4',
overflowX: 'hidden',
maxWidth: '100%',
whiteSpace: 'normal',
wordBreak: 'normal',
overflowWrap: 'anywhere',
},
comment: {
color: '#777',

View File

@@ -15,8 +15,8 @@ import {
} from '../../InstructionOrExpression/CreateTree';
import {
enumerateAllInstructions,
enumerateFreeInstructions,
deduplicateInstructionsList,
enumerateFreeInstructionsWithTranslatedCategories,
} from '../../InstructionOrExpression/EnumerateInstructions';
import {
type EnumeratedInstructionMetadata,
@@ -96,7 +96,6 @@ type Props = {|
onSearchStartOrReset?: () => void,
style?: Object,
onClickMore?: () => void,
i18n: I18nType,
|};
const iconSize = 24;
@@ -121,10 +120,7 @@ export default class InstructionOrObjectSelector extends React.PureComponent<
// Free instructions, to be displayed in a tab next to the objects.
freeInstructionsInfo: Array<EnumeratedInstructionMetadata> = filterEnumeratedInstructionOrExpressionMetadataByScope(
enumerateFreeInstructionsWithTranslatedCategories(
this.props.isCondition,
this.props.i18n
),
enumerateFreeInstructions(this.props.isCondition),
this.props.scope
);
freeInstructionsInfoTree: InstructionOrExpressionTreeNode = createTree(
@@ -140,12 +136,9 @@ export default class InstructionOrObjectSelector extends React.PureComponent<
groupSearchApi = null;
tagSearchApi = null;
reEnumerateInstructions = (i18n: I18nType) => {
reEnumerateInstructions = () => {
this.freeInstructionsInfo = filterEnumeratedInstructionOrExpressionMetadataByScope(
enumerateFreeInstructionsWithTranslatedCategories(
this.props.isCondition,
i18n
),
enumerateFreeInstructions(this.props.isCondition),
this.props.scope
);
this.freeInstructionsInfoTree = createTree(this.freeInstructionsInfo);

View File

@@ -1,7 +1,5 @@
// @flow
import { Trans } from '@lingui/macro';
import { I18n } from '@lingui/react';
import { type I18n as I18nType } from '@lingui/core';
import * as React from 'react';
import Dialog, { DialogPrimaryButton } from '../../UI/Dialog';
@@ -196,10 +194,10 @@ export default function NewInstructionEditorDialog({
chooseObject(chosenObject.getName());
};
const onExtensionInstalled = (i18n: I18nType) => {
const onExtensionInstalled = () => {
setNewExtensionDialogOpen(false);
freeInstructionComponentRef.current &&
freeInstructionComponentRef.current.reEnumerateInstructions(i18n);
freeInstructionComponentRef.current.reEnumerateInstructions();
};
// Focus the parameters when showing them
@@ -227,38 +225,31 @@ export default function NewInstructionEditorDialog({
: undefined;
const renderInstructionOrObjectSelector = () => (
<I18n>
{({ i18n }) => (
<InstructionOrObjectSelector
key="instruction-or-object-selector"
style={styles.fullHeightSelector}
project={project}
scope={scope}
ref={freeInstructionComponentRef}
currentTab={currentInstructionOrObjectSelectorTab}
onChangeTab={setCurrentInstructionOrObjectSelectorTab}
globalObjectsContainer={globalObjectsContainer}
objectsContainer={objectsContainer}
isCondition={isCondition}
chosenInstructionType={
!chosenObjectName ? instructionType : undefined
}
onChooseInstruction={(instructionType: string) => {
chooseInstruction(instructionType);
setStep('parameters');
}}
chosenObjectName={chosenObjectName}
onChooseObject={(chosenObjectName: string) => {
chooseObject(chosenObjectName);
setStep('object-instructions');
}}
focusOnMount={!instructionType}
onSearchStartOrReset={forceUpdate}
onClickMore={() => setNewExtensionDialogOpen(true)}
i18n={i18n}
/>
)}
</I18n>
<InstructionOrObjectSelector
key="instruction-or-object-selector"
style={styles.fullHeightSelector}
project={project}
scope={scope}
ref={freeInstructionComponentRef}
currentTab={currentInstructionOrObjectSelectorTab}
onChangeTab={setCurrentInstructionOrObjectSelectorTab}
globalObjectsContainer={globalObjectsContainer}
objectsContainer={objectsContainer}
isCondition={isCondition}
chosenInstructionType={!chosenObjectName ? instructionType : undefined}
onChooseInstruction={(instructionType: string) => {
chooseInstruction(instructionType);
setStep('parameters');
}}
chosenObjectName={chosenObjectName}
onChooseObject={(chosenObjectName: string) => {
chooseObject(chosenObjectName);
setStep('object-instructions');
}}
focusOnMount={!instructionType}
onSearchStartOrReset={forceUpdate}
onClickMore={() => setNewExtensionDialogOpen(true)}
/>
);
const renderParameters = () => (
@@ -400,7 +391,6 @@ export default function NewInstructionEditorDialog({
{newBehaviorDialogOpen && chosenObject && (
<NewBehaviorDialog
project={project}
eventsFunctionsExtension={scope.eventsFunctionsExtension}
open={newBehaviorDialogOpen}
objectType={chosenObject.getType()}
objectBehaviorsTypes={listObjectBehaviorsTypes(chosenObject)}
@@ -409,16 +399,12 @@ export default function NewInstructionEditorDialog({
/>
)}
{newExtensionDialogOpen && (
<I18n>
{({ i18n }) => (
<ExtensionsSearchDialog
project={project}
onClose={() => setNewExtensionDialogOpen(false)}
onInstallExtension={() => {}}
onExtensionInstalled={() => onExtensionInstalled(i18n)}
/>
)}
</I18n>
<ExtensionsSearchDialog
project={project}
onClose={() => setNewExtensionDialogOpen(false)}
onInstallExtension={() => {}}
onExtensionInstalled={onExtensionInstalled}
/>
)}
</>
);

View File

@@ -1,6 +1,5 @@
// @flow
import { Trans } from '@lingui/macro';
import { I18n } from '@lingui/react';
import Popover from '@material-ui/core/Popover';
import * as React from 'react';
import {
@@ -139,38 +138,31 @@ export default function NewInstructionEditorMenu({
};
const renderInstructionOrObjectSelector = () => (
<I18n>
{({ i18n }) => (
<InstructionOrObjectSelector
key="instruction-or-object-selector"
style={styles.fullHeightSelector}
project={project}
scope={scope}
currentTab={currentInstructionOrObjectSelectorTab}
onChangeTab={setCurrentInstructionOrObjectSelectorTab}
globalObjectsContainer={globalObjectsContainer}
objectsContainer={objectsContainer}
isCondition={isCondition}
chosenInstructionType={
!chosenObjectName ? instructionType : undefined
}
onChooseInstruction={(instructionType: string) => {
const { instruction, chosenObjectName } = chooseInstruction(
instructionType
);
submitInstruction({ instruction, chosenObjectName });
}}
chosenObjectName={chosenObjectName}
onChooseObject={chosenObjectName => {
chooseObject(chosenObjectName);
setStep('object-instructions');
}}
focusOnMount={!instructionType}
onSearchStartOrReset={forceUpdate}
i18n={i18n}
/>
)}
</I18n>
<InstructionOrObjectSelector
key="instruction-or-object-selector"
style={styles.fullHeightSelector}
project={project}
scope={scope}
currentTab={currentInstructionOrObjectSelectorTab}
onChangeTab={setCurrentInstructionOrObjectSelectorTab}
globalObjectsContainer={globalObjectsContainer}
objectsContainer={objectsContainer}
isCondition={isCondition}
chosenInstructionType={!chosenObjectName ? instructionType : undefined}
onChooseInstruction={(instructionType: string) => {
const { instruction, chosenObjectName } = chooseInstruction(
instructionType
);
submitInstruction({ instruction, chosenObjectName });
}}
chosenObjectName={chosenObjectName}
onChooseObject={chosenObjectName => {
chooseObject(chosenObjectName);
setStep('object-instructions');
}}
focusOnMount={!instructionType}
onSearchStartOrReset={forceUpdate}
/>
);
const renderObjectInstructionSelector = () =>

View File

@@ -72,7 +72,6 @@ const components = {
operator: OperatorField,
yesorno: YesNoField,
trueorfalse: TrueFalseField,
number: ExpressionField,
expression: ExpressionField,
string: StringField,
stringWithSelector: StringWithSelectorField,
@@ -129,7 +128,6 @@ const userFriendlyTypeName: { [string]: MessageDescriptor } = {
yesorno: t`Yes or No`,
trueorfalse: t`True or False`,
expression: t`Number`,
number: t`Number`,
string: t`String`,
stringWithSelector: t`String`,
behavior: t`Behavior`,

View File

@@ -891,6 +891,9 @@ export class EventsSheetComponentWithoutHandle extends React.Component<
locatingEvent: gdBaseEvent,
parameterContext: ParameterContext
) => {
// Prevent state from changing when clicking on a parameter with inline parameter
// editor (it will close the editor).
if (this.state.editedParameter.instruction) return;
const { instruction, parameterIndex } = parameterContext;
this.setState({

View File

@@ -28,7 +28,7 @@ import { formatScore } from '../../Leaderboard/LeaderboardScoreFormatter';
type Props = {|
entries: ?Array<LeaderboardDisplayData>,
customizationSettings: ?LeaderboardCustomizationSettings,
onDeleteEntry: (entry: LeaderboardDisplayData) => Promise<void>,
onDeleteEntry: (entryId: string) => Promise<void>,
isLoading: boolean,
erroredEntry?: {| entryId: string, message: React.Node |},
navigation: {|
@@ -108,7 +108,7 @@ const LeaderboardEntriesTable = ({
<Line>
<IconButton
size="small"
onClick={() => onDeleteEntry(entry)}
onClick={() => onDeleteEntry(entry.id)}
disabled={isLoading}
tooltip={t`Remove entry`}
>

View File

@@ -1,71 +0,0 @@
// @flow
import * as React from 'react';
import { Trans } from '@lingui/macro';
import Text from '../../UI/Text';
import RaisedButton from '../../UI/RaisedButton';
import AlertMessage from '../../UI/AlertMessage';
import { Line, Column } from '../../UI/Grid';
import { type Limits } from '../../Utils/GDevelopServices/Usage';
type Props = {|
onUpgrade: () => void,
onClose: () => void,
limits: Limits,
|};
const MaxLeaderboardCountAlertMessage = ({
onUpgrade,
onClose,
limits,
}: Props) => {
const leaderboardLimits = limits.capabilities.leaderboards;
if (!leaderboardLimits) return null;
return (
<Line>
<Column expand>
<AlertMessage
kind="warning"
onHide={onClose}
renderRightButton={
leaderboardLimits.canMaximumCountPerGameBeIncreased
? () => (
<RaisedButton
primary
label={<Trans>Check our premiums plans</Trans>}
onClick={onUpgrade}
/>
)
: undefined
}
>
<Text size="block-title">
<Trans>
You've reached your maximum of{' '}
{leaderboardLimits.maximumCountPerGame} leaderboards for your game
</Trans>
</Text>
<Text>
{leaderboardLimits.canMaximumCountPerGameBeIncreased ? (
<Trans>
Update to GDevelop Premium to get more leaderboards, storage,
and one-click packagings!
</Trans>
) : (
// This should not happen at the moment since leaderboards are unlimited
// in any paid plans but it could happen in the future with a plan that
// cannot be increased and that has a max number of leaderboards.
<Trans>
To keep using GDevelop leaderboards, consider deleting old,
unused leaderboards.
</Trans>
)}
</Text>
</AlertMessage>
</Column>
</Line>
);
};
export default MaxLeaderboardCountAlertMessage;

View File

@@ -49,11 +49,11 @@ import {
type Leaderboard,
type LeaderboardCustomizationSettings,
type LeaderboardUpdatePayload,
type LeaderboardDisplayData,
shortenUuidForDisplay,
} from '../../Utils/GDevelopServices/Play';
import LeaderboardContext from '../../Leaderboard/LeaderboardContext';
import LeaderboardProvider from '../../Leaderboard/LeaderboardProvider';
import Window from '../../Utils/Window';
import LeaderboardEntriesTable from './LeaderboardEntriesTable';
import { ResponsiveLineStackLayout } from '../../UI/Layout';
import { useResponsiveWindowWidth } from '../../UI/Reponsive/ResponsiveWindowMeasurer';
@@ -68,10 +68,6 @@ import { type LeaderboardSortOption } from '../../Utils/GDevelopServices/Play';
import { formatScore } from '../../Leaderboard/LeaderboardScoreFormatter';
import Toggle from '../../UI/Toggle';
import GDevelopThemeContext from '../../UI/Theme/ThemeContext';
import AuthenticatedUserContext from '../../Profile/AuthenticatedUserContext';
import SubscriptionDialog from '../../Profile/SubscriptionDialog';
import MaxLeaderboardCountAlertMessage from './MaxLeaderboardCountAlertMessage';
import useAlertDialog from '../../UI/Alert/useAlertDialog';
type Props = {|
onLoading: boolean => void,
@@ -193,18 +189,6 @@ export const LeaderboardAdmin = ({
const [isEditingAppearance, setIsEditingAppearance] = React.useState<boolean>(
false
);
const { showConfirmation, showDeleteConfirmation } = useAlertDialog();
const [
displayMaxLeaderboardCountReachedWarning,
setDisplayMaxLeaderboardCountReachedWarning,
] = React.useState<boolean>(false);
const [
subscriptionDialogOpen,
setSubscriptionDialogOpen,
] = React.useState<boolean>(false);
const authenticatedUser = React.useContext(AuthenticatedUserContext);
const { limits } = authenticatedUser;
const [
isEditingSortOptions,
setIsEditingSortOptions,
@@ -328,18 +312,6 @@ export const LeaderboardAdmin = ({
setIsLoading(true);
setApiError(null);
try {
if (limits && leaderboards) {
const leaderboardLimits = limits.capabilities.leaderboards;
if (
leaderboardLimits &&
leaderboardLimits.maximumCountPerGame > 0 &&
leaderboards.length >= leaderboardLimits.maximumCountPerGame
) {
setDisplayMaxLeaderboardCountReachedWarning(true);
return;
}
}
await createLeaderboard({
name: 'New leaderboard',
sort: 'ASC',
@@ -361,11 +333,11 @@ export const LeaderboardAdmin = ({
};
const onResetLeaderboard = async (i18n: I18nType) => {
if (!currentLeaderboard) return;
const answer = await showConfirmation({
title: t`Reset leaderboard ${currentLeaderboard.name}`,
message: t`All current entries will be deleted, are you sure you want to reset this leaderboard? This can't be undone.`,
});
const answer = Window.showConfirmDialog(
i18n._(
t`All current entries will be deleted, are you sure you want to reset this leaderboard? This can't be undone.`
)
);
if (!answer) return;
setIsLoading(true);
@@ -395,13 +367,11 @@ export const LeaderboardAdmin = ({
};
const onDeleteLeaderboard = async (i18n: I18nType) => {
if (!currentLeaderboard) return;
const answer = await showDeleteConfirmation({
title: t`Delete leaderboard ${currentLeaderboard.name}`,
message: t`Are you sure you want to delete this leaderboard and all of its entries? This can't be undone.`,
confirmText: currentLeaderboard.name,
fieldMessage: t`Type the name of the leaderboard:`,
});
const answer = Window.showConfirmDialog(
i18n._(
t`Are you sure you want to delete this leaderboard and all of its entries? This can't be undone.`
)
);
if (!answer) return;
setIsLoading(true);
@@ -424,21 +394,18 @@ export const LeaderboardAdmin = ({
}
};
const onDeleteEntry = async (
i18n: I18nType,
entry: LeaderboardDisplayData
) => {
if (!currentLeaderboard) return;
const answer = await showConfirmation({
title: t`Delete score ${entry.score} from ${entry.playerName}`,
message: t`Are you sure you want to delete this entry? This can't be undone.`,
});
const onDeleteEntry = async (i18n: I18nType, entryId: string) => {
const answer = Window.showConfirmDialog(
i18n._(
t`Are you sure you want to delete this entry? This can't be undone.`
)
);
if (!answer) return;
setIsLoading(true);
setApiError(null);
try {
await deleteLeaderboardEntry(entry.id);
await deleteLeaderboardEntry(entryId);
} catch (err) {
console.error('An error occurred when deleting entry', err);
setApiError({
@@ -448,7 +415,7 @@ export const LeaderboardAdmin = ({
An error occurred when deleting the entry, please try again.
</Trans>
),
itemId: entry.id,
itemId: entryId,
});
} finally {
setIsLoading(false);
@@ -872,201 +839,183 @@ export const LeaderboardAdmin = ({
<I18n>
{({ i18n }) => (
<>
<Column noMargin expand>
{displayMaxLeaderboardCountReachedWarning && limits && (
<MaxLeaderboardCountAlertMessage
onUpgrade={() => setSubscriptionDialogOpen(true)}
onClose={() =>
setDisplayMaxLeaderboardCountReachedWarning(false)
}
limits={limits}
/>
)}
<ResponsiveLineStackLayout noMargin expand noColumnMargin>
<div style={styles.leftColumn}>
<Paper
elevation={5}
style={{
...styles.leaderboardConfigurationPaper,
backgroundColor: gdevelopTheme.palette.alternateCanvasColor,
}}
>
<Column>
<Line noMargin>
{currentLeaderboard && leaderboards ? (
<SelectField
fullWidth
floatingLabelText={<Trans>Leaderboard name</Trans>}
value={currentLeaderboard.id}
onChange={(e, i, leaderboardId) => {
selectLeaderboard(leaderboardId);
}}
>
{leaderboards.map(leaderboard => (
<SelectOption
key={leaderboard.id}
value={leaderboard.id}
primaryText={
leaderboard.primary
? t`${leaderboard.name} (default)`
: leaderboard.name
}
/>
))}
</SelectField>
) : null}
<IconButton
onClick={onCreateLeaderboard}
disabled={isEditingName || isRequestPending}
>
<Add />
</IconButton>
</Line>
{currentLeaderboard ? (
<>
<List>
{getLeaderboardDescription(
i18n,
currentLeaderboard
).map((item, index) => (
<React.Fragment key={`fragment-${item.key}`}>
{index > 0 ? (
<Divider
key={`divider-${item.key}`}
component="li"
/>
) : null}
<ListItem key={item.key} disableGutters>
<ListItemAvatar>
<Avatar>{item.avatar}</Avatar>
</ListItemAvatar>
<ListItemText
disableTypography
secondary={item.secondaryText}
>
{item.text}
</ListItemText>
{item.secondaryAction ? (
<ListItemSecondaryAction>
{item.secondaryAction}
</ListItemSecondaryAction>
) : null}
</ListItem>
</React.Fragment>
))}
</List>
<Line justifyContent="space-between">
<FlatButton
leftIcon={<Delete />}
label={<Trans>Delete</Trans>}
disabled={isRequestPending || isEditingName}
onClick={() => onDeleteLeaderboard(i18n)}
/>
<RaisedButton
label={
currentLeaderboard.primary ? (
<Trans>Default</Trans>
) : (
<Trans>Set as default</Trans>
)
}
disabled={
isRequestPending ||
isEditingName ||
currentLeaderboard.primary
}
onClick={() =>
onUpdateLeaderboard(i18n, { primary: true })
}
/>
</Line>
{apiError &&
(apiError.action === 'leaderboardDeletion' ||
apiError.action === 'leaderboardPrimaryUpdate') ? (
<PlaceholderError>
{apiError.message}
</PlaceholderError>
) : null}
</>
) : null}
</Column>
</Paper>
</div>
<div
<ResponsiveLineStackLayout noMargin expand noColumnMargin>
<div style={styles.leftColumn}>
<Paper
elevation={5}
style={{
...styles.rightColumn,
paddingLeft: windowWidth === 'small' ? 0 : 20,
...styles.leaderboardConfigurationPaper,
backgroundColor: gdevelopTheme.palette.alternateCanvasColor,
}}
>
<Line alignItems="center" justifyContent="flex-end">
<Toggle
size="small"
labelPosition="left"
toggled={displayOnlyBestEntry}
onToggle={(e, newValue) =>
setDisplayOnlyBestEntry(newValue)
}
label={
<Tooltip
title={i18n._(
t`When checked, will only display the best score of each player (only for the display below).`
)}
<Column>
<Line>
{currentLeaderboard && leaderboards ? (
<SelectField
fullWidth
floatingLabelText={<Trans>Leaderboard name</Trans>}
value={currentLeaderboard.id}
onChange={(e, i, leaderboardId) => {
selectLeaderboard(leaderboardId);
}}
>
<Text size="body2">
<Trans>Player best entry</Trans>
</Text>
</Tooltip>
}
/>
<LargeSpacer />
<Divider orientation="vertical" />
<Spacer />
<IconButton
onClick={onFetchLeaderboardEntries}
disabled={isRequestPending || isEditingName}
tooltip={t`Refresh`}
size="small"
>
<Refresh />
</IconButton>
<Spacer />
</Line>
{apiError && apiError.action === 'entriesFetching' ? (
<CenteredError>
<PlaceholderError onRetry={onFetchLeaderboardEntries}>
{apiError.message}
</PlaceholderError>
</CenteredError>
) : (
<LeaderboardEntriesTable
entries={entries}
customizationSettings={
currentLeaderboard
? currentLeaderboard.customizationSettings
: null
}
onDeleteEntry={entry => onDeleteEntry(i18n, entry)}
isLoading={isRequestPending || isEditingName}
navigation={{
goToNextPage,
goToPreviousPage,
goToFirstPage,
}}
erroredEntry={
apiError &&
apiError.action === 'entryDeletion' &&
apiError.itemId
? {
entryId: apiError.itemId,
message: apiError.message,
{leaderboards.map(leaderboard => (
<SelectOption
key={leaderboard.id}
value={leaderboard.id}
primaryText={
leaderboard.primary
? t`${leaderboard.name} (default)`
: leaderboard.name
}
/>
))}
</SelectField>
) : null}
<IconButton
onClick={onCreateLeaderboard}
disabled={isEditingName || isRequestPending}
>
<Add />
</IconButton>
</Line>
{currentLeaderboard ? (
<>
<List>
{getLeaderboardDescription(
i18n,
currentLeaderboard
).map((item, index) => (
<React.Fragment key={`fragment-${item.key}`}>
{index > 0 ? (
<Divider
key={`divider-${item.key}`}
component="li"
/>
) : null}
<ListItem key={item.key} disableGutters>
<ListItemAvatar>
<Avatar>{item.avatar}</Avatar>
</ListItemAvatar>
<ListItemText
disableTypography
secondary={item.secondaryText}
>
{item.text}
</ListItemText>
{item.secondaryAction ? (
<ListItemSecondaryAction>
{item.secondaryAction}
</ListItemSecondaryAction>
) : null}
</ListItem>
</React.Fragment>
))}
</List>
<Line justifyContent="space-between">
<FlatButton
leftIcon={<Delete />}
label={<Trans>Delete</Trans>}
disabled={isRequestPending || isEditingName}
onClick={() => onDeleteLeaderboard(i18n)}
/>
<RaisedButton
label={
currentLeaderboard.primary ? (
<Trans>Default</Trans>
) : (
<Trans>Set as default</Trans>
)
}
: undefined
}
/>
)}
</div>
</ResponsiveLineStackLayout>
</Column>
disabled={
isRequestPending ||
isEditingName ||
currentLeaderboard.primary
}
onClick={() =>
onUpdateLeaderboard(i18n, { primary: true })
}
/>
</Line>
{apiError &&
(apiError.action === 'leaderboardDeletion' ||
apiError.action === 'leaderboardPrimaryUpdate') ? (
<PlaceholderError>{apiError.message}</PlaceholderError>
) : null}
</>
) : null}
</Column>
</Paper>
</div>
<div
style={{
...styles.rightColumn,
paddingLeft: windowWidth === 'small' ? 0 : 20,
}}
>
<Line alignItems="center" justifyContent="flex-end">
<Toggle
size="small"
labelPosition="left"
toggled={displayOnlyBestEntry}
onToggle={(e, newValue) => setDisplayOnlyBestEntry(newValue)}
label={
<Tooltip
title={i18n._(
t`When checked, will only display the best score of each player (only for the display below).`
)}
>
<Text size="body2">
<Trans>Player best entry</Trans>
</Text>
</Tooltip>
}
/>
<LargeSpacer />
<Divider orientation="vertical" />
<Spacer />
<IconButton
onClick={onFetchLeaderboardEntries}
disabled={isRequestPending || isEditingName}
tooltip={t`Refresh`}
size="small"
>
<Refresh />
</IconButton>
<Spacer />
</Line>
{apiError && apiError.action === 'entriesFetching' ? (
<CenteredError>
<PlaceholderError onRetry={onFetchLeaderboardEntries}>
{apiError.message}
</PlaceholderError>
</CenteredError>
) : (
<LeaderboardEntriesTable
entries={entries}
customizationSettings={
currentLeaderboard
? currentLeaderboard.customizationSettings
: null
}
onDeleteEntry={entryId => onDeleteEntry(i18n, entryId)}
isLoading={isRequestPending || isEditingName}
navigation={{
goToNextPage,
goToPreviousPage,
goToFirstPage,
}}
erroredEntry={
apiError &&
apiError.action === 'entryDeletion' &&
apiError.itemId
? { entryId: apiError.itemId, message: apiError.message }
: undefined
}
/>
)}
</div>
</ResponsiveLineStackLayout>
{isEditingAppearance ? (
<LeaderboardAppearanceDialog
open
@@ -1109,12 +1058,6 @@ export const LeaderboardAdmin = ({
extremeAllowedScore={currentLeaderboard.extremeAllowedScore}
/>
) : null}
{subscriptionDialogOpen && (
<SubscriptionDialog
open
onClose={() => setSubscriptionDialogOpen(false)}
/>
)}
</>
)}
</I18n>

View File

@@ -1,10 +1,8 @@
// @flow
import { type I18n as I18nType } from '@lingui/core';
import {
type EnumeratedInstructionMetadata,
type InstructionOrExpressionScope,
} from './EnumeratedInstructionOrExpressionMetadata';
import { translateExtensionCategory } from '../Utils/Extension/ExtensionCategories.js';
const gd: libGDevelop = global.gd;
@@ -72,17 +70,6 @@ export const getExtensionPrefix = (extension: gdPlatformExtension): string => {
return extension.getCategory() + GROUP_DELIMITER + extension.getFullName();
};
const getExtensionTranslatedPrefix = (
extension: gdPlatformExtension,
i18n: I18nType
): string => {
return (
translateExtensionCategory(extension.getCategory(), i18n) +
GROUP_DELIMITER +
extension.getFullName()
);
};
/**
* When all instructions are searched, some can be duplicated
* (on purpose, so that it's easier to find them for users)
@@ -472,30 +459,6 @@ export const enumerateObjectAndBehaviorsInstructions = (
*/
export const enumerateFreeInstructions = (
isCondition: boolean
): Array<EnumeratedInstructionMetadata> => {
return doEnumerateFreeInstructions(isCondition, getExtensionPrefix);
};
/**
* Enumerate all the instructions that are not directly tied
* to an object.
*/
export const enumerateFreeInstructionsWithTranslatedCategories = (
isCondition: boolean,
i18n: I18nType
): Array<EnumeratedInstructionMetadata> => {
return doEnumerateFreeInstructions(isCondition, extension =>
getExtensionTranslatedPrefix(extension, i18n)
);
};
/**
* Enumerate all the instructions that are not directly tied
* to an object.
*/
const doEnumerateFreeInstructions = (
isCondition: boolean,
getExtensionPrefix: (extension: gdPlatformExtension) => string
): Array<EnumeratedInstructionMetadata> => {
let allFreeInstructions = [];

View File

@@ -18,7 +18,7 @@ describe('EnumerateInstructions', () => {
expect.objectContaining({
displayedName: 'Animation finished',
fullGroupName: 'General/Sprite/Animations and images',
type: 'AnimationEnded2',
type: 'AnimationEnded',
}),
expect.objectContaining({
displayedName: 'Trigger once while true',
@@ -105,11 +105,11 @@ describe('EnumerateInstructions', () => {
expect(triggerOnce).not.toBeUndefined();
expect(getObjectParameterIndex(triggerOnce.metadata)).toBe(-1);
const spriteAnimationEnded = conditions.filter(
({ type }) => type === 'AnimationEnded2'
const spriteAnimatedEnded = conditions.filter(
({ type }) => type === 'AnimationEnded'
)[0];
expect(spriteAnimationEnded).not.toBeUndefined();
expect(getObjectParameterIndex(spriteAnimationEnded.metadata)).toBe(0);
expect(spriteAnimatedEnded).not.toBeUndefined();
expect(getObjectParameterIndex(spriteAnimatedEnded.metadata)).toBe(0);
});
it('can enumerate instructions for an object (Sprite)', () => {
@@ -129,7 +129,7 @@ describe('EnumerateInstructions', () => {
expect.arrayContaining([
expect.objectContaining({
displayedName: 'Animation finished',
type: 'AnimationEnded2',
type: 'AnimationEnded',
}),
expect.objectContaining({
displayedName: 'The cursor/touch is on an object',

View File

@@ -57,34 +57,38 @@ export const filterEnumeratedInstructionOrExpressionMetadataByScope = <
scope: EventsScope
): Array<T> => {
return list.filter(enumeratedInstructionOrExpressionMetadata => {
if (!enumeratedInstructionOrExpressionMetadata.isPrivate) return true;
// The instruction or expression is marked as "private":
// we now compare its scope (where it was declared) and the current scope
// (where we are) to see if we should filter it or not.
const {
behaviorMetadata,
extension,
} = enumeratedInstructionOrExpressionMetadata.scope;
const { eventsBasedBehavior, eventsFunctionsExtension } = scope;
return (
(!enumeratedInstructionOrExpressionMetadata.isPrivate &&
(!behaviorMetadata || !behaviorMetadata.isPrivate())) ||
// The instruction or expression is marked as "private":
// we now compare its scope (where it was declared) and the current scope
// (where we are) to see if we should filter it or not.
// Show private behavior functions when editing the behavior
if (
behaviorMetadata &&
eventsBasedBehavior &&
eventsFunctionsExtension &&
getBehaviorFullType(
eventsFunctionsExtension.getName(),
eventsBasedBehavior.getName()
) === behaviorMetadata.getName()
)
return true;
// Show private behavior functions when editing the behavior
(behaviorMetadata &&
eventsBasedBehavior &&
eventsFunctionsExtension &&
getBehaviorFullType(
eventsFunctionsExtension.getName(),
eventsBasedBehavior.getName()
) === behaviorMetadata.getName()) ||
// When editing the extension...
(eventsFunctionsExtension &&
eventsFunctionsExtension.getName() === extension.getName() &&
// ...show public functions of a private behavior
(!enumeratedInstructionOrExpressionMetadata.isPrivate ||
// ...show private non-behavior functions
!behaviorMetadata))
);
// Show private non-behavior functions when editing the extension
if (
!behaviorMetadata &&
eventsFunctionsExtension &&
eventsFunctionsExtension.getName() === extension.getName()
)
return true;
return false;
});
};

View File

@@ -53,10 +53,7 @@ const LayerEditorDialog = (props: Props) => {
hotReloadPreviewButtonProps,
} = props;
const forceUpdate = useForceUpdate();
const {
onCancelChanges,
notifyOfChange,
} = useSerializableObjectCancelableEditor({
const onCancelChanges = useSerializableObjectCancelableEditor({
serializableObject: layer,
onCancel: onClose,
});
@@ -146,7 +143,6 @@ const LayerEditorDialog = (props: Props) => {
onCheck={(e, checked) => {
layer.setVisibility(!checked);
forceUpdate();
notifyOfChange();
}}
tooltipOrHelperText={
<Trans>
@@ -167,7 +163,6 @@ const LayerEditorDialog = (props: Props) => {
onCheck={(e, checked) => {
layer.setFollowBaseLayerCamera(checked);
forceUpdate();
notifyOfChange();
}}
/>
<ColorField
@@ -179,21 +174,15 @@ const LayerEditorDialog = (props: Props) => {
g: layer.getAmbientLightColorGreen(),
b: layer.getAmbientLightColorBlue(),
})}
onChange={newColor => {
const currentRgbColor = {
r: layer.getAmbientLightColorRed(),
g: layer.getAmbientLightColorGreen(),
b: layer.getAmbientLightColorBlue(),
};
const newRgbColor = rgbStringAndAlphaToRGBColor(newColor);
if (newRgbColor) {
onChange={color => {
const rgbColor = rgbStringAndAlphaToRGBColor(color);
if (rgbColor) {
layer.setAmbientLightColor(
newRgbColor.r,
newRgbColor.g,
newRgbColor.b
rgbColor.r,
rgbColor.g,
rgbColor.b
);
forceUpdate();
if (currentRgbColor !== newRgbColor) notifyOfChange();
}
}}
/>
@@ -215,10 +204,9 @@ const LayerEditorDialog = (props: Props) => {
onChooseResource={props.onChooseResource}
resourceExternalEditors={props.resourceExternalEditors}
effectsContainer={layer.getEffects()}
onEffectsUpdated={() => {
forceUpdate(); /*Force update to ensure dialog is properly positioned*/
notifyOfChange();
}}
onEffectsUpdated={
forceUpdate /*Force update to ensure dialog is properly positioned*/
}
/>
)}
</Column>

View File

@@ -45,8 +45,8 @@ export const MaxProjectCountAlertMessage = ({ onUpgrade, limits }: Props) => {
<Text>
{canMaximumCountBeIncreased ? (
<Trans>
Update to GDevelop Premium to get more storage, leaderboards,
and one-click packagings!
Update to GDevelop Premium to get more storage, one click
packagings, and a shiny unicorn!
</Trans>
) : (
<Trans>

View File

@@ -206,7 +206,6 @@ export type PreferencesValues = {|
eventsSheetCancelInlineParameter: 'cancel' | 'apply',
showCommunityExtensions: boolean,
showGetStartedSection: boolean,
showEventBasedObjectsEditor: boolean,
|};
/**
@@ -269,8 +268,6 @@ export type Preferences = {|
setEventsSheetCancelInlineParameter: (value: string) => void,
setShowCommunityExtensions: (enabled: boolean) => void,
setShowGetStartedSection: (enabled: boolean) => void,
setShowEventBasedObjectsEditor: (enabled: boolean) => void,
getShowEventBasedObjectsEditor: () => boolean,
|};
export const initialPreferences = {
@@ -310,7 +307,6 @@ export const initialPreferences = {
eventsSheetCancelInlineParameter: 'apply',
showCommunityExtensions: false,
showGetStartedSection: true,
showEventBasedObjectsEditor: false,
},
setLanguage: () => {},
setThemeName: () => {},
@@ -363,8 +359,6 @@ export const initialPreferences = {
setEventsSheetCancelInlineParameter: () => {},
setShowCommunityExtensions: () => {},
setShowGetStartedSection: (enabled: boolean) => {},
setShowEventBasedObjectsEditor: (enabled: boolean) => {},
getShowEventBasedObjectsEditor: () => false,
};
const PreferencesContext = React.createContext<Preferences>(initialPreferences);

View File

@@ -58,7 +58,6 @@ const PreferencesDialog = ({ i18n, onClose }: Props) => {
setIsAlwaysOnTopInPreview,
setEventsSheetCancelInlineParameter,
setShowCommunityExtensions,
setShowEventBasedObjectsEditor,
} = React.useContext(PreferencesContext);
return (
@@ -330,16 +329,6 @@ const PreferencesDialog = ({ i18n, onClose }: Props) => {
</Trans>
}
/>
<Toggle
onToggle={(e, check) => setShowEventBasedObjectsEditor(check)}
toggled={values.showEventBasedObjectsEditor}
labelPosition="right"
label={
<Trans>
Show custom objects in the extension editor (experimental)
</Trans>
}
/>
{electron && (
<>
<ColumnStackLayout expand noMargin>

View File

@@ -145,12 +145,6 @@ export default class PreferencesProvider extends React.Component<Props, State> {
),
setShowCommunityExtensions: this._setShowCommunityExtensions.bind(this),
setShowGetStartedSection: this._setShowGetStartedSection.bind(this),
setShowEventBasedObjectsEditor: this._setShowEventBasedObjectsEditor.bind(
this
),
getShowEventBasedObjectsEditor: this._getShowEventBasedObjectsEditor.bind(
this
),
};
componentDidMount() {
@@ -358,22 +352,6 @@ export default class PreferencesProvider extends React.Component<Props, State> {
);
}
_setShowEventBasedObjectsEditor(showEventBasedObjectsEditor: boolean) {
this.setState(
state => ({
values: {
...state.values,
showEventBasedObjectsEditor,
},
}),
() => this._persistValuesToLocalStorage(this.state)
);
}
_getShowEventBasedObjectsEditor() {
return this.state.values.showEventBasedObjectsEditor;
}
_checkUpdates(forceDownload?: boolean) {
// Checking for updates is only done on Electron.
// Note: This could be abstracted away later if other updates mechanisms

View File

@@ -16,7 +16,6 @@ export type EditorProps = {|
onChooseResource: ChooseResourceFunction,
resourceExternalEditors: Array<ResourceExternalEditor>,
onSizeUpdated: () => void,
onObjectUpdated?: () => void,
objectName: string,
unsavedChanges?: UnsavedChanges,
|};

View File

@@ -2,7 +2,7 @@
import { Trans } from '@lingui/macro';
import React from 'react';
import { Column, Line } from '../../../UI/Grid';
import { Column } from '../../../UI/Grid';
import { LineStackLayout } from '../../../UI/Layout';
import ImagePreview from '../../../ResourcesList/ResourcePreview/ImagePreview';
import Replay from '@material-ui/icons/Replay';
@@ -16,6 +16,9 @@ import useForceUpdate from '../../../Utils/UseForceUpdate';
import PlaceholderLoader from '../../../UI/PlaceholderLoader';
const styles = {
imageContainer: {
position: 'relative',
},
loaderContainer: {
position: 'absolute',
left: 'calc(50% - 30px)',
@@ -45,7 +48,6 @@ type Props = {|
fixedHeight?: number,
fixedWidth?: number,
isAssetPrivate?: boolean,
hideAnimationLoader?: boolean,
|};
const AnimationPreview = ({
@@ -63,7 +65,6 @@ const AnimationPreview = ({
fixedHeight,
fixedWidth,
isAssetPrivate,
hideAnimationLoader,
}: Props) => {
const forceUdpate = useForceUpdate();
@@ -80,7 +81,6 @@ const AnimationPreview = ({
const timeBetweenFramesRef = React.useRef(timeBetweenFrames);
const pausedRef = React.useRef(false);
const currentFrameIndexRef = React.useRef(0);
const currentResourceNameRef = React.useRef(resourceNames[0]);
const isLoopingRef = React.useRef(isLooping);
const animationNameRef = React.useRef(animationName);
const imagesLoadedArray = React.useRef(
@@ -161,28 +161,17 @@ const AnimationPreview = ({
currentFrameIndexRef.current = newFrameIndex;
currentFrameElapsedTimeRef.current = newFrameElapsedTime;
const newResourceName = resourceNames[currentFrameIndexRef.current];
// Ensure we trigger an update if the frame changes,
// Ensure we trigger an update if the animation changes,
// as the refs will not do it.
if (currentFrameIndex !== newFrameIndex) {
if (newResourceName === currentResourceNameRef.current) {
// Important: if the resource name is the same on the following frame,
// it means the same image is used for multiple frames in the animation.
// In this case, we can consider the image as already loaded.
// Not doing so will cause the animation to be stuck on this frame,
// as the image onLoad will never be triggered.
imagesLoadedArray.current[currentFrameIndexRef.current] = true;
} else {
imagesLoadedArray.current[currentFrameIndexRef.current] = false;
// When the array of loaders changes, wait a bit to display the loader to avoid flickering.
loaderTimeout.current = setTimeout(() => {
console.warn(
'The image took too long to load, displaying a loader.'
);
setIsStillLoadingResources(true);
}, 500);
}
currentResourceNameRef.current = newResourceName;
imagesLoadedArray.current[currentFrameIndexRef.current] = false;
// When the array of loaders changes, wait a bit to display the loader to avoid flickering.
loaderTimeout.current = setTimeout(() => {
console.warn(
'The image took too long to load, displaying a loader.'
);
setIsStillLoadingResources(true);
}, 500);
forceUdpate();
}
}
@@ -232,7 +221,7 @@ const AnimationPreview = ({
return (
<Column expand noOverflowParent noMargin>
<Line noMargin expand>
<div style={styles.imageContainer}>
<ImagePreview
resourceName={resourceName}
imageResourceSource={getImageResourceSource(resourceName)}
@@ -247,12 +236,12 @@ const AnimationPreview = ({
isImagePrivate={isAssetPrivate}
hideLoader // Handled by the animation preview, important to let the browser cache the image.
/>
{!hideAnimationLoader && isStillLoadingResources && (
{isStillLoadingResources && (
<div style={styles.loaderContainer}>
<PlaceholderLoader />
</div>
)}
</Line>
</div>
{!hideControls && (
<LineStackLayout noMargin alignItems="center">
<Text>

View File

@@ -46,15 +46,9 @@ type Props = {|
objectConfiguration: gdSpriteObject,
resourcesLoader: typeof ResourcesLoader,
project: gdProject,
onMasksUpdated?: () => void,
|};
const CollisionMasksEditor = ({
objectConfiguration,
resourcesLoader,
project,
onMasksUpdated,
}: Props) => {
const CollisionMasksEditor = (props: Props) => {
const [animationIndex, setAnimationIndex] = React.useState(0);
const [directionIndex, setDirectionIndex] = React.useState(0);
const [spriteIndex, setSpriteIndex] = React.useState(0);
@@ -81,7 +75,9 @@ const CollisionMasksEditor = ({
const [spriteHeight, setSpriteHeight] = React.useState(0);
const forceUpdate = useForceUpdate();
const spriteConfiguration = gd.asSpriteConfiguration(objectConfiguration);
const spriteConfiguration = gd.asSpriteConfiguration(
props.objectConfiguration
);
const { animation, sprite } = getCurrentElements(
spriteConfiguration,
animationIndex,
@@ -103,7 +99,6 @@ const CollisionMasksEditor = ({
}
forceUpdate(); // Refresh the preview and the list
if (onMasksUpdated) onMasksUpdated();
},
[
animation,
@@ -112,7 +107,6 @@ const CollisionMasksEditor = ({
sameCollisionMasksForAnimations,
sameCollisionMasksForSprites,
forceUpdate,
onMasksUpdated,
]
);
@@ -203,7 +197,7 @@ const CollisionMasksEditor = ({
const editorNodes =
screenSize === 'small' ? verticalMosaicNodes : horizontalMosaicNodes;
if (!objectConfiguration.getAnimationsCount()) return null;
if (!props.objectConfiguration.getAnimationsCount()) return null;
const resourceName = sprite ? sprite.getImageName() : '';
const editors: { [string]: Editor } = {
@@ -214,16 +208,16 @@ const CollisionMasksEditor = ({
<Background>
<ImagePreview
resourceName={resourceName}
imageResourceSource={resourcesLoader.getResourceFullUrl(
project,
imageResourceSource={props.resourcesLoader.getResourceFullUrl(
props.project,
resourceName,
{}
)}
isImageResourceSmooth={isProjectImageResourceSmooth(
project,
props.project,
resourceName
)}
project={project}
project={props.project}
onSize={setCurrentSpriteSize}
renderOverlay={overlayProps =>
sprite && (

View File

@@ -185,7 +185,6 @@ export default class DirectionTools extends Component<Props, State> {
this.setState({ timeBetweenFrames: text })
}
isLooping={direction.isLooping()}
hideAnimationLoader // No need to show a loader in the Direction Tools.
/>
</Dialog>
)}

View File

@@ -46,15 +46,9 @@ type Props = {|
objectConfiguration: gdSpriteObject,
resourcesLoader: typeof ResourcesLoader,
project: gdProject,
onPointsUpdated?: () => void,
|};
const PointsEditor = ({
objectConfiguration,
resourcesLoader,
project,
onPointsUpdated,
}: Props) => {
const PointsEditor = (props: Props) => {
const [animationIndex, setAnimationIndex] = React.useState(0);
const [directionIndex, setDirectionIndex] = React.useState(0);
const [spriteIndex, setSpriteIndex] = React.useState(0);
@@ -76,7 +70,9 @@ const PointsEditor = ({
const [samePointsForSprites, setSamePointsForSprites] = React.useState(false);
const forceUpdate = useForceUpdate();
const spriteConfiguration = gd.asSpriteConfiguration(objectConfiguration);
const spriteConfiguration = gd.asSpriteConfiguration(
props.objectConfiguration
);
const { animation, sprite } = getCurrentElements(
spriteConfiguration,
animationIndex,
@@ -98,7 +94,6 @@ const PointsEditor = ({
}
forceUpdate(); // Refresh the preview
if (onPointsUpdated) onPointsUpdated();
},
[
animation,
@@ -107,7 +102,6 @@ const PointsEditor = ({
samePointsForAnimations,
samePointsForSprites,
forceUpdate,
onPointsUpdated,
]
);
@@ -180,7 +174,7 @@ const PointsEditor = ({
const editorNodes =
screenSize === 'small' ? verticalMosaicNodes : horizontalMosaicNodes;
if (!objectConfiguration.getAnimationsCount()) return null;
if (!props.objectConfiguration.getAnimationsCount()) return null;
const resourceName = sprite ? sprite.getImageName() : '';
const editors: { [string]: Editor } = {
@@ -191,16 +185,16 @@ const PointsEditor = ({
<Background>
<ImagePreview
resourceName={resourceName}
imageResourceSource={resourcesLoader.getResourceFullUrl(
project,
imageResourceSource={props.resourcesLoader.getResourceFullUrl(
props.project,
resourceName,
{}
)}
isImageResourceSmooth={isProjectImageResourceSmooth(
project,
props.project,
resourceName
)}
project={project}
project={props.project}
renderOverlay={overlayProps =>
sprite && (
<PointsPreview

View File

@@ -198,7 +198,6 @@ type AnimationsListContainerProps = {|
extraBottomTools: React.Node,
onSizeUpdated: () => void,
objectName: string,
onObjectUpdated?: () => void,
|};
type AnimationsListContainerState = {|
@@ -228,7 +227,6 @@ class AnimationsListContainer extends React.Component<
emptyAnimation.delete();
this.forceUpdate();
this.props.onSizeUpdated();
if (this.props.onObjectUpdated) this.props.onObjectUpdated();
};
removeAnimation = i => {
@@ -240,7 +238,6 @@ class AnimationsListContainer extends React.Component<
this.props.spriteConfiguration.removeAnimation(i);
this.forceUpdate();
this.props.onSizeUpdated();
if (this.props.onObjectUpdated) this.props.onObjectUpdated();
}
};
@@ -267,7 +264,6 @@ class AnimationsListContainer extends React.Component<
spriteConfiguration.getAnimation(i).setName(newName);
this.forceUpdate();
if (this.props.onObjectUpdated) this.props.onObjectUpdated();
};
deleteSelection = () => {
@@ -281,7 +277,6 @@ class AnimationsListContainer extends React.Component<
this.setState({
selectedSprites: {},
});
if (this.props.onObjectUpdated) this.props.onObjectUpdated();
};
duplicateSelection = () => {
@@ -295,7 +290,6 @@ class AnimationsListContainer extends React.Component<
this.setState({
selectedSprites: {},
});
if (this.props.onObjectUpdated) this.props.onObjectUpdated();
};
openSpriteContextMenu = (x, y, sprite, index) => {
@@ -317,7 +311,6 @@ class AnimationsListContainer extends React.Component<
.getAnimation(animationId)
.setDirection(newDirection, directionId);
this.forceUpdate();
if (this.props.onObjectUpdated) this.props.onObjectUpdated();
};
render() {
@@ -400,7 +393,6 @@ export default function SpriteEditor({
onChooseResource,
resourceExternalEditors,
onSizeUpdated,
onObjectUpdated,
objectName,
}: EditorProps) {
const [pointsEditorOpen, setPointsEditorOpen] = React.useState(false);
@@ -423,7 +415,6 @@ export default function SpriteEditor({
project={project}
objectName={objectName}
onSizeUpdated={onSizeUpdated}
onObjectUpdated={onObjectUpdated}
extraBottomTools={
<ResponsiveLineStackLayout noMargin noColumnMargin>
<RaisedButton
@@ -475,7 +466,6 @@ export default function SpriteEditor({
spriteConfiguration.setUpdateIfNotVisible(!value);
forceUpdate();
if (onObjectUpdated) onObjectUpdated();
}}
/>
</Column>
@@ -508,7 +498,6 @@ export default function SpriteEditor({
objectConfiguration={spriteConfiguration}
resourcesLoader={ResourcesLoader}
project={project}
onPointsUpdated={onObjectUpdated}
/>
</Dialog>
)}
@@ -539,7 +528,6 @@ export default function SpriteEditor({
objectConfiguration={spriteConfiguration}
resourcesLoader={ResourcesLoader}
project={project}
onMasksUpdated={onObjectUpdated}
/>
</Dialog>
)}

View File

@@ -56,9 +56,6 @@ type Props = {|
onUpdateBehaviorsSharedData: () => void,
initialTab: ?ObjectEditorTab,
// Passed down to the behaviors editor:
eventsFunctionsExtension?: gdEventsFunctionsExtension,
// Preview:
hotReloadPreviewButtonProps: HotReloadPreviewButtonProps,
|};
@@ -77,10 +74,7 @@ const InnerDialog = (props: InnerDialogProps) => {
);
const [newObjectName, setNewObjectName] = React.useState(props.objectName);
const forceUpdate = useForceUpdate();
const {
onCancelChanges,
notifyOfChange,
} = useSerializableObjectCancelableEditor({
const onCancelChanges = useSerializableObjectCancelableEditor({
serializableObject: props.object,
useProjectToUnserialize: props.project,
onCancel: props.onCancel,
@@ -208,7 +202,6 @@ const InnerDialog = (props: InnerDialogProps) => {
if (props.canRenameObject(text)) {
setNewObjectName(text);
notifyOfChange();
}
}}
/>
@@ -224,7 +217,6 @@ const InnerDialog = (props: InnerDialogProps) => {
forceUpdate /*Force update to ensure dialog is properly positionned*/
}
objectName={props.objectName}
onObjectUpdated={notifyOfChange}
/>
</Column>
)}
@@ -232,7 +224,6 @@ const InnerDialog = (props: InnerDialogProps) => {
<BehaviorsEditor
object={props.object}
project={props.project}
eventsFunctionsExtension={props.eventsFunctionsExtension}
resourceSources={props.resourceSources}
onChooseResource={props.onChooseResource}
resourceExternalEditors={props.resourceExternalEditors}
@@ -240,7 +231,6 @@ const InnerDialog = (props: InnerDialogProps) => {
forceUpdate /*Force update to ensure dialog is properly positionned*/
}
onUpdateBehaviorsSharedData={props.onUpdateBehaviorsSharedData}
onBehaviorsUpdated={notifyOfChange}
/>
)}
{currentTab === 'variables' && (
@@ -264,7 +254,6 @@ const InnerDialog = (props: InnerDialogProps) => {
}
helpPagePath={'/all-features/variables/object-variables'}
onComputeAllVariableNames={props.onComputeAllVariableNames}
onVariablesUpdated={notifyOfChange}
/>
</Column>
)}
@@ -276,10 +265,9 @@ const InnerDialog = (props: InnerDialogProps) => {
onChooseResource={props.onChooseResource}
resourceExternalEditors={props.resourceExternalEditors}
effectsContainer={props.object.getEffects()}
onEffectsUpdated={() => {
forceUpdate(); /*Force update to ensure dialog is properly positionned*/
notifyOfChange();
}}
onEffectsUpdated={
forceUpdate /*Force update to ensure dialog is properly positionned*/
}
/>
)}
</Dialog>

View File

@@ -25,10 +25,7 @@ const ObjectGroupEditorDialog = ({
objectsContainer,
}: Props) => {
const forceUpdate = useForceUpdate();
const {
onCancelChanges,
notifyOfChange,
} = useSerializableObjectCancelableEditor({
const onCancelChanges = useSerializableObjectCancelableEditor({
serializableObject: group,
onCancel,
});
@@ -64,7 +61,6 @@ const ObjectGroupEditorDialog = ({
onSizeUpdated={
forceUpdate /*Force update to ensure dialog is properly positionned*/
}
onObjectGroupUpdated={notifyOfChange}
/>
</Dialog>
);

View File

@@ -22,7 +22,6 @@ type Props = {|
globalObjectsContainer: gdObjectsContainer,
objectsContainer: gdObjectsContainer,
onSizeUpdated?: () => void,
onObjectGroupUpdated?: () => void,
|};
const ObjectGroupEditor = ({
@@ -31,23 +30,20 @@ const ObjectGroupEditor = ({
globalObjectsContainer,
objectsContainer,
onSizeUpdated,
onObjectGroupUpdated,
}: Props) => {
const [objectName, setObjectName] = React.useState<string>('');
const [newObjectName, setNewObjectName] = React.useState<string>('');
const objectsInGroup = group.getAllObjectsNames().toJSArray();
const removeObject = (objectName: string) => {
group.removeObject(objectName);
if (onSizeUpdated) onSizeUpdated();
if (onObjectGroupUpdated) onObjectGroupUpdated();
};
const addObject = (objectName: string) => {
group.addObject(objectName);
setObjectName('');
setNewObjectName('');
if (onSizeUpdated) onSizeUpdated();
if (onObjectGroupUpdated) onObjectGroupUpdated();
};
const renderExplanation = () => {
@@ -118,9 +114,9 @@ const ObjectGroupEditor = ({
project={project}
globalObjectsContainer={globalObjectsContainer}
objectsContainer={objectsContainer}
value={objectName}
value={newObjectName}
excludedObjectOrGroupNames={objectsInGroup}
onChange={setObjectName}
onChange={setNewObjectName}
onChoose={addObject}
openOnFocus
noGroups

View File

@@ -813,24 +813,19 @@ const ObjectsList = React.forwardRef<Props, ObjectsListInterface>(
placeholder={t`Search objects`}
/>
{newObjectDialogOpen && (
<I18n>
{({ i18n }) => (
<NewObjectDialog
onClose={() => setNewObjectDialogOpen(false)}
onCreateNewObject={addObject}
onObjectAddedFromAsset={onObjectAddedFromAsset}
project={project}
layout={layout}
objectsContainer={objectsContainer}
resourceSources={resourceSources}
onChooseResource={onChooseResource}
resourceExternalEditors={resourceExternalEditors}
onFetchNewlyAddedResources={onFetchNewlyAddedResources}
canInstallPrivateAsset={canInstallPrivateAsset}
i18n={i18n}
/>
)}
</I18n>
<NewObjectDialog
onClose={() => setNewObjectDialogOpen(false)}
onCreateNewObject={addObject}
onObjectAddedFromAsset={onObjectAddedFromAsset}
project={project}
layout={layout}
objectsContainer={objectsContainer}
resourceSources={resourceSources}
onChooseResource={onChooseResource}
resourceExternalEditors={resourceExternalEditors}
onFetchNewlyAddedResources={onFetchNewlyAddedResources}
canInstallPrivateAsset={canInstallPrivateAsset}
/>
)}
{tagEditedObject && (
<EditTagsDialog

View File

@@ -23,9 +23,8 @@ import SelectField from '../UI/SelectField';
import SelectOption from '../UI/SelectOption';
import Text from '../UI/Text';
type Props = {|
type Props = {
loadingScreen: gdLoadingScreen,
onLoadingScreenUpdated: () => void,
onChangeSubscription: () => void,
// For resources:
@@ -33,11 +32,10 @@ type Props = {|
resourceSources: Array<ResourceSource>,
onChooseResource: ChooseResourceFunction,
resourceExternalEditors: Array<ResourceExternalEditor>,
|};
};
export const LoadingScreenEditor = ({
loadingScreen,
onLoadingScreenUpdated,
onChangeSubscription,
project,
resourceSources,
@@ -47,11 +45,6 @@ export const LoadingScreenEditor = ({
const subscriptionChecker = React.useRef<?SubscriptionChecker>(null);
const forceUpdate = useForceUpdate();
const onUpdate = () => {
forceUpdate();
onLoadingScreenUpdated();
};
return (
<I18n>
{({ i18n }) => (
@@ -68,11 +61,9 @@ export const LoadingScreenEditor = ({
resourceExternalEditors={resourceExternalEditors}
resourceKind="image"
resourceName={loadingScreen.getBackgroundImageResourceName()}
onChange={newResourceName => {
const currentResourceName = loadingScreen.getBackgroundImageResourceName();
if (currentResourceName === newResourceName) return;
loadingScreen.setBackgroundImageResourceName(newResourceName);
onUpdate();
onChange={resourceName => {
loadingScreen.setBackgroundImageResourceName(resourceName);
forceUpdate();
}}
/>
</Line>
@@ -82,12 +73,9 @@ export const LoadingScreenEditor = ({
floatingLabelText={<Trans>Background color</Trans>}
disableAlpha
color={hexNumberToRGBString(loadingScreen.getBackgroundColor())}
onChange={newColor => {
const currentBackgroundColor = loadingScreen.getBackgroundColor();
const newBackgroundColor = rgbStringToHexNumber(newColor);
if (currentBackgroundColor === newBackgroundColor) return;
loadingScreen.setBackgroundColor(newBackgroundColor);
onUpdate();
onChange={color => {
loadingScreen.setBackgroundColor(rgbStringToHexNumber(color));
forceUpdate();
}}
/>
<SemiControlledTextField
@@ -98,21 +86,11 @@ export const LoadingScreenEditor = ({
fullWidth
type="number"
value={'' + loadingScreen.getBackgroundFadeInDuration()}
onChange={newValue => {
const currentBackgroundFadeInDuration = loadingScreen.getBackgroundFadeInDuration();
const newBackgroundFadeInDuration = Math.max(
0,
parseFloat(newValue)
);
if (
currentBackgroundFadeInDuration ===
newBackgroundFadeInDuration
)
return;
onChange={value => {
loadingScreen.setBackgroundFadeInDuration(
newBackgroundFadeInDuration
Math.max(0, parseFloat(value))
);
onUpdate();
forceUpdate();
}}
/>
</ResponsiveLineStackLayout>
@@ -133,18 +111,16 @@ export const LoadingScreenEditor = ({
return;
}
loadingScreen.showGDevelopSplash(checked);
onUpdate();
forceUpdate();
}}
/>
<SelectField
fullWidth
floatingLabelText={<Trans>GDevelop logo style</Trans>}
value={loadingScreen.getGDevelopLogoStyle()}
onChange={(e, i, newGdevelopLogoStyle: string) => {
const currentGDevelopLogoStyle = loadingScreen.getGDevelopLogoStyle();
if (currentGDevelopLogoStyle === newGdevelopLogoStyle) return;
loadingScreen.setGDevelopLogoStyle(newGdevelopLogoStyle);
onUpdate();
onChange={(e, i, value: string) => {
loadingScreen.setGDevelopLogoStyle(value);
forceUpdate();
}}
>
<SelectOption value="light" primaryText={t`Light (plain)`} />
@@ -167,21 +143,11 @@ export const LoadingScreenEditor = ({
fullWidth
type="number"
value={'' + loadingScreen.getLogoAndProgressLogoFadeInDelay()}
onChange={newValue => {
const currentLogoAndProgressLogoFadeInDelay = loadingScreen.getLogoAndProgressLogoFadeInDelay();
const newLogoAndProgressLogoFadeInDelay = Math.max(
0,
parseFloat(newValue)
);
if (
currentLogoAndProgressLogoFadeInDelay ===
newLogoAndProgressLogoFadeInDelay
)
return;
onChange={value => {
loadingScreen.setLogoAndProgressLogoFadeInDelay(
newLogoAndProgressLogoFadeInDelay
Math.max(0, parseFloat(value))
);
onUpdate();
forceUpdate();
}}
/>
<SemiControlledTextField
@@ -192,21 +158,11 @@ export const LoadingScreenEditor = ({
fullWidth
type="number"
value={'' + loadingScreen.getLogoAndProgressFadeInDuration()}
onChange={newValue => {
const currentLogoAndProgressFadeInDuration = loadingScreen.getLogoAndProgressFadeInDuration();
const newLogoAndProgressFadeInDuration = Math.max(
0,
parseFloat(newValue)
);
if (
currentLogoAndProgressFadeInDuration ===
newLogoAndProgressFadeInDuration
)
return;
onChange={value => {
loadingScreen.setLogoAndProgressFadeInDuration(
newLogoAndProgressFadeInDuration
Math.max(0, parseFloat(value))
);
onUpdate();
forceUpdate();
}}
/>
</ResponsiveLineStackLayout>
@@ -218,7 +174,7 @@ export const LoadingScreenEditor = ({
checked={loadingScreen.getShowProgressBar()}
onCheck={(e, checked) => {
loadingScreen.setShowProgressBar(checked);
onUpdate();
forceUpdate();
}}
/>
<ResponsiveLineStackLayout noMargin>
@@ -227,17 +183,11 @@ export const LoadingScreenEditor = ({
fullWidth
type="number"
value={'' + loadingScreen.getProgressBarMinWidth()}
onChange={newValue => {
const currentProgressBarMinWidth = loadingScreen.getProgressBarMinWidth();
const newProgressBarMinWidth = Math.max(
0,
parseFloat(newValue)
onChange={value => {
loadingScreen.setProgressBarMinWidth(
Math.max(0, parseFloat(value))
);
if (currentProgressBarMinWidth === newProgressBarMinWidth) {
return;
}
loadingScreen.setProgressBarMinWidth(newProgressBarMinWidth);
onUpdate();
forceUpdate();
}}
helperMarkdownText={i18n._(t`In pixels. 0 to ignore.`)}
/>
@@ -246,22 +196,11 @@ export const LoadingScreenEditor = ({
fullWidth
type="number"
value={'' + loadingScreen.getProgressBarWidthPercent()}
onChange={newValue => {
const currentProgressBarWidthPercent = loadingScreen.getProgressBarWidthPercent();
const newProgressBarWidthPercent = Math.min(
100,
Math.max(1, parseFloat(newValue))
);
if (
currentProgressBarWidthPercent === newProgressBarWidthPercent
) {
return;
}
onChange={value => {
loadingScreen.setProgressBarWidthPercent(
newProgressBarWidthPercent
Math.min(100, Math.max(1, parseFloat(value)))
);
onUpdate();
forceUpdate();
}}
helperMarkdownText={i18n._(t`As a percent of the game width.`)}
/>
@@ -270,17 +209,11 @@ export const LoadingScreenEditor = ({
fullWidth
type="number"
value={'' + loadingScreen.getProgressBarMaxWidth()}
onChange={newValue => {
const currentProgressBarMaxWidth = loadingScreen.getProgressBarMaxWidth();
const newProgressBarMaxWidth = Math.max(
0,
parseFloat(newValue)
onChange={value => {
loadingScreen.setProgressBarMaxWidth(
Math.max(0, parseFloat(value))
);
if (currentProgressBarMaxWidth === newProgressBarMaxWidth) {
return;
}
loadingScreen.setProgressBarMaxWidth(newProgressBarMaxWidth);
onUpdate();
forceUpdate();
}}
helperMarkdownText={i18n._(t`In pixels. 0 to ignore.`)}
/>
@@ -290,14 +223,11 @@ export const LoadingScreenEditor = ({
fullWidth
type="number"
value={'' + loadingScreen.getProgressBarHeight()}
onChange={newValue => {
const currentProgressBarHeight = loadingScreen.getProgressBarHeight();
const newProgressBarHeight = Math.max(1, parseFloat(newValue));
if (currentProgressBarHeight === newProgressBarHeight) {
return;
}
loadingScreen.setProgressBarHeight(newProgressBarHeight);
onUpdate();
onChange={value => {
loadingScreen.setProgressBarHeight(
Math.max(1, parseFloat(value))
);
forceUpdate();
}}
/>
<ColorField
@@ -305,14 +235,9 @@ export const LoadingScreenEditor = ({
floatingLabelText={<Trans>Progress bar color</Trans>}
disableAlpha
color={hexNumberToRGBString(loadingScreen.getProgressBarColor())}
onChange={newColor => {
const currentProgressBarColor = loadingScreen.getProgressBarColor();
const newProgressBarColor = rgbStringToHexNumber(newColor);
if (currentProgressBarColor === newProgressBarColor) {
return;
}
loadingScreen.setProgressBarColor(newProgressBarColor);
onUpdate();
onChange={color => {
loadingScreen.setProgressBarColor(rgbStringToHexNumber(color));
forceUpdate();
}}
/>
<Text size="block-title">
@@ -326,14 +251,9 @@ export const LoadingScreenEditor = ({
fullWidth
type="number"
value={'' + loadingScreen.getMinDuration()}
onChange={newValue => {
const currentMinDuration = loadingScreen.getMinDuration();
const newMinDuration = Math.max(0, parseFloat(newValue));
if (currentMinDuration === newMinDuration) {
return;
}
loadingScreen.setMinDuration(newMinDuration);
onUpdate();
onChange={value => {
loadingScreen.setMinDuration(Math.max(0, parseFloat(value)));
forceUpdate();
}}
helperMarkdownText={i18n._(
t`When previewing the game in the editor, this duration is ignored (the game preview starts as soon as possible).`

View File

@@ -196,17 +196,11 @@ function ProjectPropertiesDialog(props: Props) {
'properties' | 'loading-screen'
>(props.initialTab);
const {
onCancelChanges: onCancelLoadingScreenChanges,
notifyOfChange: notifyOfLoadingScreenChange,
} = useSerializableObjectCancelableEditor({
const onCancelLoadingScreenChanges = useSerializableObjectCancelableEditor({
serializableObject: project.getLoadingScreen(),
onCancel: props.onClose,
});
const {
onCancelChanges,
notifyOfChange,
} = useSerializableObjectCancelableEditor({
const onCancelChanges = useSerializableObjectCancelableEditor({
serializableObject: project.getExtensionProperties(),
onCancel: onCancelLoadingScreenChanges,
});
@@ -308,35 +302,16 @@ function ProjectPropertiesDialog(props: Props) {
</Text>
<PublicGameProperties
name={name}
setName={newName => {
if (newName.trim() === name) {
return;
}
setName(newName.trim());
notifyOfChange();
}}
setName={newName => setName(newName.trim())}
description={description}
setDescription={newDescription => {
if (newDescription === description) {
return;
}
setDescription(newDescription.trim());
notifyOfChange();
}}
setDescription={newDescription =>
setDescription(newDescription.trim())
}
project={project}
authorIds={authorIds}
setAuthorIds={newAuthorIds => {
setAuthorIds(newAuthorIds);
notifyOfChange();
}}
setAuthorIds={setAuthorIds}
orientation={orientation}
setOrientation={newOrientation => {
if (newOrientation === orientation) {
return;
}
setOrientation(newOrientation);
notifyOfChange();
}}
setOrientation={setOrientation}
/>
<Text size="block-title">
<Trans>Packaging</Trans>
@@ -349,13 +324,7 @@ function ProjectPropertiesDialog(props: Props) {
hintText={defaultPackageName}
type="text"
value={packageName}
onChange={newPackageName => {
if (newPackageName === packageName) {
return;
}
setPackageName(newPackageName);
notifyOfChange();
}}
onChange={setPackageName}
errorText={
validatePackageName(packageName) ? (
undefined
@@ -374,13 +343,7 @@ function ProjectPropertiesDialog(props: Props) {
hintText={defaultVersion}
type="text"
value={version}
onChange={newVersion => {
if (newVersion === version) {
return;
}
setVersion(newVersion);
notifyOfChange();
}}
onChange={setVersion}
/>
<SemiControlledTextField
floatingLabelText={<Trans>Publisher name</Trans>}
@@ -391,13 +354,7 @@ function ProjectPropertiesDialog(props: Props) {
)}
type="text"
value={author}
onChange={newAuthor => {
if (newAuthor === author) {
return;
}
setAuthor(newAuthor);
notifyOfChange();
}}
onChange={setAuthor}
/>
{useDeprecatedZeroAsDefaultZOrder ? (
<React.Fragment>
@@ -427,7 +384,6 @@ function ProjectPropertiesDialog(props: Props) {
if (!answer) return;
setUseDeprecatedZeroAsDefaultZOrder(false);
notifyOfChange();
}}
label={
<Trans>
@@ -451,34 +407,18 @@ function ProjectPropertiesDialog(props: Props) {
fullWidth
type="number"
value={'' + gameResolutionWidth}
onChange={value => {
const newResolutionWidth = Math.max(
1,
parseInt(value, 10)
);
if (newResolutionWidth === gameResolutionWidth) {
return;
}
setGameResolutionWidth(newResolutionWidth);
notifyOfChange();
}}
onChange={value =>
setGameResolutionWidth(Math.max(1, parseInt(value, 10)))
}
/>
<SemiControlledTextField
floatingLabelText={<Trans>Game resolution height</Trans>}
fullWidth
type="number"
value={'' + gameResolutionHeight}
onChange={value => {
const newResolutionHeight = Math.max(
1,
parseInt(value, 10)
);
if (newResolutionHeight === gameResolutionHeight) {
return;
}
setGameResolutionHeight(newResolutionHeight);
notifyOfChange();
}}
onChange={value =>
setGameResolutionHeight(Math.max(1, parseInt(value, 10)))
}
/>
</ResponsiveLineStackLayout>
<SelectField
@@ -489,13 +429,9 @@ function ProjectPropertiesDialog(props: Props) {
</Trans>
}
value={sizeOnStartupMode}
onChange={(e, i, newSizeOnStartupMode: string) => {
if (newSizeOnStartupMode === sizeOnStartupMode) {
return;
}
setSizeOnStartupMode(newSizeOnStartupMode);
notifyOfChange();
}}
onChange={(e, i, value: string) =>
setSizeOnStartupMode(value)
}
>
<SelectOption
value=""
@@ -519,10 +455,9 @@ function ProjectPropertiesDialog(props: Props) {
}
disabled={sizeOnStartupMode === ''}
checked={adaptGameResolutionAtRuntime}
onCheck={(e, checked) => {
setAdaptGameResolutionAtRuntime(checked);
notifyOfChange();
}}
onCheck={(e, checked) =>
setAdaptGameResolutionAtRuntime(checked)
}
/>
<ResponsiveLineStackLayout noMargin>
<SemiControlledTextField
@@ -530,14 +465,9 @@ function ProjectPropertiesDialog(props: Props) {
fullWidth
type="number"
value={'' + minFPS}
onChange={value => {
const newMinFPS = Math.max(0, parseInt(value, 10));
if (newMinFPS === minFPS) {
return;
}
setMinFPS(newMinFPS);
notifyOfChange();
}}
onChange={value =>
setMinFPS(Math.max(0, parseInt(value, 10)))
}
/>
<SemiControlledTextField
floatingLabelText={
@@ -546,14 +476,9 @@ function ProjectPropertiesDialog(props: Props) {
fullWidth
type="number"
value={'' + maxFPS}
onChange={value => {
const newMaxFPS = Math.max(0, parseInt(value, 10));
if (newMaxFPS === maxFPS) {
return;
}
setMaxFPS(newMaxFPS);
notifyOfChange();
}}
onChange={value =>
setMaxFPS(Math.max(0, parseInt(value, 10)))
}
/>
</ResponsiveLineStackLayout>
{maxFPS > 0 && maxFPS < 60 && (
@@ -592,13 +517,7 @@ function ProjectPropertiesDialog(props: Props) {
<Trans>Scale mode (also called "Sampling")</Trans>
}
value={scaleMode}
onChange={(e, i, newScaleMode: string) => {
if (newScaleMode === scaleMode) {
return;
}
setScaleMode(newScaleMode);
notifyOfChange();
}}
onChange={(e, i, value: string) => setScaleMode(value)}
>
<SelectOption
value="linear"
@@ -617,10 +536,7 @@ function ProjectPropertiesDialog(props: Props) {
</Trans>
}
checked={pixelsRounding}
onCheck={(e, checked) => {
setPixelsRounding(checked);
notifyOfChange();
}}
onCheck={(e, checked) => setPixelsRounding(checked)}
/>
{scaleMode === 'nearest' && (
<DismissableAlertMessage
@@ -654,14 +570,9 @@ function ProjectPropertiesDialog(props: Props) {
fullWidth
floatingLabelText={<Trans>Project file type</Trans>}
value={isFolderProject ? 'folder-project' : 'single-file'}
onChange={(e, i, value: string) => {
const newIsFolderProject = value === 'folder-project';
if (newIsFolderProject === isFolderProject) {
return;
}
setIsFolderProject(newIsFolderProject);
notifyOfChange();
}}
onChange={(e, i, value: string) =>
setIsFolderProject(value === 'folder-project')
}
helperMarkdownText={i18n._(
t`Note that this option will only have an effect when saving your project on your computer's filesystem from the desktop app.`
)}
@@ -681,7 +592,6 @@ function ProjectPropertiesDialog(props: Props) {
{currentTab === 'loading-screen' && (
<LoadingScreenEditor
loadingScreen={project.getLoadingScreen()}
onLoadingScreenUpdated={notifyOfLoadingScreenChange}
onChangeSubscription={() => {
onCancelChanges();
props.onChangeSubscription();

View File

@@ -11,11 +11,28 @@ import {
} from '../../Utils/BlobDownloader';
import { type FileMetadata } from '../index';
import { type AuthenticatedUser } from '../../Profile/AuthenticatedUserContext';
import {
extractFilenameWithExtensionFromProductAuthorizedUrl,
isProductAuthorizedResourceUrl,
} from '../../Utils/GDevelopServices/Shop';
import { isBlobURL, isURL } from '../../ResourcesList/ResourceUtils';
import { extractFilenameAndExtensionFromProductAuthorizedUrl } from '../../Utils/GDevelopServices/Shop';
const isURL = (filename: string) => {
return (
filename.startsWith('http://') ||
filename.startsWith('https://') ||
filename.startsWith('ftp://') ||
filename.startsWith('blob:') ||
filename.startsWith('data:')
);
};
const isPrivateAssetUrl = (filename: string) => {
return (
filename.startsWith('https://private-assets-dev.gdevelop.io') ||
filename.startsWith('https://private-assets.gdevelop.io')
);
};
const isBlobURL = (filename: string) => {
return filename.startsWith('blob:');
};
export const moveUrlResourcesToCloudFilesIfPrivate = async ({
project,
@@ -56,14 +73,17 @@ export const moveUrlResourcesToCloudFilesIfPrivate = async ({
const resourceFile = resource.getFile();
if (isURL(resourceFile)) {
if (isProductAuthorizedResourceUrl(resourceFile)) {
const filenameWithExtension = extractFilenameWithExtensionFromProductAuthorizedUrl(
if (isPrivateAssetUrl(resourceFile)) {
const {
extension,
filenameWithoutExtension,
} = extractFilenameAndExtensionFromProductAuthorizedUrl(
resourceFile
);
return {
resource,
url: resourceFile,
filename: filenameWithExtension,
filename: filenameWithoutExtension + extension,
};
} else if (isBlobURL(resourceFile)) {
result.erroredResources.push({

View File

@@ -16,7 +16,20 @@ import {
downloadUrlsToBlobs,
type ItemResult,
} from '../../Utils/BlobDownloader';
import { isBlobURL, isURL } from '../../ResourcesList/ResourceUtils';
const isURL = (filename: string) => {
return (
filename.startsWith('http://') ||
filename.startsWith('https://') ||
filename.startsWith('ftp://') ||
filename.startsWith('blob:') ||
filename.startsWith('data:')
);
};
const isBlobURL = (filename: string) => {
return filename.startsWith('blob:');
};
export const moveAllCloudProjectResourcesToCloudProject = async ({
project,

View File

@@ -4,20 +4,20 @@ import PromisePool from '@supercharge/promise-pool';
import { retryIfFailed } from '../../Utils/RetryIfFailed';
import newNameGenerator from '../../Utils/NewNameGenerator';
import { type FileMetadata } from '../index';
import {
extractFilenameWithExtensionFromProductAuthorizedUrl,
isProductAuthorizedResourceUrl,
} from '../../Utils/GDevelopServices/Shop';
import {
extractFilenameWithExtensionFromPublicAssetResourceUrl,
isPublicAssetResourceUrl,
} from '../../Utils/GDevelopServices/Asset';
import { isFetchableUrl } from '../../ResourcesList/ResourceUtils';
import { extractFilenameAndExtensionFromProductAuthorizedUrl } from '../../Utils/GDevelopServices/Shop';
const electron = optionalRequire('electron');
const ipcRenderer = electron ? electron.ipcRenderer : null;
const fs = optionalRequire('fs-extra');
const path = optionalRequire('path');
const isFetchableUrl = (filename: string) => {
return (
filename.startsWith('http://') ||
filename.startsWith('https://') ||
filename.startsWith('ftp://')
);
};
type Options = {|
project: gdProject,
fileMetadata: FileMetadata,
@@ -53,45 +53,25 @@ export const moveUrlResourcesToLocalFiles = async ({
const resource = resourcesManager.getResource(resourceName);
const url = resource.getFile();
let filenameWithExtension;
if (isProductAuthorizedResourceUrl(url)) {
// Resource is a private asset.
filenameWithExtension = extractFilenameWithExtensionFromProductAuthorizedUrl(
url
);
} else if (isPublicAssetResourceUrl(url)) {
// Resource is a public asset.
filenameWithExtension = extractFilenameWithExtensionFromPublicAssetResourceUrl(
url
);
} else {
// Resource is a generic url.
filenameWithExtension = path.basename(url);
}
const extension = path.extname(filenameWithExtension);
const filenameWithoutExtension = path.basename(
filenameWithExtension,
extension
);
const {
extension,
filenameWithoutExtension,
} = extractFilenameAndExtensionFromProductAuthorizedUrl(url);
const name = newNameGenerator(filenameWithoutExtension, name => {
const tentativePath = path.join(baseAssetsPath, name) + extension;
return (
fs.existsSync(tentativePath) || downloadedFilePaths.has(tentativePath)
);
});
const downloadedFilePath = path.join(baseAssetsPath, name) + extension;
downloadedFilePaths.add(downloadedFilePath);
const newPath = path.join(baseAssetsPath, name) + extension;
downloadedFilePaths.add(newPath);
try {
await retryIfFailed({ times: 2 }, async () => {
await fs.ensureDir(baseAssetsPath);
await ipcRenderer.invoke(
'local-file-download',
url,
downloadedFilePath
);
await ipcRenderer.invoke('local-file-download', url, newPath);
resource.setFile(
path.relative(projectPath, downloadedFilePath).replace(/\\/g, '/')
path.relative(projectPath, newPath).replace(/\\/g, '/')
);
});
} catch (error) {

View File

@@ -9,39 +9,23 @@ jest.mock('../../Utils/OptionalRequire');
const mockFn = (fn: Function): JestMockFn<any, any> => fn;
const classicUrl = 'https://www.example.com/file-to-download.png';
const productAuthorizedUrl =
'https://private-assets.gdevelop.io/a2adcae7-ceba-4c0d-ad0f-411bf83692ea/resources/Misc/stars_levels (3).png?token=eyJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJnZGV2ZWxvcC1zaG9wLWFwaSIsImF1ZCI6IjNwejJvWEZHSmVTaTVyVjROQ0pkclU4MjVUVDIiLCJleHAiOjE2NjY5NjM5NDY1OTUsInN1YiI6WyJhMmFkY2FlNy1jZWJhLTRjMGQtYWQwZi00MTFiZjgzNjkyZWEiLCJjM2ZmZjUyZS1lMTZjLTQxMTYtYTYzNS03ZjUzOGRmN2Y1YWEiXX0%3D.WY0V%2B2ypgT0PEWPUKVPSaiazKNfl4ib%2Bf89CpgcdxGo';
const publicResourceUrl =
'https://asset-resources.gdevelop.io/public-resources/16x16 Dungeon Tileset/Armor/0a130324cd2501a97027b518b41231896a81e25034fd3a7baaca9581d079f8b6_Imp_Run_2.png';
const localFileUrl = 'some-local-file.png';
const makeTestProjectWithResourcesToDownload = () => {
const { project } = makeTestProject(gd);
// Add a resource that uses a URL, which will be download
// by the LocalResourceMover.
// by the LocalResourceMover (whatever the origin).
{
const newResource = new gd.ImageResource();
newResource.setName('MyResourceToDownload');
newResource.setFile(classicUrl);
newResource.setFile('http://example/file-to-download.png');
project.getResourcesManager().addResource(newResource);
newResource.delete();
}
// Resource with an authorized URL
{
const newResource = new gd.ImageResource();
newResource.setName('MyAuthorizedResourceToDownload');
newResource.setFile(productAuthorizedUrl);
project.getResourcesManager().addResource(newResource);
newResource.delete();
}
// Resource with a public asset URL
{
const newResource = new gd.ImageResource();
newResource.setName('MyPublicResourceToDownload');
newResource.setFile(publicResourceUrl);
newResource.setName('MyResourceToDownload');
newResource.setFile('http://example/file-to-download.png?token=123');
project.getResourcesManager().addResource(newResource);
newResource.delete();
}
@@ -51,7 +35,7 @@ const makeTestProjectWithResourcesToDownload = () => {
{
const newResource = new gd.ImageResource();
newResource.setName('MyAlreadyLocalResource');
newResource.setFile(localFileUrl);
newResource.setFile('some-local-file.png');
project.getResourcesManager().addResource(newResource);
newResource.delete();
}
@@ -76,9 +60,15 @@ describe('LocalResourceMover', () => {
const project = makeTestProjectWithResourcesToDownload();
// Mock a proper download
mockFn(optionalRequire.mockFsExtra.ensureDir).mockResolvedValue({});
mockFn(optionalRequire.mockFsExtra.existsSync).mockReturnValue(false);
mockFn(optionalRequire.mockElectron.ipcRenderer.invoke).mockResolvedValue();
mockFn(optionalRequire.mockFsExtra.ensureDir).mockImplementation(
async () => {}
);
mockFn(optionalRequire.mockFsExtra.existsSync).mockImplementation(
() => false
);
mockFn(optionalRequire.mockElectron.ipcRenderer.invoke).mockImplementation(
() => Promise.resolve()
);
const options = makeMoveAllProjectResourcesOptions(project);
const fetchedResources = await moveUrlResourcesToLocalFiles(options);
@@ -88,71 +78,42 @@ describe('LocalResourceMover', () => {
optionalRequire.mockElectron.ipcRenderer.invoke
).toHaveBeenCalledWith(
'local-file-download',
classicUrl,
'http://example/file-to-download.png',
path.join('assets', 'file-to-download.png')
);
expect(
optionalRequire.mockElectron.ipcRenderer.invoke
).toHaveBeenCalledWith(
'local-file-download',
productAuthorizedUrl,
path.join('assets', 'stars_levels (3).png')
);
expect(
optionalRequire.mockElectron.ipcRenderer.invoke
).toHaveBeenCalledWith(
'local-file-download',
publicResourceUrl,
path.join('assets', 'Imp_Run_2.png')
);
expect(fetchedResources.erroredResources).toEqual([]);
});
it('reports errors in case of download failure', async () => {
const project = makeTestProjectWithResourcesToDownload();
mockFn(optionalRequire.mockFsExtra.ensureDir).mockResolvedValue({});
mockFn(optionalRequire.mockFsExtra.existsSync).mockReturnValue(false);
// Mock failed download twice for first file
mockFn(optionalRequire.mockElectron.ipcRenderer.invoke).mockRejectedValue(
new Error('Fake download failure')
// Mock a failed download
mockFn(optionalRequire.mockFsExtra.ensureDir).mockImplementation(
async () => {}
);
mockFn(optionalRequire.mockFsExtra.existsSync).mockImplementation(
() => false
);
mockFn(optionalRequire.mockElectron.ipcRenderer.invoke).mockImplementation(
() => Promise.reject(new Error('Fake download failure'))
);
const options = makeMoveAllProjectResourcesOptions(project);
const fetchedResources = await moveUrlResourcesToLocalFiles(options);
// Verify that download was done and reported as failed, even after 2 tries for each file.
// Verify that download was done and reported as failed, even after 2 tries.
expect(
optionalRequire.mockElectron.ipcRenderer.invoke
).toHaveBeenCalledTimes(6);
).toHaveBeenCalledTimes(2);
expect(
optionalRequire.mockElectron.ipcRenderer.invoke
).toHaveBeenCalledWith(
'local-file-download',
classicUrl,
'http://example/file-to-download.png',
path.join('assets', 'file-to-download.png')
);
expect(
optionalRequire.mockElectron.ipcRenderer.invoke
).toHaveBeenCalledWith(
'local-file-download',
productAuthorizedUrl,
path.join('assets', 'stars_levels (3).png')
);
expect(
optionalRequire.mockElectron.ipcRenderer.invoke
).toHaveBeenCalledWith(
'local-file-download',
publicResourceUrl,
path.join('assets', 'Imp_Run_2.png')
);
expect(fetchedResources.erroredResources).toEqual([
{ resourceName: 'MyResourceToDownload', error: expect.any(Error) },
{
resourceName: 'MyAuthorizedResourceToDownload',
error: expect.any(Error),
},
{ resourceName: 'MyPublicResourceToDownload', error: expect.any(Error) },
]);
});
@@ -160,10 +121,16 @@ describe('LocalResourceMover', () => {
const project = makeTestProjectWithResourcesToDownload();
// Mock a failed download once, then successful
mockFn(optionalRequire.mockFsExtra.ensureDir).mockResolvedValue({});
mockFn(optionalRequire.mockFsExtra.existsSync).mockReturnValue(false);
mockFn(optionalRequire.mockFsExtra.ensureDir).mockImplementation(
async () => {}
);
mockFn(optionalRequire.mockFsExtra.existsSync).mockImplementation(
() => false
);
mockFn(optionalRequire.mockElectron.ipcRenderer.invoke)
.mockRejectedValueOnce(new Error('Fake download failure'))
.mockImplementationOnce(() =>
Promise.reject(new Error('Fake download failure'))
)
.mockImplementationOnce(() => Promise.resolve());
const options = makeMoveAllProjectResourcesOptions(project);
@@ -172,27 +139,22 @@ describe('LocalResourceMover', () => {
// Verify that download was done.
expect(
optionalRequire.mockElectron.ipcRenderer.invoke
).toHaveBeenCalledTimes(4);
).toHaveBeenCalledTimes(2);
expect(
optionalRequire.mockElectron.ipcRenderer.invoke
).toHaveBeenCalledWith(
).toHaveBeenNthCalledWith(
1,
'local-file-download',
classicUrl,
'http://example/file-to-download.png',
path.join('assets', 'file-to-download.png')
);
expect(
optionalRequire.mockElectron.ipcRenderer.invoke
).toHaveBeenCalledWith(
).toHaveBeenNthCalledWith(
2,
'local-file-download',
productAuthorizedUrl,
path.join('assets', 'stars_levels (3).png')
);
expect(
optionalRequire.mockElectron.ipcRenderer.invoke
).toHaveBeenCalledWith(
'local-file-download',
publicResourceUrl,
path.join('assets', 'Imp_Run_2.png')
'http://example/file-to-download.png',
path.join('assets', 'file-to-download.png')
);
expect(fetchedResources.erroredResources).toEqual([]);
});

View File

@@ -11,8 +11,20 @@ import UrlStorageProvider from '../UrlStorageProvider';
import DownloadFileStorageProvider from '../DownloadFileStorageProvider';
import { checkIfIsGDevelopCloudBucketUrl } from '../../Utils/CrossOrigin';
import { moveAllCloudProjectResourcesToCloudProject } from '../CloudStorageProvider/CloudResourceMover';
import { isBlobURL, isURL } from '../../ResourcesList/ResourceUtils';
const isURL = (filename: string) => {
return (
filename.startsWith('http://') ||
filename.startsWith('https://') ||
filename.startsWith('ftp://') ||
filename.startsWith('blob:') ||
filename.startsWith('data:')
);
};
const isBlobURL = (filename: string) => {
return filename.startsWith('blob:');
};
const ensureNoCloudProjectResources = async ({
project,
}: MoveAllProjectResourcesOptions): Promise<MoveAllProjectResourcesResult> => {

View File

@@ -1,9 +1,26 @@
// @flow
import PromisePool from '@supercharge/promise-pool';
import axios from 'axios';
import { isFetchableUrl, isURL } from '../../ResourcesList/ResourceUtils';
import { type FileMetadata } from '../index';
const isURL = (filename: string) => {
return (
filename.startsWith('http://') ||
filename.startsWith('https://') ||
filename.startsWith('ftp://') ||
filename.startsWith('blob:') ||
filename.startsWith('data:')
);
};
const isFetchableUrl = (url: string) => {
return (
url.startsWith('http://') ||
url.startsWith('https://') ||
url.startsWith('ftp://')
);
};
type Options = {
project: gdProject,
fileMetadata: FileMetadata,

View File

@@ -152,25 +152,3 @@ export const renameResourcesInProject = (
project.exposeResources(resourcesRenamer);
resourcesRenamer.delete();
};
export const isFetchableUrl = (url: string) => {
return (
url.startsWith('http://') ||
url.startsWith('https://') ||
url.startsWith('ftp://')
);
};
export const isURL = (filename: string) => {
return (
filename.startsWith('http://') ||
filename.startsWith('https://') ||
filename.startsWith('ftp://') ||
filename.startsWith('blob:') ||
filename.startsWith('data:')
);
};
export const isBlobURL = (filename: string) => {
return filename.startsWith('blob:');
};

View File

@@ -1,5 +1,6 @@
// @flow
import { renameResourcesInProject } from './ResourceUtils';
import { makeTestProject } from '../fixtures/TestProject';
const gd: libGDevelop = global.gd;
const addNewAnimationWithImageToSpriteObject = (

View File

@@ -74,7 +74,7 @@ type Props = {|
* If `onApply` is also specified, this must be interpreted as a "cancelling"
* of changes.
*/
onRequestClose?: () => void | Promise<void>,
onRequestClose?: () => void,
/**
* If specified, will be called when the dialog is dismissed in a way where changes

Some files were not shown because too many files have changed in this diff Show More