Compare commits

...

27 Commits

Author SHA1 Message Date
AlexandreS
81ef11163d Bump newIDE version 2022-06-21 16:41:38 +02:00
AlexandreS
fcc19a6dcf Fix collision bug introduced when fixing a collision bug 2022-06-21 16:41:18 +02:00
github-actions[bot]
bcad2d5667 Update translations [skip ci] (#4042)
Co-authored-by: D8H <D8H@users.noreply.github.com>
2022-06-21 16:01:54 +02:00
D8H
3c83e5d24a No longer count pauses (when the tab is hidden) for the players session duration statistics (#4054) 2022-06-21 15:04:41 +02:00
Florian Rival
d07088900f Bump newIDE version 2022-06-16 18:28:13 +02:00
github-actions[bot]
fcb0c27e23 Update translations [skip ci] (#4030) 2022-06-16 17:06:43 +02:00
Fannie Yan
a6ee1c3e1c Allow more freedom on horizontal movements in events sheet (#4037)
* Allow more freedom on horizontal movements in events sheet
2022-06-16 11:09:26 +02:00
D8H
5669eae198 [Physics] Add mass and moment of inertia expressions (#4000) 2022-06-16 10:29:28 +02:00
AlexandreS
8f97a5ba69 Fix: Detect Physics2 contacts between objects that happen between frames 2022-06-16 10:12:15 +02:00
D8H
d57a755b2f Extract the analytics panel from the game detail dialog (#4038)
* Don't show in changelogs
2022-06-15 19:33:14 +02:00
Fannie Yan
5118421de7 Suggest all expression types in Text fields (#4033)
* Suggest all expressions in autocompletion suggestion and in expression selector for Text parameter fields
* Automatically convert number expressions to Text (using ToString()) when needed
2022-06-15 15:58:06 +02:00
Arthur Pacaud
ff15a37da7 Add WebManifest to exported web games (#4021)
* This allows a game hosted on a website to have a proper icon and orientation when a shortcut to it is added to the home screen on Android or iOS
2022-06-14 15:15:35 +02:00
Florian Rival
beac19089f Upgrade gh-pages to avoid history of the published web-app to take useless space
* history option was not working previously

Don't show in changelog
2022-06-13 15:45:37 +02:00
Florian Rival
e86e4ef9f5 Update all dark themes and rework the light theme to use the modern look'n'feel (#4028) 2022-06-13 12:54:10 +02:00
github-actions[bot]
693a2dbd2c Update translations [skip ci] (#4006)
Co-authored-by: AlexandreSi <AlexandreSi@users.noreply.github.com>
2022-06-13 12:32:11 +02:00
AlexandreS
23318c2c28 Fix: Remove clicks on checkboxes that are far from the label on the same line 2022-06-13 10:55:11 +02:00
Clément Pasteau
3e811f1b9d Fix asset thumbnail sometimes having a wrong size when navigating the Asset store 2022-06-13 10:26:46 +02:00
Florian Rival
9ea6c034c3 Fix category of "Error of last save attempt" leaderboard expression 2022-06-12 17:47:24 +02:00
Sebastian Krzyszkowiak
b0ac0b1254 Fix music files staying muted after loading after upgrading to Howler.js 2.2.3 (#4011)
* This is caused by a bug in recent versions of Howler.js. Since there's no fix in Howler yet, this change reverts the problematic commit (0323af9b84) on top of v2.2.3 release.
* See https://github.com/goldfire/howler.js/issues/1603 for more details.

Only show in developer changelog
2022-06-09 10:19:42 +02:00
Florian Rival
e8b34ca535 Fix camera actions/conditions/expressions broken since introducing new expressions (#4012)
Removed some logs so that it's easier to spot errors reported.

Don't show in changelog
2022-06-09 10:09:14 +02:00
Clément Pasteau
70226f45aa Rework Asset Pack add to a list of filtered assets
Do not show in changelog
2022-06-08 17:10:58 +02:00
Clément Pasteau
b1572102c7 Bump IDE to 5.0.136
Do not show in changelog
2022-06-08 15:10:49 +02:00
Sebastian Krzyszkowiak
63da874469 Preload sounds sequentially to avoid overloading the browser
- Trying to preload all sounds at once can overwhelm the browser
in case of projects with many big sound files. Load the files
sequentially, the same way images are being preloaded.
2022-06-08 15:02:54 +02:00
Sebastian Krzyszkowiak
0963f2b8f6 Fix sounds being looped correctly on Firefox
- Upgrading Howler.js to v2.2.3 fixes sounds not being looped under recent Firefox versions (see https://github.com/goldfire/howler.js/issues/1442)
2022-06-08 15:00:01 +02:00
github-actions[bot]
5aa4fd5739 Update translations [skip ci] (#3924) 2022-06-08 14:57:29 +02:00
Oxey405
11c0248df5 Added an option to remove all unused resources 2022-06-08 14:53:11 +02:00
Fannie Yan
5bdc9769df Add camera borders expressions and conditions (#3999)
* Add CameraBorderLeft, CameraBorderRight, CameraBorderTop and CameraBorderBottom expressions and conditions
* Add CameraCenterX and CameraCenterY expressions and hide CameraX and CameraY expressions
2022-06-08 10:01:45 +02:00
155 changed files with 4809 additions and 2202 deletions

View File

@@ -29,7 +29,7 @@ void GD_CORE_API BuiltinExtensionsImplementer::ImplementsCameraExtension(
extension
.AddExpressionAndConditionAndAction(
"number",
"CameraX",
"CameraCenterX",
_("Camera center X position"),
_("the X position of the center of a camera"),
_("the X position of camera _PARAM4_ (layer: _PARAM3_)"),
@@ -43,15 +43,25 @@ void GD_CORE_API BuiltinExtensionsImplementer::ImplementsCameraExtension(
.SetDefaultValue("0")
.MarkAsAdvanced();
// Compatibility with GD <= 5.0.135
extension.AddDuplicatedCondition("CameraX", "CameraCenterX")
.SetHidden(); // Deprecated
extension.AddDuplicatedExpression("CameraX", "CameraCenterX")
.SetHidden(); // Deprecated
extension.AddDuplicatedAction("SetCameraX", "SetCameraCenterX")
.SetHidden(); // Deprecated
extension.AddDuplicatedAction("CameraX", "SetCameraX")
.SetHidden(); // Deprecated
extension.AddDuplicatedExpression("VueX", "CameraX")
.SetHidden(); // Deprecated
// end of compatibility code
extension
.AddExpressionAndConditionAndAction(
"number",
"CameraY",
"CameraCenterY",
_("Camera center Y position"),
_("the Y position of the center of a camera"),
_("the Y position of camera _PARAM4_ (layer: _PARAM3_)"),
@@ -65,10 +75,20 @@ void GD_CORE_API BuiltinExtensionsImplementer::ImplementsCameraExtension(
.SetDefaultValue("0")
.MarkAsAdvanced();
// Compatibility with GD <= 5.0.135
extension.AddDuplicatedCondition("CameraY", "CameraCenterY")
.SetHidden(); // Deprecated
extension.AddDuplicatedExpression("CameraY", "CameraCenterY")
.SetHidden(); // Deprecated
extension.AddDuplicatedAction("SetCameraY", "SetCameraCenterY")
.SetHidden(); // Deprecated
extension.AddDuplicatedAction("CameraY", "SetCameraY")
.SetHidden(); // Deprecated
extension.AddDuplicatedExpression("VueY", "CameraY")
.SetHidden(); // Deprecated
// end of compatibility code
extension
.AddExpressionAndCondition(
@@ -80,9 +100,9 @@ void GD_CORE_API BuiltinExtensionsImplementer::ImplementsCameraExtension(
"",
"res/conditions/camera24.png")
.AddCodeOnlyParameter("currentScene", "")
.AddParameter("layer", _("Layer (base layer if empty)"))
.AddParameter("layer", _("Layer (base layer if empty)"), "", true)
.SetDefaultValue("\"\"")
.AddParameter("expression", _("Camera number"))
.AddParameter("expression", _("Camera number"), "", true)
.UseStandardParameters("number")
.MarkAsAdvanced();
@@ -96,9 +116,77 @@ void GD_CORE_API BuiltinExtensionsImplementer::ImplementsCameraExtension(
"",
"res/conditions/camera24.png")
.AddCodeOnlyParameter("currentScene", "")
.AddParameter("layer", _("Layer (base layer if empty)"))
.AddParameter("layer", _("Layer (base layer if empty)"), "", true)
.SetDefaultValue("\"\"")
.AddParameter("expression", _("Camera number"))
.AddParameter("expression", _("Camera number"), "", true)
.UseStandardParameters("number")
.MarkAsAdvanced();
extension
.AddExpressionAndCondition(
"number",
"CameraBorderLeft",
_("Camera left border position"),
_("the position of the left border of a camera"),
_("the position of the left border of camera _PARAM2_ of layer "
"_PARAM1_"),
"",
"res/conditions/camera24.png")
.AddCodeOnlyParameter("currentScene", "")
.AddParameter("layer", _("Layer (base layer if empty)"), "", true)
.SetDefaultValue("\"\"")
.AddParameter("expression", _("Camera number"), "", true)
.UseStandardParameters("number")
.MarkAsAdvanced();
extension
.AddExpressionAndCondition(
"number",
"CameraBorderRight",
_("Camera right border position"),
_("the position of the right border of a camera"),
_("the position of the right border of camera _PARAM2_ of layer "
"_PARAM1_"),
"",
"res/conditions/camera24.png")
.AddCodeOnlyParameter("currentScene", "")
.AddParameter("layer", _("Layer (base layer if empty)"), "", true)
.SetDefaultValue("\"\"")
.AddParameter("expression", _("Camera number"), "", true)
.UseStandardParameters("number")
.MarkAsAdvanced();
extension
.AddExpressionAndCondition(
"number",
"CameraBorderTop",
_("Camera top border position"),
_("the position of the top border of a camera"),
_("the position of the top border of camera _PARAM2_ of layer "
"_PARAM1_"),
"",
"res/conditions/camera24.png")
.AddCodeOnlyParameter("currentScene", "")
.AddParameter("layer", _("Layer (base layer if empty)"), "", true)
.SetDefaultValue("\"\"")
.AddParameter("expression", _("Camera number"), "", true)
.UseStandardParameters("number")
.MarkAsAdvanced();
extension
.AddExpressionAndCondition(
"number",
"CameraBorderBottom",
_("Camera bottom border position"),
_("the position of the bottom border of a camera"),
_("the position of the bottom border of camera _PARAM2_ of layer "
"_PARAM1_"),
"",
"res/conditions/camera24.png")
.AddCodeOnlyParameter("currentScene", "")
.AddParameter("layer", _("Layer (base layer if empty)"), "", true)
.SetDefaultValue("\"\"")
.AddParameter("expression", _("Camera number"), "", true)
.UseStandardParameters("number")
.MarkAsAdvanced();

View File

@@ -380,7 +380,7 @@ gd::InstructionMetadata& PlatformExtension::AddDuplicatedAction(
auto copiedAction = actionsInfos.find(copiedNameWithNamespace);
if (copiedAction == actionsInfos.end()) {
gd::LogWarning("Could not find an action with name " +
gd::LogError("Could not find an action with name " +
copiedNameWithNamespace + " to copy.");
} else {
actionsInfos[newNameWithNamespace] = copiedAction->second;
@@ -400,7 +400,7 @@ gd::InstructionMetadata& PlatformExtension::AddDuplicatedCondition(
auto copiedCondition = conditionsInfos.find(copiedNameWithNamespace);
if (copiedCondition == conditionsInfos.end()) {
gd::LogWarning("Could not find a condition with name " +
gd::LogError("Could not find a condition with name " +
copiedNameWithNamespace + " to copy.");
} else {
conditionsInfos[newNameWithNamespace] = copiedCondition->second;
@@ -417,7 +417,7 @@ gd::ExpressionMetadata& PlatformExtension::AddDuplicatedExpression(
auto copiedExpression = expressionsInfos.find(copiedNameWithNamespace);
if (copiedExpression == expressionsInfos.end()) {
gd::LogWarning("Could not find an expression with name " +
gd::LogError("Could not find an expression with name " +
copiedNameWithNamespace + " to copy.");
} else {
expressionsInfos[newNameWithNamespace] = copiedExpression->second;
@@ -434,7 +434,7 @@ gd::ExpressionMetadata& PlatformExtension::AddDuplicatedStrExpression(
auto copiedExpression = strExpressionsInfos.find(copiedNameWithNamespace);
if (copiedExpression == strExpressionsInfos.end()) {
gd::LogWarning("Could not find a string expression with name " +
gd::LogError("Could not find a string expression with name " +
copiedNameWithNamespace + " to copy.");
} else {
strExpressionsInfos[newNameWithNamespace] = copiedExpression->second;

View File

@@ -1,5 +1,5 @@
// @ts-check
describe.only('gdjs.AnchorRuntimeBehavior', function () {
describe('gdjs.AnchorRuntimeBehavior', function () {
const runtimeGame = new gdjs.RuntimeGame({
variables: [],
resources: { resources: [] },

View File

@@ -133,7 +133,7 @@ module.exports = {
'LastSaveError',
_('Error of last save attempt'),
_('Get the error of the last save attempt.'),
_('Error of last save attempt in leaderboard _PARAM0_'),
_('Save score'),
'JsPlatform/Extensions/leaderboard.svg'
)
.addParameter('leaderboardId', _('Leaderboard'), '', true)

View File

@@ -419,7 +419,8 @@ module.exports = {
sharedData
)
.setIncludeFile('Extensions/Physics2Behavior/physics2runtimebehavior.js')
.addIncludeFile('Extensions/Physics2Behavior/box2d.js');
.addIncludeFile('Extensions/Physics2Behavior/box2d.js')
.addIncludeFile('Extensions/Physics2Behavior/utils.js');
// Global
aut
@@ -1396,8 +1397,8 @@ module.exports = {
)
.addParameter('object', _('Object'), '', false)
.addParameter('behavior', _('Behavior'), 'Physics2Behavior')
.addParameter('expression', _('X component (N.m)'))
.addParameter('expression', _('Y component (N.m)'))
.addParameter('expression', _('X component (in Newton * seconds or kilogram * meter per second)'))
.addParameter('expression', _('Y component (in Newton * seconds or kilogram * meter per second)'))
.addParameter('expression', _('Applying X position'))
.addParameter('expression', _('Applying Y position'))
.getCodeExtraInformation()
@@ -1420,7 +1421,7 @@ module.exports = {
.addParameter('object', _('Object'), '', false)
.addParameter('behavior', _('Behavior'), 'Physics2Behavior')
.addParameter('expression', _('Angle'))
.addParameter('expression', _('Length (N.m)'))
.addParameter('expression', _('Length (in Newton * seconds or kilogram * meter per second)'))
.addParameter('expression', _('Applying X position'))
.addParameter('expression', _('Applying Y position'))
.getCodeExtraInformation()
@@ -1442,7 +1443,7 @@ module.exports = {
)
.addParameter('object', _('Object'), '', false)
.addParameter('behavior', _('Behavior'), 'Physics2Behavior')
.addParameter('expression', _('Length (N.m)'))
.addParameter('expression', _('Length (in Newton * seconds or kilogram * meter per second)'))
.addParameter('expression', _('X position'))
.addParameter('expression', _('Y position'))
.addParameter('expression', _('Applying X position'))
@@ -1486,18 +1487,44 @@ module.exports = {
.getCodeExtraInformation()
.setFunctionName('applyAngularImpulse');
aut
aut
.addExpression(
'MassCenterX',
_('Mass center X'),
_('Mass center X'),
'Mass',
_('Mass'),
_('Return the mass of the object (in kilograms)'),
'',
'res/physics32.png'
)
.addParameter('object', _('Object'), '', false)
.addParameter('behavior', _('Behavior'), 'Physics2Behavior')
.getCodeExtraInformation()
.setFunctionName('getMassCenterX');
.setFunctionName('getMass');
aut
.addExpression(
'Inertia',
_('Inertia'),
_('Return the rotational inertia of the object (in kilograms * meters * meters)'),
'',
'res/physics32.png'
)
.addParameter('object', _('Object'), '', false)
.addParameter('behavior', _('Behavior'), 'Physics2Behavior')
.getCodeExtraInformation()
.setFunctionName('getInertia');
aut
.addExpression(
'MassCenterX',
_('Mass center X'),
_('Mass center X'),
'',
'res/physics32.png'
)
.addParameter('object', _('Object'), '', false)
.addParameter('behavior', _('Behavior'), 'Physics2Behavior')
.getCodeExtraInformation()
.setFunctionName('getMassCenterX');
aut
.addExpression(
@@ -3732,9 +3759,9 @@ module.exports = {
.addCondition(
'Collision',
_('Collision'),
_('Test if two objects collide.'),
_('Check if two objects collide.'),
_('_PARAM0_ is colliding with _PARAM2_'),
'',
'',
'res/physics32.png',
'res/physics32.png'
)
@@ -3746,6 +3773,42 @@ module.exports = {
.setIncludeFile('Extensions/Physics2Behavior/physics2tools.js')
.setFunctionName('gdjs.physics2.objectsCollide');
extension
.addCondition(
'CollisionStarted',
_('Collision started'),
_('Check if two objects just started colliding during this frame.'),
_('_PARAM0_ started colliding with _PARAM2_'),
_('Collision'),
'res/physics32.png',
'res/physics32.png'
)
.addParameter('objectList', _('Object'), '', false)
.addParameter('behavior', _('Behavior'), 'Physics2Behavior')
.addParameter('objectList', _('Object'), '', false)
.addCodeOnlyParameter('conditionInverted', '')
.getCodeExtraInformation()
.setIncludeFile('Extensions/Physics2Behavior/physics2tools.js')
.setFunctionName('gdjs.physics2.haveObjectsStartedColliding');
extension
.addCondition(
'CollisionStopped',
_('Collision stopped'),
_('Check if two objects just stopped colliding at this frame.'),
_('_PARAM0_ stopped colliding with _PARAM2_'),
_('Collision'),
'res/physics32.png',
'res/physics32.png'
)
.addParameter('objectList', _('Object'), '', false)
.addParameter('behavior', _('Behavior'), 'Physics2Behavior')
.addParameter('objectList', _('Object'), '', false)
.addCodeOnlyParameter('conditionInverted', '')
.getCodeExtraInformation()
.setIncludeFile('Extensions/Physics2Behavior/physics2tools.js')
.setFunctionName('gdjs.physics2.haveObjectsStoppedColliding');
return extension;
},

View File

@@ -48,13 +48,16 @@ namespace gdjs {
// Get associated behaviors
const behaviorA = contact.GetFixtureA().GetBody()
.gdjsAssociatedBehavior;
.gdjsAssociatedBehavior as Physics2RuntimeBehavior | null;
const behaviorB = contact.GetFixtureB().GetBody()
.gdjsAssociatedBehavior;
.gdjsAssociatedBehavior as Physics2RuntimeBehavior | null;
// Let each behavior know about the contact against the other
behaviorA.currentContacts.push(behaviorB);
behaviorB.currentContacts.push(behaviorA);
if (!behaviorA || !behaviorB) {
return;
}
behaviorA.onContactBegin(behaviorB);
behaviorB.onContactBegin(behaviorA);
};
this.contactListener.EndContact = function (contactPtr) {
// Get the contact
@@ -70,19 +73,16 @@ namespace gdjs {
// Get associated behaviors
const behaviorA = contact.GetFixtureA().GetBody()
.gdjsAssociatedBehavior;
.gdjsAssociatedBehavior as Physics2RuntimeBehavior | null;
const behaviorB = contact.GetFixtureB().GetBody()
.gdjsAssociatedBehavior;
.gdjsAssociatedBehavior as Physics2RuntimeBehavior | null;
// Remove each other contact
let i = behaviorA.currentContacts.indexOf(behaviorB);
if (i !== -1) {
behaviorA.currentContacts.splice(i, 1);
}
i = behaviorB.currentContacts.indexOf(behaviorA);
if (i !== -1) {
behaviorB.currentContacts.splice(i, 1);
if (!behaviorA || !behaviorB) {
return;
}
behaviorA.onContactEnd(behaviorB);
behaviorB.onContactEnd(behaviorA);
};
this.contactListener.PreSolve = function () {};
this.contactListener.PostSolve = function () {};
@@ -241,7 +241,9 @@ namespace gdjs {
layers: any;
masks: any;
shapeScale: number = 1;
currentContacts: any;
contactsStartedThisFrame: Array<Physics2RuntimeBehavior>;
contactsEndedThisFrame: Array<Physics2RuntimeBehavior>;
currentContacts: Array<Physics2RuntimeBehavior>;
_body: any = null;
_sharedData: any;
_tempb2Vec2: any;
@@ -281,7 +283,9 @@ namespace gdjs {
this.gravityScale = behaviorData.gravityScale;
this.layers = behaviorData.layers;
this.masks = behaviorData.masks;
this.currentContacts = this.currentContacts || [];
this.contactsStartedThisFrame = [];
this.contactsEndedThisFrame = [];
this.currentContacts = [];
this.currentContacts.length = 0;
this._sharedData = Physics2SharedData.getSharedData(
runtimeScene,
@@ -741,11 +745,21 @@ namespace gdjs {
this._objectOldX = this.owner.getX();
this._objectOldY = this.owner.getY();
this._objectOldAngle = this.owner.getAngle();
gdjs.physics2.computeCurrentContactsFromStartedAndEndedContacts(
this.currentContacts,
this.contactsStartedThisFrame,
this.contactsEndedThisFrame
);
}
doStepPostEvents(runtimeScene) {
this._updateBodyFromObject();
// Reset contacts that happened this frame
this.contactsStartedThisFrame.length = 0;
this.contactsEndedThisFrame.length = 0;
// Reset world step to update next frame
this._sharedData.stepped = false;
}
@@ -1452,6 +1466,30 @@ namespace gdjs {
this._body.ApplyAngularImpulse(angularImpulse);
}
getMass(): float {
// If there is no body, set a new one
if (this._body === null) {
this.createBody();
}
// Wake up the object
this._body.SetAwake(true);
return this._body.GetMass();
}
getInertia(): float {
// If there is no body, set a new one
if (this._body === null) {
this.createBody();
}
// Wake up the object
this._body.SetAwake(true);
return this._body.GetInertia();
}
getMassCenterX(): float {
// If there is no body, set a new one
if (this._body === null) {
@@ -3797,8 +3835,28 @@ namespace gdjs {
joint.GetBodyB().SetAwake(true);
}
// Collision
static collisionTest(
onContactBegin(otherBehavior: Physics2RuntimeBehavior) {
// There might be contacts that end during the frame and
// start again right away. It is considered a glitch
// and should not be detected.
let i = this.contactsEndedThisFrame.indexOf(otherBehavior);
if (i !== -1) {
this.contactsEndedThisFrame.splice(i, 1);
} else {
this.contactsStartedThisFrame.push(otherBehavior);
}
}
onContactEnd(otherBehavior: Physics2RuntimeBehavior) {
this.contactsEndedThisFrame.push(otherBehavior);
}
/**
* @deprecated Prefer using `Physics2RuntimeBehavior.areObjectsColliding`.
*/
static collisionTest = Physics2RuntimeBehavior.areObjectsColliding;
static areObjectsColliding(
object1: gdjs.RuntimeObject,
object2: gdjs.RuntimeObject,
behaviorName: string
@@ -3806,18 +3864,60 @@ namespace gdjs {
// Test if the second object is in the list of contacts of the first one
const behavior1 = object1.getBehavior(
behaviorName
) as Physics2RuntimeBehavior;
if (!!behavior1) {
for (let i = 0, len = behavior1.currentContacts.length; i < len; ++i) {
if (behavior1.currentContacts[i].owner === object2) {
return true;
}
}
) as Physics2RuntimeBehavior | null;
if (!behavior1) return false;
if (
behavior1.currentContacts.some((behavior) => behavior.owner === object2)
) {
return true;
}
// If a contact has started at this frame and ended right away, it
// won't appear in current contacts but the condition should return
// true anyway.
if (
behavior1.contactsStartedThisFrame.some(
(behavior) => behavior.owner === object2
)
) {
return true;
}
// No contact found
return false;
}
static hasCollisionStartedBetween(
object1: gdjs.RuntimeObject,
object2: gdjs.RuntimeObject,
behaviorName: string
): boolean {
// Test if the second object is in the list of contacts of the first one
const behavior1 = object1.getBehavior(
behaviorName
) as Physics2RuntimeBehavior | null;
if (!behavior1) return false;
return behavior1.contactsStartedThisFrame.some(
(behavior) => behavior.owner === object2
);
}
static hasCollisionStoppedBetween(
object1: gdjs.RuntimeObject,
object2: gdjs.RuntimeObject,
behaviorName: string
): boolean {
// Test if the second object is in the list of contacts of the first one
const behavior1 = object1.getBehavior(
behaviorName
) as Physics2RuntimeBehavior | null;
if (!behavior1) return false;
return behavior1.contactsEndedThisFrame.some(
(behavior) => behavior.owner === object2
);
}
}
gdjs.registerBehavior(
'Physics2::Physics2Behavior',

View File

@@ -7,7 +7,37 @@ namespace gdjs {
inverted: boolean
) {
return gdjs.evtTools.object.twoListsTest(
gdjs.Physics2RuntimeBehavior.collisionTest,
gdjs.Physics2RuntimeBehavior.areObjectsColliding,
objectsLists1,
objectsLists2,
inverted,
behaviorName
);
};
export const haveObjectsStartedColliding = function (
objectsLists1: Hashtable<Array<gdjs.RuntimeObject>>,
behaviorName: string,
objectsLists2: Hashtable<Array<gdjs.RuntimeObject>>,
inverted: boolean
) {
return gdjs.evtTools.object.twoListsTest(
gdjs.Physics2RuntimeBehavior.hasCollisionStartedBetween,
objectsLists1,
objectsLists2,
inverted,
behaviorName
);
};
export const haveObjectsStoppedColliding = function (
objectsLists1: Hashtable<Array<gdjs.RuntimeObject>>,
behaviorName: string,
objectsLists2: Hashtable<Array<gdjs.RuntimeObject>>,
inverted: boolean
) {
return gdjs.evtTools.object.twoListsTest(
gdjs.Physics2RuntimeBehavior.hasCollisionStoppedBetween,
objectsLists1,
objectsLists2,
inverted,

View File

@@ -0,0 +1,321 @@
function assertCollision(object1, object2, options) {
expect(
gdjs.Physics2RuntimeBehavior.hasCollisionStartedBetween(
object1,
object2,
'Physics2'
)
).to.be(options.started);
expect(
gdjs.Physics2RuntimeBehavior.areObjectsColliding(
object1,
object2,
'Physics2'
)
).to.be(options.collision);
expect(
gdjs.Physics2RuntimeBehavior.hasCollisionStoppedBetween(
object1,
object2,
'Physics2'
)
).to.be(options.stopped);
}
function createGameWithSceneWithPhysics2SharedData() {
const runtimeGame = new gdjs.RuntimeGame({
variables: [],
resources: { resources: [] },
properties: { windowWidth: 1000, windowHeight: 1000 },
});
const runtimeScene = new gdjs.RuntimeScene(runtimeGame);
runtimeScene.loadFromScene({
layers: [
{
name: '',
visibility: true,
cameras: [],
effects: [],
ambientLightColorR: 127,
ambientLightColorB: 127,
ambientLightColorG: 127,
isLightingLayer: false,
followBaseLayerCamera: false,
},
],
variables: [],
r: 0,
v: 0,
b: 0,
mangledName: 'Scene1',
name: 'Scene1',
stopSoundsOnStartup: false,
title: '',
behaviorsSharedData: [],
objects: [],
instances: [],
});
runtimeScene.setInitialSharedDataForBehavior('Physics2', {
gravityX: 0,
gravityY: 0,
scaleX: 1,
scaleY: 1,
});
return [runtimeGame, runtimeScene];
}
function createObject(runtimeScene, behaviorProperties) {
const object = new gdjs.TestRuntimeObject(runtimeScene, {
name: 'obj1',
type: '',
behaviors: [
{
name: 'Physics2',
type: 'Physics2::Physics2Behavior',
bodyType: 'Dynamic',
bullet: false,
fixedRotation: true,
canSleep: false,
shape: 'Box',
shapeDimensionA: 0,
shapeDimensionB: 0,
shapeOffsetX: 0,
shapeOffsetY: 0,
polygonOrigin: 'Center',
vertices: [],
density: 1.0,
friction: 0.01,
restitution: 1,
linearDamping: 0,
angularDamping: 0.1,
gravityScale: 1,
layers: 1,
masks: 1,
...behaviorProperties,
},
],
variables: [],
effects: [],
});
object.setCustomWidthAndHeight(10, 10);
runtimeScene.addObject(object);
return object;
}
describe('Physics2RuntimeBehavior', () => {
describe('Contacts computation', () => {
let runtimeGame;
let runtimeScene;
beforeEach(() => {
[runtimeGame, runtimeScene] = createGameWithSceneWithPhysics2SharedData();
});
it('should bounce during the frame (at low fps), collision should be detected, as well as start and stop of the collision', () => {
const fps = 2;
runtimeGame.setGameResolutionSize(1000, 1000);
runtimeScene._timeManager.getElapsedTime = function () {
return (1 / fps) * 1000;
};
const movingObject = createObject(runtimeScene);
const staticObject = createObject(runtimeScene, { bodyType: 'Static' });
staticObject.setPosition(0, 25);
movingObject.setPosition(0, 0);
const staticObjectBehavior = staticObject.getBehavior('Physics2');
const movingObjectBehavior = movingObject.getBehavior('Physics2');
if (!staticObjectBehavior || !movingObjectBehavior) {
throw new Error('Behaviors not found, test cannot be run.');
}
movingObjectBehavior.setLinearVelocityY(40000);
let hasBounced = false;
let stepIndex = 0;
runtimeScene.setEventsFunction(() => {
if (movingObjectBehavior.getLinearVelocityY() > 0) {
// If the moving object has a positive velocity, it hasn't bounced
// on the static object
assertCollision(movingObject, staticObject, {
started: false,
collision: false,
stopped: false,
});
} else {
hasBounced = true;
expect(movingObject.getY() < staticObject.getY()).to.be(true);
assertCollision(movingObject, staticObject, {
started: true,
collision: true,
stopped: true,
});
}
});
while (stepIndex < 10 && !hasBounced) {
runtimeScene.renderAndStep(1000 / fps);
stepIndex++;
}
if (!hasBounced) {
throw new Error('Contact did not happen, nothing was tested.');
}
});
it('should begin to bounce during the frame (at high fps), collision should be detected, as well as start of the collision, but not the end', () => {
const fps = 50;
runtimeGame.setGameResolutionSize(1000, 1000);
runtimeScene._timeManager.getElapsedTime = function () {
return (1 / fps) * 1000;
};
const movingObject = createObject(runtimeScene);
const staticObject = createObject(runtimeScene, { bodyType: 'Static' });
staticObject.setPosition(0, 25);
movingObject.setPosition(0, 0);
const staticObjectBehavior = staticObject.getBehavior('Physics2');
const movingObjectBehavior = movingObject.getBehavior('Physics2');
if (!staticObjectBehavior || !movingObjectBehavior) {
throw new Error('Behaviors not found, test cannot be run.');
}
movingObjectBehavior.setLinearVelocityY(40000);
let hasBegunBouncing = false;
let stepIndex = 0;
runtimeScene.setEventsFunction(() => {
if (movingObjectBehavior.getLinearVelocityY() > 0) {
// If the moving object has a positive velocity, it hasn't bounced
// on the static object
assertCollision(movingObject, staticObject, {
started: false,
collision: false,
stopped: false,
});
} else {
hasBegunBouncing = true;
// At first frame, collision should have only started
expect(movingObject.getY() < staticObject.getY()).to.be(true);
assertCollision(movingObject, staticObject, {
started: true,
collision: true,
stopped: false,
});
}
});
while (stepIndex < 10 && !hasBegunBouncing) {
runtimeScene.renderAndStep(1000 / fps);
stepIndex++;
}
if (!hasBegunBouncing) {
throw new Error(
'Start of contact was not detected, nothing was tested.'
);
}
// At next frame, end of collision should be detected
let hasFinishedBouncing = false;
runtimeScene.setEventsFunction(() => {
hasFinishedBouncing = true;
assertCollision(movingObject, staticObject, {
started: false,
collision: false,
stopped: true,
});
});
runtimeScene.renderAndStep(1000 / fps);
if (!hasFinishedBouncing) {
throw new Error('End of contact was not detected, nothing was tested.');
}
});
});
describe('onContactBegin', () => {
let runtimeGame;
let runtimeScene;
beforeEach(() => {
[runtimeGame, runtimeScene] = createGameWithSceneWithPhysics2SharedData();
});
it('should add behavior to list of started contacts', () => {
const fps = 50;
runtimeGame.setGameResolutionSize(1000, 1000);
const object = createObject(runtimeScene);
const otherObject = createObject(runtimeScene);
const behavior = object.getBehavior('Physics2');
const otherBehavior = otherObject.getBehavior('Physics2');
if (!behavior || !otherBehavior) {
throw new Error('Behavior not found, test cannot be run.');
}
behavior.onContactBegin(otherBehavior);
expect(behavior.contactsStartedThisFrame.length).to.be(1);
expect(behavior.contactsStartedThisFrame[0]).to.be(otherBehavior);
});
it('should add behavior to list of started contacts and ended contacts', () => {
// From the user point of view the objects are colliding but it could be
// quick enough for it to happen between 2 game frames (the physics model
// uses modelling sub-steps). So contact beginning and end should be detected.
const fps = 50;
runtimeGame.setGameResolutionSize(1000, 1000);
const object = createObject(runtimeScene);
const otherObject = createObject(runtimeScene);
const behavior = object.getBehavior('Physics2');
const otherBehavior = otherObject.getBehavior('Physics2');
if (!behavior || !otherBehavior) {
throw new Error('Behavior not found, test cannot be run.');
}
behavior.onContactBegin(otherBehavior);
expect(behavior.contactsStartedThisFrame.length).to.be(1);
expect(behavior.contactsStartedThisFrame[0]).to.be(otherBehavior);
expect(behavior.contactsEndedThisFrame.length).to.be(0);
behavior.onContactEnd(otherBehavior);
expect(behavior.contactsStartedThisFrame.length).to.be(1);
expect(behavior.contactsStartedThisFrame[0]).to.be(otherBehavior);
expect(behavior.contactsEndedThisFrame.length).to.be(1);
expect(behavior.contactsEndedThisFrame[0]).to.be(otherBehavior);
});
it('should not add behavior to list of started contacts if the behavior is also present in the list of ended contacts', () => {
// From the user point of view the objects are staying in contact with each other.
// They would be surprised if the conditions for a contact beginning and
// end were true.
const fps = 50;
runtimeGame.setGameResolutionSize(1000, 1000);
const object = createObject(runtimeScene);
const otherObject = createObject(runtimeScene);
const behavior = object.getBehavior('Physics2');
const otherBehavior = otherObject.getBehavior('Physics2');
if (!behavior || !otherBehavior) {
throw new Error('Behavior not found, test cannot be run.');
}
behavior.onContactEnd(otherBehavior);
expect(behavior.contactsStartedThisFrame.length).to.be(0);
expect(behavior.contactsEndedThisFrame.length).to.be(1);
expect(behavior.contactsEndedThisFrame[0]).to.be(otherBehavior);
behavior.onContactBegin(otherBehavior);
expect(behavior.contactsStartedThisFrame.length).to.be(0);
expect(behavior.contactsEndedThisFrame.length).to.be(0);
});
});
});

View File

@@ -0,0 +1,108 @@
// @ts-check
describe('computeCurrentContactsFromStartedAndEndedContacts', () => {
it('returns same current contacts if nothing happened', () => {
const contacts = ['A', 'B', 'C'];
const startedContacts = [];
const endedContacts = [];
const expectedResolvedContacts = [...contacts];
gdjs.physics2.computeCurrentContactsFromStartedAndEndedContacts(
contacts,
startedContacts,
endedContacts
);
expect(contacts).to.eql(expectedResolvedContacts);
});
it('returns current contacts with started contacts added', () => {
const contacts = ['A', 'B', 'C'];
const startedContacts = ['Z', 'Q'];
const endedContacts = [];
const expectedResolvedContacts = [...contacts, ...startedContacts];
gdjs.physics2.computeCurrentContactsFromStartedAndEndedContacts(
contacts,
startedContacts,
endedContacts
);
expect(contacts).to.eql(expectedResolvedContacts);
});
it('returns current contacts with ended contacts removed', () => {
const contacts = ['A', 'B', 'C'];
const startedContacts = [];
const endedContacts = ['A', 'C'];
const expectedResolvedContacts = ['B'];
gdjs.physics2.computeCurrentContactsFromStartedAndEndedContacts(
contacts,
startedContacts,
endedContacts
);
expect(contacts).to.eql(expectedResolvedContacts);
});
it('returns same current contacts if all started contacts also ended', () => {
const contacts = ['A', 'B', 'C'];
const startedContacts = ['Z', 'X'];
const endedContacts = ['Z', 'X'];
const expectedResolvedContacts = [...contacts];
gdjs.physics2.computeCurrentContactsFromStartedAndEndedContacts(
contacts,
startedContacts,
endedContacts
);
expect(contacts).to.eql(expectedResolvedContacts);
});
it('returns current contacts without started contacts that also ended', () => {
const contacts = ['A', 'B', 'C'];
const startedContacts = ['Z', 'X', 'W'];
const endedContacts = ['Z', 'A', 'X'];
const expectedResolvedContacts = ['B', 'C', 'W'];
gdjs.physics2.computeCurrentContactsFromStartedAndEndedContacts(
contacts,
startedContacts,
endedContacts
);
expect(contacts).to.eql(expectedResolvedContacts);
});
it('returns current contacts with a contact that started and also jittered', () => {
// Should handle cases when this happens during the frame:
// - contact Z starts
// - contact Z ends
// - contact Z starts
// Contact Z should appear in the current contacts.
// We consider a contact shouldn't be able to do that but it should be handled
// in case it happens.
const contacts = ['A', 'B', 'C'];
const startedContacts = ['Z', 'Z'];
const endedContacts = ['Z'];
const expectedResolvedContacts = [...contacts, 'Z'];
gdjs.physics2.computeCurrentContactsFromStartedAndEndedContacts(
contacts,
startedContacts,
endedContacts
);
expect(contacts).to.eql(expectedResolvedContacts);
});
it('returns current contacts without a contact that ended and also jittered', () => {
// Should handle cases where contact C was here and, during the frame:
// - contact C ends
// - contact C starts
// - contact C ends
// Contact C should not appear in the current contacts
// We consider a contact shouldn't be able to do that but it should be handled
// in case it happens.
const contacts = ['A', 'B', 'C'];
const startedContacts = ['C'];
const endedContacts = ['C', 'C'];
const expectedResolvedContacts = ['A', 'B'];
gdjs.physics2.computeCurrentContactsFromStartedAndEndedContacts(
contacts,
startedContacts,
endedContacts
);
expect(contacts).to.eql(expectedResolvedContacts);
});
});

View File

@@ -0,0 +1,19 @@
namespace gdjs {
export namespace physics2 {
export const computeCurrentContactsFromStartedAndEndedContacts = <T>(
current: Array<T>,
started: Array<T>,
ended: Array<T>
): void => {
started.forEach((startedItem) => {
current.push(startedItem);
});
ended.forEach((endedItem) => {
const index = current.indexOf(endedItem);
if (index !== -1) {
current.splice(index, 1);
}
});
};
}
}

File diff suppressed because one or more lines are too long

View File

@@ -20,20 +20,39 @@ CameraExtension::CameraExtension() {
GetAllActions()["CameraY"] // Deprecated
.SetFunctionName("gdjs.evtTools.camera.setCameraY")
.SetGetter("gdjs.evtTools.camera.getCameraY");
GetAllActions()["SetCameraX"]
GetAllActions()["SetCameraX"] // Deprecated
.SetFunctionName("gdjs.evtTools.camera.setCameraX")
.SetGetter("gdjs.evtTools.camera.getCameraX");
GetAllActions()["SetCameraY"]
GetAllActions()["SetCameraY"] // Deprecated
.SetFunctionName("gdjs.evtTools.camera.setCameraY")
.SetGetter("gdjs.evtTools.camera.getCameraY");
GetAllActions()["SetCameraCenterX"]
.SetFunctionName("gdjs.evtTools.camera.setCameraX")
.SetGetter("gdjs.evtTools.camera.getCameraX");
GetAllActions()["SetCameraCenterY"]
.SetFunctionName("gdjs.evtTools.camera.setCameraY")
.SetGetter("gdjs.evtTools.camera.getCameraY");
GetAllConditions()["CameraX"].SetFunctionName(
"gdjs.evtTools.camera.getCameraX");
"gdjs.evtTools.camera.getCameraX"); // Deprecated
GetAllConditions()["CameraY"].SetFunctionName(
"gdjs.evtTools.camera.getCameraY"); // Deprecated
GetAllConditions()["CameraCenterX"].SetFunctionName(
"gdjs.evtTools.camera.getCameraX");
GetAllConditions()["CameraCenterY"].SetFunctionName(
"gdjs.evtTools.camera.getCameraY");
GetAllConditions()["CameraWidth"].SetFunctionName(
"gdjs.evtTools.camera.getCameraWidth");
GetAllConditions()["CameraHeight"].SetFunctionName(
"gdjs.evtTools.camera.getCameraHeight");
GetAllConditions()["CameraBorderLeft"].SetFunctionName(
"gdjs.evtTools.camera.getCameraBorderLeft");
GetAllConditions()["CameraBorderRight"].SetFunctionName(
"gdjs.evtTools.camera.getCameraBorderRight");
GetAllConditions()["CameraBorderTop"].SetFunctionName(
"gdjs.evtTools.camera.getCameraBorderTop");
GetAllConditions()["CameraBorderBottom"].SetFunctionName(
"gdjs.evtTools.camera.getCameraBorderBottom");
GetAllActions()["ShowLayer"].SetFunctionName(
"gdjs.evtTools.camera.showLayer");
GetAllActions()["HideLayer"].SetFunctionName(
@@ -52,13 +71,25 @@ CameraExtension::CameraExtension() {
"gdjs.evtTools.camera.setCameraZoom");
GetAllExpressions()["CameraX"].SetFunctionName(
"gdjs.evtTools.camera.getCameraX");
"gdjs.evtTools.camera.getCameraX"); // Deprecated
GetAllExpressions()["VueX"].SetFunctionName(
"gdjs.evtTools.camera.getCameraX");
"gdjs.evtTools.camera.getCameraX"); // Deprecated
GetAllExpressions()["CameraY"].SetFunctionName(
"gdjs.evtTools.camera.getCameraY");
"gdjs.evtTools.camera.getCameraY"); // Deprecated
GetAllExpressions()["VueY"].SetFunctionName(
"gdjs.evtTools.camera.getCameraY"); // Deprecated
GetAllExpressions()["CameraCenterY"].SetFunctionName(
"gdjs.evtTools.camera.getCameraY");
GetAllExpressions()["CameraCenterX"].SetFunctionName(
"gdjs.evtTools.camera.getCameraX");
GetAllExpressions()["CameraBorderLeft"].SetFunctionName(
"gdjs.evtTools.camera.getCameraBorderLeft");
GetAllExpressions()["CameraBorderRight"].SetFunctionName(
"gdjs.evtTools.camera.getCameraBorderRight");
GetAllExpressions()["CameraBorderTop"].SetFunctionName(
"gdjs.evtTools.camera.getCameraBorderTop");
GetAllExpressions()["CameraBorderBottom"].SetFunctionName(
"gdjs.evtTools.camera.getCameraBorderBottom");
GetAllExpressions()["CameraAngle"].SetFunctionName(
"gdjs.evtTools.camera.getCameraRotation");
GetAllExpressions()["CameraRotation"].SetFunctionName(

View File

@@ -121,6 +121,11 @@ bool Exporter::ExportWholePixiProject(
fs, exportedProject, codeOutputDir + "/data.js", noRuntimeGameOptions);
includesFiles.push_back(codeOutputDir + "/data.js");
// Export a WebManifest with project metadata
if (!fs.WriteToFile(exportDir + "/manifest.webmanifest",
helper.GenerateWebManifest(exportedProject)))
gd::LogError("Unable to export WebManifest.");
helper.ExportIncludesAndLibs(includesFiles, exportDir, false);
gd::String source = gdjsRoot + "/Runtime/index.html";

View File

@@ -9,6 +9,7 @@
#include <emscripten.h>
#endif
#include <algorithm>
#include <array>
#include <fstream>
#include <functional>
#include <sstream>
@@ -844,4 +845,74 @@ void ExporterHelper::AddDeprecatedFontFilesToFontResources(
// end of compatibility code
}
const std::array<int, 20> IOS_ICONS_SIZES = {
180, 60, 120, 76, 152, 40, 80, 57, 114, 72,
144, 167, 29, 58, 87, 50, 20, 100, 167, 1024,
};
const std::array<int, 6> ANDROID_ICONS_SIZES = {36, 48, 72, 96, 144, 192};
const gd::String ExporterHelper::GenerateWebManifest(
const gd::Project &project) {
const gd::String &orientation = project.GetOrientation();
gd::String icons = "[";
{
std::map<int, gd::String> resourcesForSizes;
const auto getFileNameForIcon = [&project](const gd::String &platform,
const int size) {
const gd::String iconName = "icon-" + gd::String::From(size);
return project.GetPlatformSpecificAssets().Has(platform, iconName)
? project.GetResourcesManager()
.GetResource(project.GetPlatformSpecificAssets().Get(
platform, iconName))
.GetFile()
: "";
};
for (const int size : IOS_ICONS_SIZES) {
const auto iconFile = getFileNameForIcon("ios", size);
if (!iconFile.empty()) resourcesForSizes[size] = iconFile;
};
for (const int size : ANDROID_ICONS_SIZES) {
const auto iconFile = getFileNameForIcon("android", size);
if (!iconFile.empty()) resourcesForSizes[size] = iconFile;
};
const auto desktopIconFile = getFileNameForIcon("desktop", 512);
if (!desktopIconFile.empty()) resourcesForSizes[512] = desktopIconFile;
for (const auto &sizeAndFile : resourcesForSizes) {
icons +=
gd::String(R"({
"src": "{FILE}",
"sizes": "{SIZE}x{SIZE}"
},)")
.FindAndReplace("{SIZE}", gd::String::From(sizeAndFile.first))
.FindAndReplace("{FILE}", sizeAndFile.second);
}
}
icons = icons.RightTrim(",") + "]";
return gd::String(R"webmanifest({
"name": "{NAME}",
"short_name": "{NAME}",
"id": "{PACKAGE_ID}",
"description": "{DESCRIPTION}",
"orientation": "{ORIENTATION}",
"start_url": "./index.html",
"display": "standalone",
"background_color": "black",
"categories": ["games", "entertainment"],
"icons": {ICONS}
})webmanifest")
.FindAndReplace("{NAME}", project.GetName())
.FindAndReplace("{PACKAGE_ID}", project.GetPackageName())
.FindAndReplace("{DESCRIPTION}", project.GetDescription())
.FindAndReplace("{ORIENTATION}",
orientation == "default" ? "any" : orientation)
.FindAndReplace("{ICONS}", icons);
};
} // namespace gdjs

View File

@@ -117,11 +117,12 @@ struct PreviewExportOptions {
/**
* Set the path to use for the game engine to require "@electron/remote".
* This is because the preview is run in a folder without any node_module, but this
* is still required for now for some features.
* This should be removed once the dependency to "@electron/remote" is removed.
* This is because the preview is run in a folder without any node_module, but
* this is still required for now for some features. This should be removed
* once the dependency to "@electron/remote" is removed.
*/
PreviewExportOptions &SetElectronRemoteRequirePath(const gd::String& electronRemoteRequirePath_) {
PreviewExportOptions &SetElectronRemoteRequirePath(
const gd::String &electronRemoteRequirePath_) {
electronRemoteRequirePath = electronRemoteRequirePath_;
return *this;
}
@@ -303,6 +304,15 @@ class ExporterHelper {
unsigned int nonRuntimeScriptsCacheBurst,
gd::String additionalSpec);
/**
* \brief Generates a WebManifest, a metadata file that allow to make the
* exported game a working PWA.
*
* \param project The project containing the game properties to generate the
* manifest from.
*/
const gd::String GenerateWebManifest(const gd::Project &project);
/**
* \brief Generate the Cordova configuration file and save it to the export
* directory.

View File

@@ -74,6 +74,62 @@ namespace gdjs {
return runtimeScene.getLayer(layer).getCameraHeight();
};
export const getCameraBorderLeft = function (
runtimeScene: gdjs.RuntimeScene,
layer: string,
cameraId: integer
): number {
if (!runtimeScene.hasLayer(layer)) {
return 0;
}
return (
getCameraX(runtimeScene, layer, cameraId) -
getCameraWidth(runtimeScene, layer, cameraId) / 2
);
};
export const getCameraBorderRight = function (
runtimeScene: gdjs.RuntimeScene,
layer: string,
cameraId: integer
): number {
if (!runtimeScene.hasLayer(layer)) {
return 0;
}
return (
getCameraX(runtimeScene, layer, cameraId) +
getCameraWidth(runtimeScene, layer, cameraId) / 2
);
};
export const getCameraBorderTop = function (
runtimeScene: gdjs.RuntimeScene,
layer: string,
cameraId: integer
): number {
if (!runtimeScene.hasLayer(layer)) {
return 0;
}
return (
getCameraY(runtimeScene, layer, cameraId) -
getCameraHeight(runtimeScene, layer, cameraId) / 2
);
};
export const getCameraBorderBottom = function (
runtimeScene: gdjs.RuntimeScene,
layer: string,
cameraId: integer
): number {
if (!runtimeScene.hasLayer(layer)) {
return 0;
}
return (
getCameraY(runtimeScene, layer, cameraId) +
getCameraHeight(runtimeScene, layer, cameraId) / 2
);
};
export const showLayer = function (
runtimeScene: gdjs.RuntimeScene,
layer: string

View File

@@ -761,7 +761,8 @@ namespace gdjs {
}
}
const totalCount = Object.keys(files).length;
const filesToLoad = Object.keys(files);
const totalCount = filesToLoad.length;
if (totalCount === 0) return onComplete(totalCount); // Nothing to load.
let loadedCount: integer = 0;
@@ -775,6 +776,7 @@ namespace gdjs {
if (loadedCount === totalCount) return onComplete(totalCount);
onProgress(loadedCount, totalCount);
loadNextFile();
};
const preloadAudioFile = (
@@ -796,29 +798,30 @@ namespace gdjs {
);
};
for (let file in files) {
if (files.hasOwnProperty(file)) {
const fileData = files[file][0];
if (!fileData.preloadAsSound && !fileData.preloadAsMusic) {
onLoad();
} else if (fileData.preloadAsSound && fileData.preloadAsMusic) {
let loadedOnce = false;
const callback = (_, error) => {
if (!loadedOnce) {
loadedOnce = true;
return;
}
onLoad(_, error);
};
const loadNextFile = () => {
if (!filesToLoad.length) return;
const file = filesToLoad.shift()!;
const fileData = files[file][0];
if (!fileData.preloadAsSound && !fileData.preloadAsMusic) {
onLoad();
} else if (fileData.preloadAsSound && fileData.preloadAsMusic) {
let loadedOnce = false;
const callback = (_, error) => {
if (!loadedOnce) {
loadedOnce = true;
return;
}
onLoad(_, error);
};
preloadAudioFile(file, callback, /* isMusic= */ true);
preloadAudioFile(file, callback, /* isMusic= */ false);
} else if (fileData.preloadAsSound) {
preloadAudioFile(file, onLoad, /* isMusic= */ false);
} else if (fileData.preloadAsMusic)
preloadAudioFile(file, onLoad, /* isMusic= */ true);
}
}
preloadAudioFile(file, callback, /* isMusic= */ true);
preloadAudioFile(file, callback, /* isMusic= */ false);
} else if (fileData.preloadAsSound) {
preloadAudioFile(file, onLoad, /* isMusic= */ false);
} else if (fileData.preloadAsMusic)
preloadAudioFile(file, onLoad, /* isMusic= */ true);
};
loadNextFile();
}
}

File diff suppressed because one or more lines are too long

View File

@@ -2,6 +2,7 @@
<html>
<head>
<meta charset="UTF-8"/>
<link rel="manifest" href="manifest.webmanifest">
<meta name="apple-mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">

View File

@@ -664,7 +664,21 @@ namespace gdjs {
}
const baseUrl = 'https://api.gdevelop-app.com/analytics';
this._playerId = this._makePlayerUuid();
let lastSessionHitTime = Date.now();
/**
* The duration that is already sent to the service
* (in milliseconds).
**/
let sentDuration = 0;
/**
* The duration that is not yet sent to the service to avoid flooding
* (in milliseconds).
**/
let notYetSentDuration = 0;
/**
* The last time when duration has been counted
* either in sendedDuration or notYetSentDuration.
**/
let lastSessionResumeTime = Date.now();
fetch(baseUrl + '/session', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
@@ -715,24 +729,39 @@ namespace gdjs {
return;
}
const now = Date.now();
notYetSentDuration += now - lastSessionResumeTime;
lastSessionResumeTime = now;
// Group repeated calls to sendSessionHit - which could
// happen because of multiple event listeners being fired.
if (Date.now() - lastSessionHitTime < 3 * 1000) {
if (notYetSentDuration < 5 * 1000) {
return;
}
lastSessionHitTime = Date.now();
// The backend use seconds for duration.
// The milliseconds will stay in notYetSentDuration.
const toBeSentDuration = Math.floor(notYetSentDuration / 1000) * 1000;
sentDuration += toBeSentDuration;
notYetSentDuration -= toBeSentDuration;
navigator.sendBeacon(
baseUrl + '/session-hit',
JSON.stringify({
gameId: this._data.properties.projectUuid,
playerId: this._playerId,
sessionId: this._sessionId,
duration: Math.floor(sentDuration / 1000),
})
);
};
if (typeof navigator !== 'undefined' && typeof document !== 'undefined') {
document.addEventListener('visibilitychange', () => {
sendSessionHit();
if (document.visibilityState === 'visible') {
// Skip the duration the game was hidden.
lastSessionResumeTime = Date.now();
} else {
sendSessionHit();
}
});
window.addEventListener(
'pagehide',

114
GDJS/package-lock.json generated
View File

@@ -827,10 +827,11 @@
"dev": true
},
"node_modules/acorn": {
"version": "3.3.0",
"resolved": "https://registry.npmjs.org/acorn/-/acorn-3.3.0.tgz",
"integrity": "sha1-ReN/s56No/JbruP/U2niu18iAXo=",
"version": "8.7.1",
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.7.1.tgz",
"integrity": "sha512-Xx54uLJQZ19lKygFXOWsscKUbsBZW0CPykPhVQdhIeIwrbPmJzqeASDInc8nKBnp/JT6igTs82qPXz069H8I/A==",
"dev": true,
"peer": true,
"bin": {
"acorn": "bin/acorn"
},
@@ -3193,6 +3194,21 @@
"node": ">=0.4.x"
}
},
"node_modules/react": {
"version": "16.14.0",
"resolved": "https://registry.npmjs.org/react/-/react-16.14.0.tgz",
"integrity": "sha512-0X2CImDkJGApiAlcf0ODKIneSwBPhqJawOa5wCtKbu7ZECrmS26NvtSILynQ66cgkT/RJ4LidJOc3bUESwmU8g==",
"dev": true,
"peer": true,
"dependencies": {
"loose-envify": "^1.1.0",
"object-assign": "^4.1.1",
"prop-types": "^15.6.2"
},
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/react-ace": {
"version": "6.6.0",
"resolved": "https://registry.npmjs.org/react-ace/-/react-ace-6.6.0.tgz",
@@ -3233,6 +3249,22 @@
"node": ">=8.10.0"
}
},
"node_modules/react-dom": {
"version": "16.14.0",
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-16.14.0.tgz",
"integrity": "sha512-1gCeQXDLoIqMgqD3IO2Ah9bnf0w9kzhwN5q4FGnHZ67hBm9yePzB5JJAIQCc8x3pFnNlwFq4RidZggNAAkzWWw==",
"dev": true,
"peer": true,
"dependencies": {
"loose-envify": "^1.1.0",
"object-assign": "^4.1.1",
"prop-types": "^15.6.2",
"scheduler": "^0.19.1"
},
"peerDependencies": {
"react": "^16.14.0"
}
},
"node_modules/react-frame-component": {
"version": "4.1.3",
"resolved": "https://registry.npmjs.org/react-frame-component/-/react-frame-component-4.1.3.tgz",
@@ -3422,6 +3454,17 @@
"truncate-utf8-bytes": "^1.0.0"
}
},
"node_modules/scheduler": {
"version": "0.19.1",
"resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.19.1.tgz",
"integrity": "sha512-n/zwRWRYSUj0/3g/otKDRPMh6qv2SYMWNq85IEa8iZyAv8od9zDYpGSnpBEjNgcMNq6Scbu5KfIPxNF72R/2EA==",
"dev": true,
"peer": true,
"dependencies": {
"loose-envify": "^1.1.0",
"object-assign": "^4.1.1"
}
},
"node_modules/semver": {
"version": "5.7.1",
"resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz",
@@ -3932,6 +3975,18 @@
"acorn-globals": "^3.0.0"
}
},
"node_modules/with/node_modules/acorn": {
"version": "3.3.0",
"resolved": "https://registry.npmjs.org/acorn/-/acorn-3.3.0.tgz",
"integrity": "sha512-OLUyIIZ7mF5oaAUT1w0TFqQS81q3saT46x8t7ukpPjMNk+nbs4ZHhs7ToV8EWnLYLepjETXd4XaCE4uxkMeqUw==",
"dev": true,
"bin": {
"acorn": "bin/acorn"
},
"engines": {
"node": ">=0.4.0"
}
},
"node_modules/wordwrap": {
"version": "0.0.2",
"resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-0.0.2.tgz",
@@ -4598,10 +4653,11 @@
"dev": true
},
"acorn": {
"version": "3.3.0",
"resolved": "https://registry.npmjs.org/acorn/-/acorn-3.3.0.tgz",
"integrity": "sha1-ReN/s56No/JbruP/U2niu18iAXo=",
"dev": true
"version": "8.7.1",
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.7.1.tgz",
"integrity": "sha512-Xx54uLJQZ19lKygFXOWsscKUbsBZW0CPykPhVQdhIeIwrbPmJzqeASDInc8nKBnp/JT6igTs82qPXz069H8I/A==",
"dev": true,
"peer": true
},
"acorn-globals": {
"version": "3.1.0",
@@ -6499,6 +6555,18 @@
"integrity": "sha1-sgmEkgO7Jd+CDadW50cAWHhSFiA=",
"dev": true
},
"react": {
"version": "16.14.0",
"resolved": "https://registry.npmjs.org/react/-/react-16.14.0.tgz",
"integrity": "sha512-0X2CImDkJGApiAlcf0ODKIneSwBPhqJawOa5wCtKbu7ZECrmS26NvtSILynQ66cgkT/RJ4LidJOc3bUESwmU8g==",
"dev": true,
"peer": true,
"requires": {
"loose-envify": "^1.1.0",
"object-assign": "^4.1.1",
"prop-types": "^15.6.2"
}
},
"react-ace": {
"version": "6.6.0",
"resolved": "https://registry.npmjs.org/react-ace/-/react-ace-6.6.0.tgz",
@@ -6529,6 +6597,19 @@
"strip-indent": "^3.0.0"
}
},
"react-dom": {
"version": "16.14.0",
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-16.14.0.tgz",
"integrity": "sha512-1gCeQXDLoIqMgqD3IO2Ah9bnf0w9kzhwN5q4FGnHZ67hBm9yePzB5JJAIQCc8x3pFnNlwFq4RidZggNAAkzWWw==",
"dev": true,
"peer": true,
"requires": {
"loose-envify": "^1.1.0",
"object-assign": "^4.1.1",
"prop-types": "^15.6.2",
"scheduler": "^0.19.1"
}
},
"react-frame-component": {
"version": "4.1.3",
"resolved": "https://registry.npmjs.org/react-frame-component/-/react-frame-component-4.1.3.tgz",
@@ -6688,6 +6769,17 @@
"truncate-utf8-bytes": "^1.0.0"
}
},
"scheduler": {
"version": "0.19.1",
"resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.19.1.tgz",
"integrity": "sha512-n/zwRWRYSUj0/3g/otKDRPMh6qv2SYMWNq85IEa8iZyAv8od9zDYpGSnpBEjNgcMNq6Scbu5KfIPxNF72R/2EA==",
"dev": true,
"peer": true,
"requires": {
"loose-envify": "^1.1.0",
"object-assign": "^4.1.1"
}
},
"semver": {
"version": "5.7.1",
"resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz",
@@ -7080,6 +7172,14 @@
"requires": {
"acorn": "^3.1.0",
"acorn-globals": "^3.0.0"
},
"dependencies": {
"acorn": {
"version": "3.3.0",
"resolved": "https://registry.npmjs.org/acorn/-/acorn-3.3.0.tgz",
"integrity": "sha512-OLUyIIZ7mF5oaAUT1w0TFqQS81q3saT46x8t7ukpPjMNk+nbs4ZHhs7ToV8EWnLYLepjETXd4XaCE4uxkMeqUw==",
"dev": true
}
}
},
"wordwrap": {

View File

@@ -75,6 +75,10 @@ module.exports = function (config) {
'../../newIDE/app/resources/GDJS/Runtime/Extensions/LinkedObjects/linkedobjects.js',
'../../newIDE/app/resources/GDJS/Runtime/Extensions/Inventory/inventory.js',
'../../newIDE/app/resources/GDJS/Runtime/Extensions/Inventory/inventorytools.js',
'../../newIDE/app/resources/GDJS/Runtime/Extensions/Physics2Behavior/utils.js',
'../../newIDE/app/resources/GDJS/Runtime/Extensions/Physics2Behavior/box2d.js',
'../../newIDE/app/resources/GDJS/Runtime/Extensions/Physics2Behavior/physics2runtimebehavior.js',
'../../newIDE/app/resources/GDJS/Runtime/Extensions/Physics2Behavior/physics2tools.js',
'../../newIDE/app/resources/GDJS/Runtime/Extensions/Leaderboards/leaderboardstools.js',
'../../newIDE/app/resources/GDJS/Runtime/Extensions/Lighting/lightruntimeobject.js',
'../../newIDE/app/resources/GDJS/Runtime/Extensions/Lighting/lightruntimeobject-pixi-renderer.js',

View File

@@ -2134,6 +2134,14 @@ interface ExpressionCompletionFinder {
//Inherited from ExpressionParser2NodeWorker:
};
interface ExpressionNodeLocationFinder {
ExpressionNode STATIC_GetNodeAtPosition([Ref] ExpressionNode node, unsigned long searchedPosition);
};
interface ExpressionTypeFinder {
[Const, Ref] DOMString STATIC_GetType([Const, Ref] Platform platform, [Const, Ref] ObjectsContainer globalObjectsContainer, [Const, Ref] ObjectsContainer objectsContainer, [Const] DOMString rootType, [Ref] ExpressionNode node);
};
interface ExpressionNode {
void Visit([Ref] ExpressionParser2NodeWorker worker);
};

View File

@@ -30,6 +30,8 @@
#include <GDCore/IDE/Events/EventsRemover.h>
#include <GDCore/IDE/Events/EventsTypesLister.h>
#include <GDCore/IDE/Events/ExpressionCompletionFinder.h>
#include <GDCore/IDE/Events/ExpressionNodeLocationFinder.h>
#include <GDCore/IDE/Events/ExpressionTypeFinder.h>
#include <GDCore/IDE/Events/ExpressionValidator.h>
#include <GDCore/IDE/Events/InstructionSentenceFormatter.h>
#include <GDCore/IDE/Events/InstructionsTypeRenamer.h>
@@ -614,6 +616,8 @@ typedef ExtensionAndMetadata<ExpressionMetadata> ExtensionAndExpressionMetadata;
IsExtensionLifecycleEventsFunction
#define STATIC_GetCompletionDescriptionsFor GetCompletionDescriptionsFor
#define STATIC_GetType GetType
#define STATIC_GetNodeAtPosition GetNodeAtPosition
#define STATIC_ScanProject ScanProject

View File

@@ -0,0 +1,6 @@
// Automatically generated by GDevelop.js/scripts/generate-types.js
declare class gdExpressionNodeLocationFinder {
static getNodeAtPosition(node: gdExpressionNode, searchedPosition: number): gdExpressionNode;
delete(): void;
ptr: number;
};

View File

@@ -0,0 +1,6 @@
// Automatically generated by GDevelop.js/scripts/generate-types.js
declare class gdExpressionTypeFinder {
static getType(platform: gdPlatform, globalObjectsContainer: gdObjectsContainer, objectsContainer: gdObjectsContainer, rootType: string, node: gdExpressionNode): string;
delete(): void;
ptr: number;
};

View File

@@ -156,6 +156,8 @@ declare class libGDevelop {
ExpressionCompletionDescription: Class<gdExpressionCompletionDescription>;
VectorExpressionCompletionDescription: Class<gdVectorExpressionCompletionDescription>;
ExpressionCompletionFinder: Class<gdExpressionCompletionFinder>;
ExpressionNodeLocationFinder: Class<gdExpressionNodeLocationFinder>;
ExpressionTypeFinder: Class<gdExpressionTypeFinder>;
ExpressionNode: Class<gdExpressionNode>;
UniquePtrExpressionNode: Class<gdUniquePtrExpressionNode>;
ExpressionParser2: Class<gdExpressionParser2>;

View File

@@ -11,7 +11,7 @@ export const globalTypes = {
toolbar: {
icon: 'circlehollow',
// See theme names in ThemeRegistry.js
items: ['GDevelop default Dark', 'GDevelop default', 'Dark', 'Nord', 'Solarized Dark', 'One Dark'],
items: ['GDevelop default Dark', 'GDevelop default Light', 'Blue Dark', 'Nord', 'Solarized Dark', 'One Dark'],
showName: true,
},
},

View File

@@ -8861,6 +8861,20 @@
"integrity": "sha512-USUftMYpmuMzeWobskoPfzDi+vkpe0dvcOBRNOscFrGxVp4jomnRxWuVohgqBow2xyIPC0S3gjxV/5079jhmDg==",
"dev": true
},
"node_modules/@storybook/builder-webpack4/node_modules/acorn": {
"version": "8.7.1",
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.7.1.tgz",
"integrity": "sha512-Xx54uLJQZ19lKygFXOWsscKUbsBZW0CPykPhVQdhIeIwrbPmJzqeASDInc8nKBnp/JT6igTs82qPXz069H8I/A==",
"dev": true,
"optional": true,
"peer": true,
"bin": {
"acorn": "bin/acorn"
},
"engines": {
"node": ">=0.4.0"
}
},
"node_modules/@storybook/builder-webpack4/node_modules/babel-loader": {
"version": "8.2.3",
"resolved": "https://registry.npmjs.org/babel-loader/-/babel-loader-8.2.3.tgz",
@@ -11403,6 +11417,20 @@
"integrity": "sha512-USUftMYpmuMzeWobskoPfzDi+vkpe0dvcOBRNOscFrGxVp4jomnRxWuVohgqBow2xyIPC0S3gjxV/5079jhmDg==",
"dev": true
},
"node_modules/@storybook/manager-webpack4/node_modules/acorn": {
"version": "8.7.1",
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.7.1.tgz",
"integrity": "sha512-Xx54uLJQZ19lKygFXOWsscKUbsBZW0CPykPhVQdhIeIwrbPmJzqeASDInc8nKBnp/JT6igTs82qPXz069H8I/A==",
"dev": true,
"optional": true,
"peer": true,
"bin": {
"acorn": "bin/acorn"
},
"engines": {
"node": ">=0.4.0"
}
},
"node_modules/@storybook/manager-webpack4/node_modules/babel-loader": {
"version": "8.2.3",
"resolved": "https://registry.npmjs.org/babel-loader/-/babel-loader-8.2.3.tgz",
@@ -29063,6 +29091,15 @@
"node": ">=6"
}
},
"node_modules/load-json-file/node_modules/type-fest": {
"version": "0.3.1",
"resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.3.1.tgz",
"integrity": "sha512-cUGJnCdr4STbePCgqNFbpVNCepa+kAVohJs1sLhxzdH+gnEoOd8VhbYa7pD3zZYGiURWM2xzEII3fQcRizDkYQ==",
"dev": true,
"engines": {
"node": ">=6"
}
},
"node_modules/load-pkg": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/load-pkg/-/load-pkg-3.0.1.tgz",
@@ -39210,6 +39247,15 @@
"node": ">=8"
}
},
"node_modules/tempy/node_modules/type-fest": {
"version": "0.3.1",
"resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.3.1.tgz",
"integrity": "sha512-cUGJnCdr4STbePCgqNFbpVNCepa+kAVohJs1sLhxzdH+gnEoOd8VhbYa7pD3zZYGiURWM2xzEII3fQcRizDkYQ==",
"dev": true,
"engines": {
"node": ">=6"
}
},
"node_modules/term-size": {
"version": "2.2.1",
"resolved": "https://registry.npmjs.org/term-size/-/term-size-2.2.1.tgz",
@@ -39765,12 +39811,17 @@
}
},
"node_modules/type-fest": {
"version": "0.3.1",
"resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.3.1.tgz",
"integrity": "sha512-cUGJnCdr4STbePCgqNFbpVNCepa+kAVohJs1sLhxzdH+gnEoOd8VhbYa7pD3zZYGiURWM2xzEII3fQcRizDkYQ==",
"version": "0.13.1",
"resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.13.1.tgz",
"integrity": "sha512-34R7HTnG0XIJcBSn5XhDd7nNFPRcXYRZrBB2O2jdKqYODldSzBAqzsWoZYYvduky73toYS/ESqxPvkDf/F0XMg==",
"dev": true,
"optional": true,
"peer": true,
"engines": {
"node": ">=6"
"node": ">=10"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/type-is": {
@@ -39802,9 +39853,9 @@
}
},
"node_modules/typescript": {
"version": "4.1.3",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-4.1.3.tgz",
"integrity": "sha512-B3ZIOf1IKeH2ixgHhj6la6xdwR9QrLC5d1VKeCSY4tvkqhF2eqd9O7txNlS0PO3GrBAFIdr3L1ndNwteUbZLYg==",
"version": "4.7.3",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-4.7.3.tgz",
"integrity": "sha512-WOkT3XYvrpXx4vMMqlD+8R8R37fZkjyLGlxavMc4iB8lrl8L0DeTcHbYgw/v0N/z9wAFsgBhcsF0ruoySS22mA==",
"dev": true,
"bin": {
"tsc": "bin/tsc",
@@ -48773,6 +48824,14 @@
"integrity": "sha512-USUftMYpmuMzeWobskoPfzDi+vkpe0dvcOBRNOscFrGxVp4jomnRxWuVohgqBow2xyIPC0S3gjxV/5079jhmDg==",
"dev": true
},
"acorn": {
"version": "8.7.1",
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.7.1.tgz",
"integrity": "sha512-Xx54uLJQZ19lKygFXOWsscKUbsBZW0CPykPhVQdhIeIwrbPmJzqeASDInc8nKBnp/JT6igTs82qPXz069H8I/A==",
"dev": true,
"optional": true,
"peer": true
},
"babel-loader": {
"version": "8.2.3",
"resolved": "https://registry.npmjs.org/babel-loader/-/babel-loader-8.2.3.tgz",
@@ -50611,6 +50670,14 @@
"integrity": "sha512-USUftMYpmuMzeWobskoPfzDi+vkpe0dvcOBRNOscFrGxVp4jomnRxWuVohgqBow2xyIPC0S3gjxV/5079jhmDg==",
"dev": true
},
"acorn": {
"version": "8.7.1",
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.7.1.tgz",
"integrity": "sha512-Xx54uLJQZ19lKygFXOWsscKUbsBZW0CPykPhVQdhIeIwrbPmJzqeASDInc8nKBnp/JT6igTs82qPXz069H8I/A==",
"dev": true,
"optional": true,
"peer": true
},
"babel-loader": {
"version": "8.2.3",
"resolved": "https://registry.npmjs.org/babel-loader/-/babel-loader-8.2.3.tgz",
@@ -64592,6 +64659,14 @@
"pify": "^4.0.1",
"strip-bom": "^3.0.0",
"type-fest": "^0.3.0"
},
"dependencies": {
"type-fest": {
"version": "0.3.1",
"resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.3.1.tgz",
"integrity": "sha512-cUGJnCdr4STbePCgqNFbpVNCepa+kAVohJs1sLhxzdH+gnEoOd8VhbYa7pD3zZYGiURWM2xzEII3fQcRizDkYQ==",
"dev": true
}
}
},
"load-pkg": {
@@ -72749,6 +72824,14 @@
"temp-dir": "^1.0.0",
"type-fest": "^0.3.1",
"unique-string": "^1.0.0"
},
"dependencies": {
"type-fest": {
"version": "0.3.1",
"resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.3.1.tgz",
"integrity": "sha512-cUGJnCdr4STbePCgqNFbpVNCepa+kAVohJs1sLhxzdH+gnEoOd8VhbYa7pD3zZYGiURWM2xzEII3fQcRizDkYQ==",
"dev": true
}
}
},
"term-size": {
@@ -73184,10 +73267,12 @@
"dev": true
},
"type-fest": {
"version": "0.3.1",
"resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.3.1.tgz",
"integrity": "sha512-cUGJnCdr4STbePCgqNFbpVNCepa+kAVohJs1sLhxzdH+gnEoOd8VhbYa7pD3zZYGiURWM2xzEII3fQcRizDkYQ==",
"dev": true
"version": "0.13.1",
"resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.13.1.tgz",
"integrity": "sha512-34R7HTnG0XIJcBSn5XhDd7nNFPRcXYRZrBB2O2jdKqYODldSzBAqzsWoZYYvduky73toYS/ESqxPvkDf/F0XMg==",
"dev": true,
"optional": true,
"peer": true
},
"type-is": {
"version": "1.6.18",
@@ -73215,9 +73300,9 @@
}
},
"typescript": {
"version": "4.1.3",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-4.1.3.tgz",
"integrity": "sha512-B3ZIOf1IKeH2ixgHhj6la6xdwR9QrLC5d1VKeCSY4tvkqhF2eqd9O7txNlS0PO3GrBAFIdr3L1ndNwteUbZLYg==",
"version": "4.7.3",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-4.7.3.tgz",
"integrity": "sha512-WOkT3XYvrpXx4vMMqlD+8R8R37fZkjyLGlxavMc4iB8lrl8L0DeTcHbYgw/v0N/z9wAFsgBhcsF0ruoySS22mA==",
"dev": true
},
"ua-parser-js": {

View File

@@ -38,6 +38,7 @@ import EmptyMessage from '../UI/EmptyMessage';
import { BoxSearchResults } from '../UI/Search/BoxSearchResults';
const FIXED_HEIGHT = 250;
const FIXED_WIDTH = 300;
const styles = {
previewBackground: {
@@ -46,7 +47,7 @@ const styles = {
justifyContent: 'center',
alignItems: 'center',
padding: 10,
width: 300,
width: FIXED_WIDTH,
height: FIXED_HEIGHT,
},
chip: {
@@ -129,6 +130,8 @@ export const AssetDetails = ({
() => {
(async () => {
try {
// Reinitialise asset to trigger a loader and recalculate all parameters. (for instance zoom)
setAsset(null);
const loadedAsset = await getAsset(assetShortHeader);
setAsset(loadedAsset);
if (loadedAsset.objectType === 'sprite') {
@@ -296,43 +299,44 @@ export const AssetDetails = ({
</Line>
<ResponsiveLineStackLayout noMargin>
<Column>
<div style={styles.previewBackground}>
{asset ? (
<>
{asset.objectType === 'sprite' &&
animationResources &&
direction && (
<AnimationPreview
resourceNames={animationResources.map(
({ name }) => name
)}
getImageResourceSource={(resourceName: string) => {
const resource = assetResources[resourceName];
return resource ? resource.file : '';
}}
isImageResourceSmooth={() => isImageResourceSmooth}
project={project}
timeBetweenFrames={direction.timeBetweenFrames}
isLooping // Always loop in the asset store.
hideCheckeredBackground
hideControls
initialZoom={140 / Math.max(asset.width, asset.height)}
fixedHeight={FIXED_HEIGHT}
/>
)}
{(asset.objectType === 'tiled' ||
asset.objectType === '9patch') && (
{asset ? (
<>
{asset.objectType === 'sprite' &&
animationResources &&
direction && (
<AnimationPreview
resourceNames={animationResources.map(({ name }) => name)}
getImageResourceSource={(resourceName: string) => {
const resource = assetResources[resourceName];
return resource ? resource.file : '';
}}
isImageResourceSmooth={() => isImageResourceSmooth}
project={project}
timeBetweenFrames={direction.timeBetweenFrames}
isLooping // Always loop in the asset store.
hideCheckeredBackground
hideControls
initialZoom={140 / Math.max(asset.width, asset.height)}
fixedHeight={FIXED_HEIGHT}
fixedWidth={FIXED_WIDTH}
/>
)}
{(asset.objectType === 'tiled' ||
asset.objectType === '9patch') && (
<div style={styles.previewBackground}>
<CorsAwareImage
style={styles.previewImage}
src={asset.previewImageUrls[0]}
alt={asset.name}
/>
)}
</>
) : (
</div>
)}
</>
) : (
<div style={styles.previewBackground}>
<PlaceholderLoader />
)}
</div>
</div>
)}
{assetAnimations &&
assetAnimations.length > 1 &&
typeof selectedAnimationName === 'string' && (

View File

@@ -28,7 +28,7 @@ type Props = {|
assetShortHeaders: Array<AssetShortHeader>,
addedAssetIds: Array<string>,
onClose: () => void,
onAssetPackAdded: () => void,
onAssetsAdded: () => void,
project: gdProject,
objectsContainer: gdObjectsContainer,
events: gdEventsList,
@@ -40,7 +40,7 @@ export const AssetPackDialog = ({
assetShortHeaders,
addedAssetIds,
onClose,
onAssetPackAdded,
onAssetsAdded,
project,
objectsContainer,
events,
@@ -56,18 +56,18 @@ export const AssetPackDialog = ({
const resourcesFetcher = useResourceFetcher();
const [
isAssetPackBeingInstalled,
setIsAssetPackBeingInstalled,
areAssetsBeingInstalled,
setAreAssetsBeingInstalled,
] = React.useState<boolean>(false);
const eventsFunctionsExtensionsState = React.useContext(
EventsFunctionsExtensionsContext
);
const onInstallAssetPack = React.useCallback(
const onInstallAssets = React.useCallback(
async (assetShortHeaders: Array<AssetShortHeader>) => {
if (!assetShortHeaders || !assetShortHeaders.length) return;
setIsAssetPackBeingInstalled(true);
setAreAssetsBeingInstalled(true);
try {
const installOutputs = await Promise.all(
assetShortHeaders.map(assetShortHeader =>
@@ -88,14 +88,14 @@ export const AssetPackDialog = ({
await resourcesFetcher.ensureResourcesAreFetched(project);
setIsAssetPackBeingInstalled(false);
onAssetPackAdded();
setAreAssetsBeingInstalled(false);
onAssetsAdded();
} catch (error) {
setIsAssetPackBeingInstalled(false);
console.error('Error while installing the asset pack', error);
setAreAssetsBeingInstalled(false);
console.error('Error while installing the assets', error);
showErrorBox({
message:
'There was an error while installing the asset pack. Verify your internet connection or try again later.',
'There was an error while installing the assets. Verify your internet connection or try again later.',
rawError: error,
errorId: 'install-asset-pack-error',
});
@@ -108,11 +108,11 @@ export const AssetPackDialog = ({
objectsContainer,
events,
onObjectAddedFromAsset,
onAssetPackAdded,
onAssetsAdded,
]
);
const dialogContent = isAssetPackBeingInstalled
const dialogContent = areAssetsBeingInstalled
? {
actionButton: (
<TextButton
@@ -141,15 +141,15 @@ export const AssetPackDialog = ({
key="install-again"
label={<Trans>Install again</Trans>}
primary={false}
onClick={() => onInstallAssetPack(assetShortHeaders)}
onClick={() => onInstallAssets(assetShortHeaders)}
/>
),
onApply: () => onInstallAssetPack(assetShortHeaders),
onApply: () => onInstallAssets(assetShortHeaders),
content: (
<Text>
<Trans>
You already have this asset pack installed, do you want to add the{' '}
{assetShortHeaders.length} assets again?
You already have these {assetShortHeaders.length} assets
installed, do you want to add them again?
</Trans>
</Text>
),
@@ -161,15 +161,14 @@ export const AssetPackDialog = ({
key="continue"
label={<Trans>Continue</Trans>}
primary
onClick={() => onInstallAssetPack(assetShortHeaders)}
onClick={() => onInstallAssets(assetShortHeaders)}
/>
),
onApply: () => onInstallAssetPack(assetShortHeaders),
onApply: () => onInstallAssets(assetShortHeaders),
content: (
<Text>
<Trans>
You're about to add {assetShortHeaders.length} assets from the
asset pack. Continue?
You're about to add {assetShortHeaders.length} assets. Continue?
</Trans>
</Text>
),
@@ -181,24 +180,24 @@ export const AssetPackDialog = ({
key="install-missing"
primary
onClick={() => {
onInstallAssetPack(missingAssetShortHeaders);
onInstallAssets(missingAssetShortHeaders);
}}
buildMenuTemplate={i18n => [
{
label: i18n._(t`Install all the assets`),
click: () => onInstallAssetPack(assetShortHeaders),
click: () => onInstallAssets(assetShortHeaders),
},
]}
/>
),
onApply: () => onInstallAssetPack(missingAssetShortHeaders),
onApply: () => onInstallAssets(missingAssetShortHeaders),
content: (
<Text>
<Trans>
You already have{' '}
{assetShortHeaders.length - missingAssetShortHeaders.length}{' '}
asset(s) from this pack in your scene. Do you want to add the
remaining {missingAssetShortHeaders.length} asset(s)?
asset(s) in your scene. Do you want to add the remaining{' '}
{missingAssetShortHeaders.length} one(s)?
</Trans>
</Text>
),
@@ -210,12 +209,12 @@ export const AssetPackDialog = ({
title={assetPack.name}
open
onRequestClose={() => {
if (!isAssetPackBeingInstalled) onClose();
if (!areAssetsBeingInstalled) onClose();
}}
cannotBeDismissed
actions={[
// Installing a pack is not cancelable, so we hide the button while installing.
!isAssetPackBeingInstalled ? (
// Installing a list of assets is not cancelable, so we hide the button while installing.
!areAssetsBeingInstalled ? (
<TextButton
key="cancel"
label={<Trans>Cancel</Trans>}

View File

@@ -81,16 +81,19 @@ export const ExampleStore = ({ isOpening, onOpen, focusOnMount }: Props) => {
<ResponsiveWindowMeasurer>
{windowWidth => (
<Column expand noMargin useFullHeight>
<SearchBar
value={searchText}
onChange={setSearchText}
onRequestSearch={() => {}}
aspect="add-margins-only-if-modern-theme"
tagsHandler={tagsHandler}
tags={filters && filters.defaultTags}
ref={searchBarRef}
placeholder={t`Search examples`}
/>
<Line>
<Column expand>
<SearchBar
value={searchText}
onChange={setSearchText}
onRequestSearch={() => {}}
tagsHandler={tagsHandler}
tags={filters && filters.defaultTags}
ref={searchBarRef}
placeholder={t`Search examples`}
/>
</Column>
</Line>
<Line
expand
overflow={

View File

@@ -15,6 +15,7 @@ import {
} from '../../Utils/Analytics/EventSender';
import useDismissableTutorialMessage from '../../Hints/useDismissableTutorialMessage';
import { t } from '@lingui/macro';
import { ColumnStackLayout } from '../../UI/Layout';
type Props = {|
isInstalling: boolean,
@@ -89,20 +90,19 @@ export const ExtensionStore = ({
<ResponsiveWindowMeasurer>
{windowWidth => (
<Column expand noMargin useFullHeight>
<SearchBar
value={searchText}
onChange={setSearchText}
onRequestSearch={() => {}}
aspect="add-margins-only-if-modern-theme"
tagsHandler={tagsHandler}
tags={filters && filters.allTags}
placeholder={t`Search extensions`}
/>
{DismissableTutorialMessage && (
<Line>
<Column expand>{DismissableTutorialMessage}</Column>
</Line>
)}
<Line>
<ColumnStackLayout expand>
<SearchBar
value={searchText}
onChange={setSearchText}
onRequestSearch={() => {}}
tagsHandler={tagsHandler}
tags={filters && filters.allTags}
placeholder={t`Search extensions`}
/>
{DismissableTutorialMessage}
</ColumnStackLayout>
</Line>
<ListSearchResults
disableAutoTranslate // Search results text highlighting conflicts with dom handling by browser auto-translations features. Disables auto translation to prevent crashes.
onRetry={fetchExtensionsAndFilters}

View File

@@ -42,18 +42,22 @@ export const ResourceStore = ({ onChoose, resourceKind }: Props) => {
return (
<Column expand noMargin useFullHeight>
<SearchBar
value={searchText}
onChange={setSearchText}
onRequestSearch={() => {}}
aspect="add-margins-only-if-modern-theme"
placeholder={t`Search resources`}
/>
<Line>
<Column expand>
<SearchBar
value={searchText}
onChange={setSearchText}
onRequestSearch={() => {}}
placeholder={t`Search resources`}
/>
</Column>
</Line>
<Line
expand
overflow={
'hidden' /* Somehow required on Chrome/Firefox to avoid children growing (but not on Safari) */
}
noMargin
>
<Background noFullHeight noExpand width={250}>
<ScrollView>

View File

@@ -100,7 +100,6 @@ export const AssetStore = ({
isAssetPackDialogInstallOpen,
setIsAssetPackDialogInstallOpen,
] = React.useState(false);
const [isAssetPackAdded, setIsAssetPackAdded] = React.useState(false);
const [
isAssetBeingInstalled,
setIsAssetBeingInstalled,
@@ -235,7 +234,6 @@ export const AssetStore = ({
navigationState.openHome();
clearAllFilters(assetFiltersState);
setIsFiltersPanelOpen(false);
setIsAssetPackAdded(false);
}}
size="small"
>
@@ -280,7 +278,6 @@ export const AssetStore = ({
if (navigationState.getCurrentPage().isOnHomePage) {
clearAllFilters(assetFiltersState);
setIsFiltersPanelOpen(false);
setIsAssetPackAdded(false);
}
}}
/>
@@ -308,17 +305,13 @@ export const AssetStore = ({
<Column expand alignItems="flex-end" noMargin>
<RaisedButton
primary
label={
isAssetPackAdded ? (
<Trans>Asset pack added</Trans>
) : (
<Trans>Add pack to my scene</Trans>
)
}
label={<Trans>Add all assets to my scene</Trans>}
onClick={() =>
setIsAssetPackDialogInstallOpen(true)
}
disabled={isAssetPackAdded}
disabled={
!searchResults || searchResults.length === 0
}
/>
</Column>
</>
@@ -377,9 +370,9 @@ export const AssetStore = ({
>
<AssetStoreFilterPanel
assetFiltersState={assetFiltersState}
onChoiceChange={() =>
navigationState.openSearchIfNeeded()
}
onChoiceChange={() => {
navigationState.openSearchIfNeeded();
}}
/>
</Line>
</ScrollView>
@@ -439,8 +432,7 @@ export const AssetStore = ({
assetShortHeaders={searchResults}
addedAssetIds={addedAssetIds}
onClose={() => setIsAssetPackDialogInstallOpen(false)}
onAssetPackAdded={() => {
setIsAssetPackAdded(true);
onAssetsAdded={() => {
setIsAssetPackDialogInstallOpen(false);
}}
project={project}

View File

@@ -246,20 +246,26 @@ export default function NewBehaviorDialog({
</Tabs>
{currentTab === 'installed' && (
<React.Fragment>
<SearchBar
value={searchText}
onRequestSearch={() => {
if (behaviors.length) {
chooseBehavior(i18n, behaviors[0]);
} else if (showDeprecated && deprecatedBehaviors.length) {
chooseBehavior(i18n, deprecatedBehaviors[0]);
}
}}
aspect="add-margins-only-if-modern-theme"
onChange={setSearchText}
ref={searchBar}
placeholder={t`Search installed behaviors`}
/>
<Line>
<Column expand>
<SearchBar
value={searchText}
onRequestSearch={() => {
if (behaviors.length) {
chooseBehavior(i18n, behaviors[0]);
} else if (
showDeprecated &&
deprecatedBehaviors.length
) {
chooseBehavior(i18n, deprecatedBehaviors[0]);
}
}}
onChange={setSearchText}
ref={searchBar}
placeholder={t`Search installed behaviors`}
/>
</Column>
</Line>
{hasSearchNoResult && (
<EmptyMessage>
<Trans>

View File

@@ -5,13 +5,13 @@ import {
moveNodeAbove,
moveNodeBelow,
moveNodeAsSubEvent,
isSameDepthAndJustBelow,
isJustBelow,
isSibling,
} from './helpers';
import { type WidthType } from '../../UI/Reponsive/ResponsiveWindowMeasurer';
import './style.css';
import GDevelopThemeContext from '../../UI/Theme/ThemeContext';
import { type DropTargetComponent } from '../../UI/DragAndDrop/DropTarget';
const sharedStyles = {
dropArea: { zIndex: 1, position: 'absolute' },
dropIndicator: {
@@ -42,7 +42,7 @@ type TargetPositionStyles = { [position: string]: DropTargetContainerStyle };
const getTargetPositionStyles = (
indentWidth: number,
draggedNodeHeight: number,
isDraggedNodeChild: boolean
isDraggedNodeSibling: boolean
): TargetPositionStyles => ({
'bottom-left': { left: '0px', bottom: '0px', top: '50%', width: indentWidth },
'bottom-right': {
@@ -54,13 +54,13 @@ const getTargetPositionStyles = (
top: { left: '0px', right: '0px', top: '0px', bottom: '50%' },
bottom: { left: '0px', right: '0px', top: '50%', bottom: '0px' },
'below-left': {
left: isDraggedNodeChild ? '0px' : '10px',
left: isDraggedNodeSibling ? '10px' : '0px',
top: '100%',
height: draggedNodeHeight,
width: indentWidth,
},
'below-right': {
left: isDraggedNodeChild ? `${indentWidth + 10}px` : `${indentWidth}px`,
left: isDraggedNodeSibling ? `${indentWidth}px` : `${indentWidth + 10}px`,
right: '0px',
top: '100%',
height: draggedNodeHeight,
@@ -132,7 +132,7 @@ type DropContainerProps = {|
moveFunction: ({
targetNode: SortableTreeNode,
node: SortableTreeNode,
}) => number,
}) => void,
node: SortableTreeNode
) => void,
activateTargets: boolean,
@@ -142,8 +142,76 @@ type DropContainerProps = {|
// Used only for the node just above dragged node if it is an only child,
// so that drop area covers the whole dragged node's row in height.
draggedNodeHeight: number,
getNodeAtPath: (path: Array<number>) => SortableTreeNode,
|};
type HorizontalDraggedNodeDropContainerProps = {|
node: SortableTreeNode,
draggedNode: SortableTreeNode,
DnDComponent: DropTargetComponent<SortableTreeNode>,
onDrop: (
moveFunction: ({
targetNode: SortableTreeNode,
node: SortableTreeNode,
}) => void,
node: SortableTreeNode
) => void,
activateTargets: boolean,
getNodeAtPath: (path: Array<number>) => SortableTreeNode,
draggedNodeHeight: number,
indentWidth: number,
|};
function HorizontalDraggedNodeDropContainer({
DnDComponent,
onDrop,
activateTargets,
getNodeAtPath,
node,
draggedNode,
indentWidth,
draggedNodeHeight,
}: HorizontalDraggedNodeDropContainerProps) {
const { depth } = node;
return (
<>
{new Array(depth).fill(0).map((_, depthStep) => {
// Skip so that it does not hinder dragging and so that we don't have to
// worry about delaying the drop target activation.
if (depthStep === draggedNode.depth) return null;
return (
<DropTargetContainer
key={depthStep}
DnDComponent={DnDComponent}
onDrop={() =>
onDrop(
moveNodeBelow,
getNodeAtPath(node.nodePath.slice(0, depthStep + 1))
)
}
canDrop={() => true}
style={{
dropArea: {
top: '100%',
bottom: `-${draggedNodeHeight}px`,
left: `-${indentWidth * (depth - depthStep)}px`,
width: indentWidth,
},
dropIndicator: {
left: `-${indentWidth * (depth - depthStep)}px`,
right: '0px',
// The bottom is set so that the indicator is centered between the events.
bottom: '-2px',
},
}}
/>
);
})}
</>
);
}
/**
* DropContainer is composed of sub-containers of drop targets that allows us to identify
* where the mouse or touch is and drop the dragged node accordingly. At most, there will be 5
@@ -158,24 +226,19 @@ export function DropContainer({
activateTargets,
windowWidth,
draggedNodeHeight,
getNodeAtPath,
}: DropContainerProps) {
const isDraggedNodesOnlyChild =
node.children.length === 1 && node.children[0].key === draggedNode.key;
const isDraggedNodeSameDepthAndBelow = isSameDepthAndJustBelow(
node,
draggedNode
);
const isDraggedNodeSibling = isSibling(node, draggedNode);
const isDraggedNodeJustBelow = isJustBelow(node, draggedNode);
// We want to allow dropping below if the event has no children OR if the only
// child of the event is the dragged one.
const canDropBelow =
!!node.event && (node.children.length === 0 || isDraggedNodesOnlyChild);
const canHaveSubEvents = !!node.event && node.event.canHaveSubEvents();
const indentWidth = getIndentWidth(windowWidth);
const dropAreaStyles = getTargetPositionStyles(
indentWidth,
draggedNodeHeight,
isDraggedNodesOnlyChild
isDraggedNodeSibling
);
const indicatorStyles = getIndicatorPositionStyles(indentWidth);
const commonProps = {
@@ -196,7 +259,7 @@ export function DropContainer({
onDrop={() => onDrop(moveNodeAbove, node)}
{...commonProps}
/>
{canHaveSubEvents && canDropBelow && (
{canHaveSubEvents ? (
<>
<DropTargetContainer
style={{
@@ -214,8 +277,8 @@ export function DropContainer({
onDrop={() => onDrop(moveNodeBelow, node)}
{...commonProps}
/>
{/* Allow dragging left/right to move below or as subevent */}
{(isDraggedNodesOnlyChild || isDraggedNodeSameDepthAndBelow) && (
{/* Allow dragging left/right just below the current node. */}
{isDraggedNodeJustBelow && (
<>
<DropTargetContainer
style={{
@@ -236,8 +299,7 @@ export function DropContainer({
</>
)}
</>
)}
{!canHaveSubEvents && canDropBelow && (
) : (
<DropTargetContainer
style={{
dropIndicator: indicatorStyles['bottom'],
@@ -247,14 +309,19 @@ export function DropContainer({
{...commonProps}
/>
)}
{canHaveSubEvents && !canDropBelow && (
<DropTargetContainer
style={{
dropIndicator: indicatorStyles['bottom-right'],
dropArea: dropAreaStyles['bottom'],
}}
onDrop={() => onDrop(moveNodeAsSubEvent, node)}
{...commonProps}
{/* This DropContainer allows dragging horizontally, on any depth between
0 and the depth of the node just above the dragged one (which has information on
its parents). */}
{isDraggedNodeJustBelow && (
<HorizontalDraggedNodeDropContainer
node={node}
draggedNode={draggedNode}
DnDComponent={DnDComponent}
activateTargets={activateTargets}
onDrop={onDrop}
indentWidth={indentWidth}
draggedNodeHeight={draggedNodeHeight}
getNodeAtPath={getNodeAtPath}
/>
)}
</div>

View File

@@ -5,9 +5,6 @@ export type MoveFunctionArguments = {
node: SortableTreeNode,
};
const getRowIndexOfNode = (node: SortableTreeNode) =>
node.nodePath[node.nodePath.length - 1];
export const moveEventToEventsList = ({
targetEventsList,
movingEvent,
@@ -37,11 +34,6 @@ export const moveNodeAsSubEvent = ({
initialEventsList: node.eventsList,
toIndex: 0,
});
const previousRowIndex = getRowIndexOfNode(node);
const targetRowIndex = getRowIndexOfNode(targetNode);
return previousRowIndex <= targetRowIndex
? targetRowIndex
: targetRowIndex + 1;
};
export const moveNodeBelow = ({ targetNode, node }: MoveFunctionArguments) => {
@@ -56,11 +48,6 @@ export const moveNodeBelow = ({ targetNode, node }: MoveFunctionArguments) => {
initialEventsList: node.eventsList,
toIndex,
});
const previousRowIndex = getRowIndexOfNode(node);
const targetRowIndex = getRowIndexOfNode(targetNode);
return previousRowIndex <= targetRowIndex
? targetRowIndex
: targetRowIndex + 1;
};
export const moveNodeAbove = ({ targetNode, node }: MoveFunctionArguments) => {
@@ -75,11 +62,6 @@ export const moveNodeAbove = ({ targetNode, node }: MoveFunctionArguments) => {
initialEventsList: node.eventsList,
toIndex,
});
const previousRowIndex = getRowIndexOfNode(node);
const targetRowIndex = getRowIndexOfNode(targetNode);
return previousRowIndex <= targetRowIndex
? targetRowIndex - 1
: targetRowIndex;
};
export const isDescendant = (
@@ -92,11 +74,19 @@ export const isDescendant = (
return parentPath.every((pathValue, index) => pathValue === childPath[index]);
};
export const isSameDepthAndJustBelow = (
export const isSibling = (nodeA: SortableTreeNode, nodeB: SortableTreeNode) => {
if (nodeA.depth !== nodeB.depth) return false;
const nodeAPath = nodeA.nodePath;
const nodeBPath = nodeB.nodePath;
return nodeAPath
.slice(0, -1)
.every((pathValue, index) => pathValue === nodeBPath[index]);
};
export const isJustBelow = (
aboveNode: SortableTreeNode,
belowNode: SortableTreeNode
) => {
if (aboveNode.depth !== belowNode.depth) return false;
const belowNodePath = belowNode.nodePath;
const aboveNodePath = aboveNode.nodePath;
if (belowNodePath[belowNodePath.length - 1] === 0) return false;
@@ -105,3 +95,14 @@ export const isSameDepthAndJustBelow = (
aboveNodePath[aboveNodePath.length - 1]
);
};
export const getNodeAtPath = (
path: Array<number>,
treeData: Array<SortableTreeNode>
) => {
if (path.length === 1) return treeData[path[0]];
return getNodeAtPath(
path.slice(0, -1),
treeData[path[path.length - 1]].children
);
};

View File

@@ -5,6 +5,7 @@ import findIndex from 'lodash/findIndex';
import {
SortableTreeWithoutDndContext,
getFlatDataFromTree,
getNodeAtPath,
} from 'react-sortable-tree';
import { type ConnectDragSource } from 'react-dnd';
import { mapFor } from '../../Utils/MapFor';
@@ -588,17 +589,23 @@ export default class ThemableEventsTree extends Component<
};
_onDrop = (
moveFunction: MoveFunctionArguments => number,
moveFunction: MoveFunctionArguments => void,
currentNode: SortableTreeNode
) => {
const draggedNode = this.state.draggedNode;
if (draggedNode) {
const nextRowIndex = moveFunction({
moveFunction({
node: draggedNode,
targetNode: currentNode,
});
const { nodePath } = draggedNode;
this.props.onEventMoved(nodePath[nodePath.length - 1], nextRowIndex);
const { nodePath, event } = draggedNode;
this._onEndDrag();
if (!event) {
console.warn('EventsSheet: No event found in dragged node.');
return;
}
const newRowIndex = this.getEventRow(event);
this.props.onEventMoved(nodePath[nodePath.length - 1], newRowIndex);
}
};
@@ -669,9 +676,15 @@ export default class ThemableEventsTree extends Component<
};
_onEndDrag = () => {
this.setState({ draggedNode: null });
this._restoreFoldedNodes();
this.forceEventsUpdate();
// This method is always called at the end of the drag, regardless of whether
// an event was actually dropped. It is also already called in `_onDrop` to update
// the event list and compute history. So if draggedNode is null, we want to avoid
// recomputing the event list.
if (this.state.draggedNode) {
this.setState({ draggedNode: null });
this._restoreFoldedNodes();
this.forceEventsUpdate();
}
};
_renderEvent = ({ node }: { node: SortableTreeNode }) => {
@@ -779,6 +792,13 @@ export default class ThemableEventsTree extends Component<
onDrop={this._onDrop}
activateTargets={!isDragged && !!this.state.draggedNode}
windowWidth={this.props.windowWidth}
getNodeAtPath={path =>
getNodeAtPath({
path,
treeData: this.state.treeData,
getNodeKey,
}).node
}
/>
)}
</div>

View File

@@ -85,9 +85,11 @@ export const getVisibleParameterTypes = (
export const formatExpressionCall = (
expressionInfo: EnumeratedInstructionOrExpressionMetadata,
parameterValues: ParameterValues
parameterValues: ParameterValues,
options: {| shouldConvertToString: boolean |}
): string => {
const functionName = expressionInfo.name || '';
let functionCall = '';
if (expressionInfo.scope.objectMetadata) {
const objectName = parameterValues[0];
@@ -97,7 +99,7 @@ export const formatExpressionCall = (
expressionInfo.metadata,
1
).join(', ');
return `${objectName}.${functionName}(${functionArgs})`;
functionCall = `${objectName}.${functionName}(${functionArgs})`;
} else if (expressionInfo.scope.behaviorMetadata) {
const objectName = parameterValues[0];
const behaviorName = parameterValues[1];
@@ -107,13 +109,16 @@ export const formatExpressionCall = (
expressionInfo.metadata,
2
).join(', ');
return `${objectName}.${behaviorName}::${functionName}(${functionArgs})`;
functionCall = `${objectName}.${behaviorName}::${functionName}(${functionArgs})`;
} else {
const functionArgs = filterVisibleParameters(
parameterValues,
expressionInfo.metadata,
0
).join(', ');
return `${functionName}(${functionArgs})`;
functionCall = `${functionName}(${functionArgs})`;
}
return options.shouldConvertToString
? `ToString(${functionCall})`
: functionCall;
};

View File

@@ -12,71 +12,99 @@ import {
describe('FormatExpressionCall', () => {
it('properly formats a free function, with one or more arguments', () => {
const freeExpressions = enumerateFreeExpressions('number');
const freeExpressions = enumerateFreeExpressions('number|string');
const countExpression = filterExpressions(
freeExpressions,
'PickedInstancesCount'
)[0];
expect(formatExpressionCall(countExpression, ['MyObject'])).toBe(
'PickedInstancesCount(MyObject)'
);
expect(
formatExpressionCall(countExpression, ['MyObject'], {
shouldConvertToString: false,
})
).toBe('PickedInstancesCount(MyObject)');
const atan2Expression = filterExpressions(freeExpressions, 'atan2')[0];
expect(formatExpressionCall(atan2Expression, ['1', '2'])).toBe(
'atan2(1, 2)'
);
expect(
formatExpressionCall(atan2Expression, ['1', '2'], {
shouldConvertToString: false,
})
).toBe('atan2(1, 2)');
});
it('properly formats a free function, with "code-only" parameters', () => {
const freeExpressions = enumerateFreeExpressions('number');
const freeExpressions = enumerateFreeExpressions('number|string');
const cameraHeightExpression = filterExpressions(
freeExpressions,
'CameraHeight'
)[0];
expect(
formatExpressionCall(cameraHeightExpression, ['', '"My layer"', '0'])
formatExpressionCall(cameraHeightExpression, ['', '"My layer"', '0'], {
shouldConvertToString: false,
})
).toBe('CameraHeight("My layer", 0)');
});
it('properly formats a free function, with "code-only" and optional parameters', () => {
const freeExpressions = enumerateFreeExpressions('number');
const freeExpressions = enumerateFreeExpressions('number|string');
const touchExpression = filterExpressions(freeExpressions, 'TouchX')[0];
expect(formatExpressionCall(touchExpression, ['', '1'])).toBe('TouchX(1)');
expect(formatExpressionCall(touchExpression, ['', '1', '"My layer"'])).toBe(
'TouchX(1, "My layer")'
);
expect(formatExpressionCall(touchExpression, ['', '1', '', ''])).toBe(
'TouchX(1)'
);
expect(formatExpressionCall(touchExpression, ['', '1', '', '2'])).toBe(
'TouchX(1, "", 2)'
);
expect(
formatExpressionCall(touchExpression, ['', '1'], {
shouldConvertToString: false,
})
).toBe('TouchX(1)');
expect(
formatExpressionCall(touchExpression, ['', '1', '"My layer"'], {
shouldConvertToString: false,
})
).toBe('TouchX(1, "My layer")');
expect(
formatExpressionCall(touchExpression, ['', '1', '', ''], {
shouldConvertToString: false,
})
).toBe('TouchX(1)');
expect(
formatExpressionCall(touchExpression, ['', '1', '', '2'], {
shouldConvertToString: false,
})
).toBe('TouchX(1, "", 2)');
});
it('properly formats an object function', () => {
const objectsExpressions = enumerateObjectExpressions('number', 'Sprite');
const objectsExpressions = enumerateObjectExpressions(
'number|string',
'Sprite'
);
const variableStringExpression = filterExpressions(
objectsExpressions,
'Variable'
)[0];
expect(variableStringExpression).not.toBeUndefined();
expect(
formatExpressionCall(variableStringExpression, ['MyObject', 'Variable1'])
formatExpressionCall(
variableStringExpression,
['MyObject', 'Variable1'],
{ shouldConvertToString: false }
)
).toBe('MyObject.Variable(Variable1)');
});
it('properly formats an object function with an argument', () => {
const objectsExpressions = enumerateObjectExpressions('number', 'Sprite');
const objectsExpressions = enumerateObjectExpressions(
'number|string',
'Sprite'
);
const pointXExpression = filterExpressions(objectsExpressions, 'PointX')[0];
expect(pointXExpression).not.toBeUndefined();
expect(
formatExpressionCall(pointXExpression, ['MyObject', '"MyPoint"'])
formatExpressionCall(pointXExpression, ['MyObject', '"MyPoint"'], {
shouldConvertToString: false,
})
).toBe('MyObject.PointX("MyPoint")');
});
it('properly formats an object behavior function', () => {
const behaviorsExpressions = enumerateBehaviorExpressions(
'number',
'number|string',
'PlatformBehavior::PlatformerObjectBehavior'
);
const jumpSpeedExpression = filterExpressions(
@@ -85,15 +113,38 @@ describe('FormatExpressionCall', () => {
)[0];
expect(jumpSpeedExpression).not.toBeUndefined();
expect(
formatExpressionCall(jumpSpeedExpression, [
'MyObject',
'PlatformerObject',
])
formatExpressionCall(
jumpSpeedExpression,
['MyObject', 'PlatformerObject'],
{ shouldConvertToString: false }
)
).toBe('MyObject.PlatformerObject::JumpSpeed()');
});
it('properly formats an object behavior function and converts to string', () => {
const behaviorsExpressions = enumerateBehaviorExpressions(
'number|string',
'PlatformBehavior::PlatformerObjectBehavior'
);
const jumpSpeedExpression = filterExpressions(
behaviorsExpressions,
'JumpSpeed'
)[0];
expect(jumpSpeedExpression).not.toBeUndefined();
expect(
formatExpressionCall(
jumpSpeedExpression,
['MyObject', 'PlatformerObject'],
{ shouldConvertToString: true }
)
).toBe('ToString(MyObject.PlatformerObject::JumpSpeed())');
});
it('can return the visible parameters of a function', () => {
const objectsExpressions = enumerateObjectExpressions('number', 'Sprite');
const objectsExpressions = enumerateObjectExpressions(
'number|string',
'Sprite'
);
const pointXExpression = filterExpressions(objectsExpressions, 'PointX')[0];
expect(pointXExpression).not.toBeUndefined();

View File

@@ -268,12 +268,48 @@ export default class ExpressionField extends React.Component<Props, State> {
parameterValues: ParameterValues
) => {
if (!this._inputElement) return;
const {
globalObjectsContainer,
objectsContainer,
expressionType,
value,
} = this.props;
const cursorPosition = this._inputElement.selectionStart;
const parser = new gd.ExpressionParser2();
const functionCall = formatExpressionCall(expressionInfo, parameterValues);
// We want to know what type the expression should be so as to convert to string
// when necessary.
// We add a fake identifier so that getNodeAtPosition will return the type of
// its parent. Particularly, this is needed to get the type of a parameter in
// a function call. We could create a worker ExpectedTypeFinder that would get
// the type wanted by the parent instead.
const expressionNode = parser
.parseExpression(
value.substr(0, cursorPosition) +
'fakeIdentifier' +
value.substr(cursorPosition)
)
.get();
const currentNode = gd.ExpressionNodeLocationFinder.getNodeAtPosition(
expressionNode,
cursorPosition + 'fakeIdentifier'.length - 1
);
const type = gd.ExpressionTypeFinder.getType(
gd.JsPlatform.get(),
globalObjectsContainer,
objectsContainer,
expressionType,
currentNode
);
let shouldConvertToString =
expressionInfo.metadata.getReturnType() === 'number' && type === 'string';
const functionCall = formatExpressionCall(expressionInfo, parameterValues, {
shouldConvertToString,
});
// Generate the expression with the function call
const { value } = this.props;
const newValue =
value.substr(0, cursorPosition) +
functionCall +
@@ -338,6 +374,10 @@ export default class ExpressionField extends React.Component<Props, State> {
addParameterSeparator: expressionAutocompletion.addParameterSeparator,
addNamespaceSeparator: expressionAutocompletion.addNamespaceSeparator,
hasVisibleParameters: expressionAutocompletion.hasVisibleParameters,
shouldConvertToString:
expressionAutocompletion.kind === 'Expression'
? expressionAutocompletion.shouldConvertToString
: null,
}
);

View File

@@ -43,6 +43,7 @@ export type ExpressionAutocompletion =
...BaseExpressionAutocompletion,
kind: 'Expression',
enumeratedExpressionMetadata: EnumeratedExpressionMetadata,
shouldConvertToString: boolean,
|}
| {|
...BaseExpressionAutocompletion,
@@ -88,7 +89,8 @@ const getAutocompletionsForExpressions = (
prefix: string,
replacementStartPosition: number,
replacementEndPosition: number,
isExact: boolean
isExact: boolean,
completionType: string
): Array<ExpressionAutocompletion> => {
return expressionMetadatas
.filter(
@@ -113,6 +115,9 @@ const getAutocompletionsForExpressions = (
hasVisibleParameters:
getVisibleParameterTypes(enumeratedExpressionMetadata).length !== 0,
isExact,
shouldConvertToString:
completionType === 'string' &&
enumeratedExpressionMetadata.metadata.getReturnType() === 'number',
};
})
.filter(Boolean);
@@ -137,7 +142,8 @@ const getAutocompletionsForFreeExpressions = function(
prefix,
completionDescription.getReplacementStartPosition(),
completionDescription.getReplacementEndPosition(),
isExact
isExact,
type
);
};
@@ -172,7 +178,8 @@ const getAutocompletionsForObjectExpressions = function(
prefix,
completionDescription.getReplacementStartPosition(),
completionDescription.getReplacementEndPosition(),
isExact
isExact,
type
);
};
@@ -210,7 +217,8 @@ const getAutocompletionsForBehaviorExpressions = function(
prefix,
completionDescription.getReplacementStartPosition(),
completionDescription.getReplacementEndPosition(),
isExact
isExact,
type
);
};
@@ -513,6 +521,7 @@ type InsertedAutocompletion = {|
addDot?: ?boolean,
addParameterSeparator?: ?boolean,
addNamespaceSeparator?: ?boolean,
shouldConvertToString?: ?boolean,
|};
type ExpressionAndCaretLocation = {|
@@ -520,25 +529,119 @@ type ExpressionAndCaretLocation = {|
caretLocation: number,
|};
/**
* Returns the position where the last expression call (node) starts,
* so that we know where to insert the ToString call.
* For example:
* '"HelloWorld" + Object.Behavior::' should return 15.
*/
const findLastNodeStartPosition = (expression: string) => {
let match;
let indexes = [];
// We consider that expressions are composed of letters, digits, dot or colons
const expressionSeparatorRegex = new RegExp(/[^\w\d.:]/g);
while ((match = expressionSeparatorRegex.exec(expression))) {
indexes.push(match.index);
}
return Math.max(...indexes);
};
const insertWordInExpressionWithToString = ({
expression,
wordStartPosition,
wordEndPosition,
insertedWord,
}: {
expression: string,
wordStartPosition: number,
wordEndPosition: number,
insertedWord: string,
}) => {
const expressionStart = expression.substring(0, wordStartPosition);
// If the grammar is becoming more complex, you'll need to implement a
// NodeParenthesesBoundsFinder worker to specify the proper bounds for each node
// or give this responsibility to `ExpressionCompletionDescription`/`ExpressionCompletionFinder`.
const completedNodeStartPosition: number = findLastNodeStartPosition(
expressionStart
);
const newExpressionStart = expression.substring(
0,
completedNodeStartPosition + 1
);
const completedNodeStart = expression.substring(
completedNodeStartPosition + 1,
wordStartPosition
);
const newExpressionEnd = expression.substring(wordEndPosition);
return {
expression:
newExpressionStart +
'ToString(' +
completedNodeStart +
insertedWord +
')' +
newExpressionEnd,
// We place the caret before the closing parenthesis of ToString()
// so that the user's typing flow is not interrupted.
caretLocation:
newExpressionStart.length +
completedNodeStart.length +
insertedWord.length +
'ToString('.length,
};
};
const insertWordInExpression = ({
expression,
wordStartPosition,
wordEndPosition,
insertedWord,
}: {
expression: string,
wordStartPosition: number,
wordEndPosition: number,
insertedWord: string,
}) => {
const newExpressionStart = expression.substring(0, wordStartPosition);
const newExpressionEnd = expression.substring(wordEndPosition);
return {
expression: newExpressionStart + insertedWord + newExpressionEnd,
caretLocation: newExpressionStart.length + insertedWord.length,
};
};
export const insertAutocompletionInExpression = (
{ expression, caretLocation }: ExpressionAndCaretLocation,
insertedAutocompletion: InsertedAutocompletion
): ExpressionAndCaretLocation => {
const {
addDot,
addParameterSeparator,
addNamespaceSeparator,
addParenthesis,
completion,
shouldConvertToString,
replacementStartPosition,
replacementEndPosition,
hasVisibleParameters,
} = insertedAutocompletion;
const formatCompletion = (nextCharacter: ?string) => {
const suffix = insertedAutocompletion.addDot
const suffix = addDot
? '.'
: insertedAutocompletion.addParameterSeparator
: addParameterSeparator
? ', '
: insertedAutocompletion.addNamespaceSeparator
: addNamespaceSeparator
? '::'
: insertedAutocompletion.addParenthesis
: addParenthesis
? '()'
: '';
const addSuffix =
!nextCharacter || !suffix || nextCharacter[0] !== suffix[0];
return insertedAutocompletion.completion + (addSuffix ? suffix : '');
return completion + (addSuffix ? suffix : '');
};
if (caretLocation > expression.length) {
@@ -549,35 +652,42 @@ export const insertAutocompletionInExpression = (
const newExpression = formatCompletion(undefined) + expression;
return {
caretLocation: newExpression.length,
expression: newExpression,
expression: shouldConvertToString
? `ToString(${newExpression})`
: newExpression,
};
}
const wordStartPosition: number = insertedAutocompletion.replacementStartPosition
? insertedAutocompletion.replacementStartPosition
const wordStartPosition: number = replacementStartPosition
? replacementStartPosition
: 0;
const wordEndPosition: number = insertedAutocompletion.replacementEndPosition
? insertedAutocompletion.replacementEndPosition
const wordEndPosition: number = replacementEndPosition
? replacementEndPosition
: expression.length;
// The next character, if any, will be useful to format the completion
// (to avoid repeating an existing character).
const maybeNextCharacter: ?string = expression[wordEndPosition];
const newExpressionStart = expression.substring(0, wordStartPosition);
const insertedWord = formatCompletion(maybeNextCharacter);
const newExpressionEnd = expression.substring(wordEndPosition);
const newExpression = newExpressionStart + insertedWord + newExpressionEnd;
let newCaretLocation = newExpressionStart.length + insertedWord.length;
if (
insertedAutocompletion.addParenthesis &&
insertedAutocompletion.hasVisibleParameters
) {
newCaretLocation = newCaretLocation - 1;
const newAutocompletedExpression = shouldConvertToString
? insertWordInExpressionWithToString({
expression,
insertedWord,
wordEndPosition,
wordStartPosition,
})
: insertWordInExpression({
expression,
insertedWord,
wordEndPosition,
wordStartPosition,
});
if (addParenthesis && hasVisibleParameters) {
newAutocompletedExpression.caretLocation =
newAutocompletedExpression.caretLocation - 1;
}
return {
caretLocation: newCaretLocation,
expression: newExpression,
};
return newAutocompletedExpression;
};

View File

@@ -0,0 +1,249 @@
// @flow
import { Trans, t } from '@lingui/macro';
import { I18n } from '@lingui/react';
import * as React from 'react';
import { formatISO } from 'date-fns';
import { Line, Spacer } from '../UI/Grid';
import { type Game } from '../Utils/GDevelopServices/Game';
import { ColumnStackLayout } from '../UI/Layout';
import Text from '../UI/Text';
import {
type GameMetrics,
getGameMetrics,
} from '../Utils/GDevelopServices/Analytics';
import AuthenticatedUserContext from '../Profile/AuthenticatedUserContext';
import PlaceholderError from '../UI/PlaceholderError';
import SelectField from '../UI/SelectField';
import SelectOption from '../UI/SelectOption';
import { CircularProgress } from '@material-ui/core';
import { Table, TableBody, TableRow, TableRowColumn } from '../UI/Table';
import AlertMessage from '../UI/AlertMessage';
import subDays from 'date-fns/subDays';
import { type PublicGame } from '../Utils/GDevelopServices/Game';
const styles = {
tableRowStatColumn: {
width: 100,
},
};
type Props = {|
game: Game,
publicGame: ?PublicGame,
|};
export const GameAnalyticsPanel = ({ game, publicGame }: Props) => {
const { getAuthorizationHeader, profile } = React.useContext(
AuthenticatedUserContext
);
const [gameRollingMetrics, setGameMetrics] = React.useState<?GameMetrics>(
null
);
const [gameRollingMetricsError, setGameMetricsError] = React.useState<?Error>(
null
);
const [isGameMetricsLoading, setIsGameMetricsLoading] = React.useState(false);
const yesterdayIsoDate = formatISO(subDays(new Date(), 1), {
representation: 'date',
});
const [analyticsDate, setAnalyticsDate] = React.useState(yesterdayIsoDate);
const loadGameMetrics = React.useCallback(
async () => {
if (!profile) return;
const { id } = profile;
setIsGameMetricsLoading(true);
setGameMetricsError(null);
try {
const gameRollingMetrics = await getGameMetrics(
getAuthorizationHeader,
id,
game.id,
analyticsDate
);
setGameMetrics(gameRollingMetrics);
} catch (err) {
console.error(`Unable to load game rolling metrics:`, err);
setGameMetricsError(err);
}
setIsGameMetricsLoading(false);
},
[getAuthorizationHeader, profile, game, analyticsDate]
);
React.useEffect(
() => {
loadGameMetrics();
},
[loadGameMetrics]
);
return (
<I18n>
{({ i18n }) =>
gameRollingMetricsError ? (
<PlaceholderError
onRetry={() => {
loadGameMetrics();
}}
>
<Trans>There was an issue getting the game analytics.</Trans>{' '}
<Trans>Verify your internet connection or try again later.</Trans>
</PlaceholderError>
) : (
<ColumnStackLayout expand>
<Line noMargin alignItems="center">
<Text size="title">
<Trans>Consolidated metrics</Trans>
</Text>
<Spacer />
{!publicGame && <CircularProgress size={20} />}
</Line>
<Table>
<TableBody>
<TableRow>
<TableRowColumn>
<Trans>Last week sessions count</Trans>
</TableRowColumn>
<TableRowColumn style={styles.tableRowStatColumn}>
{publicGame && publicGame.cachedLastWeekSessionsCount
? publicGame.cachedLastWeekSessionsCount
: '-'}
</TableRowColumn>
</TableRow>
<TableRow>
<TableRowColumn>
<Trans>Last year sessions count</Trans>
</TableRowColumn>
<TableRowColumn style={styles.tableRowStatColumn}>
{publicGame && publicGame.cachedLastYearSessionsCount
? publicGame.cachedLastYearSessionsCount
: '-'}
</TableRowColumn>
</TableRow>
</TableBody>
</Table>
<Line noMargin alignItems="center">
<Text size="title">
<Trans>Daily metrics</Trans>
</Text>
<Spacer />
{isGameMetricsLoading && <CircularProgress size={20} />}
</Line>
<Line noMargin>
<SelectField
fullWidth
floatingLabelText={<Trans>Day</Trans>}
value={analyticsDate}
onChange={(_, _index, newIsoDate) => {
setAnalyticsDate(newIsoDate);
}}
>
{Array(5)
.fill('')
.map((_, index) => {
const isoDate = formatISO(subDays(new Date(), index + 2), {
representation: 'date',
});
return (
<SelectOption
key={isoDate}
value={isoDate}
primaryText={isoDate}
/>
);
})
.reverse()}
<SelectOption
value={yesterdayIsoDate}
primaryText={t`Yesterday`}
/>
<SelectOption
value={formatISO(new Date(), {
representation: 'date',
})}
primaryText={t`Today (so far, in real time)`}
/>
</SelectField>
</Line>
{!isGameMetricsLoading && !gameRollingMetrics ? (
<AlertMessage kind="warning">
<Trans>
There were no players or stored metrics for this day. Be sure
to publish your game and get players to try it to see the
collected anonymous analytics.
</Trans>
</AlertMessage>
) : null}
<Table>
<TableBody>
<TableRow>
<TableRowColumn>
<Trans>Players count</Trans>
</TableRowColumn>
<TableRowColumn style={styles.tableRowStatColumn}>
{gameRollingMetrics && gameRollingMetrics.players
? gameRollingMetrics.players.d0Players
: '-'}
</TableRowColumn>
</TableRow>
<TableRow>
<TableRowColumn>
<Trans>Sessions count</Trans>
</TableRowColumn>
<TableRowColumn style={styles.tableRowStatColumn}>
{gameRollingMetrics && gameRollingMetrics.sessions
? gameRollingMetrics.sessions.d0Sessions
: '-'}
</TableRowColumn>
</TableRow>
<TableRow>
<TableRowColumn>
<Trans>New players count</Trans>
</TableRowColumn>
<TableRowColumn style={styles.tableRowStatColumn}>
{gameRollingMetrics && gameRollingMetrics.players
? gameRollingMetrics.players.d0NewPlayers
: '-'}
</TableRowColumn>
</TableRow>
</TableBody>
</Table>
{gameRollingMetrics &&
(!gameRollingMetrics.retention || !gameRollingMetrics.players) ? (
<AlertMessage kind="info">
Upgrade your account with a subscription to unlock all the
metrics for your game.
</AlertMessage>
) : null}
<Table>
<TableBody>
{[1, 2, 3, 4, 5, 6, 7].map(dayIndex => (
<TableRow key={dayIndex}>
<TableRowColumn>
<Trans>Day {dayIndex} retained players</Trans>
</TableRowColumn>
<TableRowColumn style={styles.tableRowStatColumn}>
{gameRollingMetrics &&
gameRollingMetrics.retention &&
gameRollingMetrics.retention[
`d${dayIndex}RetainedPlayers`
] != null
? gameRollingMetrics.retention[
`d${dayIndex}RetainedPlayers`
]
: '-'}
</TableRowColumn>
</TableRow>
))}
</TableBody>
</Table>
</ColumnStackLayout>
)
}
</I18n>
);
};

View File

@@ -3,7 +3,6 @@ import { Trans, t } from '@lingui/macro';
import { I18n } from '@lingui/react';
import { type I18n as I18nType } from '@lingui/core';
import * as React from 'react';
import { formatISO } from 'date-fns';
import FlatButton from '../UI/FlatButton';
import { Line, Spacer } from '../UI/Grid';
import {
@@ -20,19 +19,13 @@ import Dialog from '../UI/Dialog';
import { Tab, Tabs } from '../UI/Tabs';
import { ColumnStackLayout } from '../UI/Layout';
import Text from '../UI/Text';
import {
type GameMetrics,
getGameMetrics,
} from '../Utils/GDevelopServices/Analytics';
import AuthenticatedUserContext from '../Profile/AuthenticatedUserContext';
import PlaceholderError from '../UI/PlaceholderError';
import SelectField from '../UI/SelectField';
import SelectOption from '../UI/SelectOption';
import { Chip, CircularProgress } from '@material-ui/core';
import { Table, TableBody, TableRow, TableRowColumn } from '../UI/Table';
import { Chip } from '@material-ui/core';
import Builds from '../Export/Builds';
import AlertMessage from '../UI/AlertMessage';
import subDays from 'date-fns/subDays';
import RaisedButton from '../UI/RaisedButton';
import Window from '../Utils/Window';
import HelpButton from '../UI/HelpButton';
@@ -49,12 +42,7 @@ import SmartphoneIcon from '@material-ui/icons/Smartphone';
import Crown from '../UI/CustomSvgIcons/Crown';
import { showErrorBox, showWarningBox } from '../UI/Messages/MessageBox';
import LeaderboardAdmin from './LeaderboardAdmin';
const styles = {
tableRowStatColumn: {
width: 100,
},
};
import { GameAnalyticsPanel } from './GameAnalyticsPanel';
export type GamesDetailsTab =
| 'details'
@@ -83,25 +71,13 @@ export const GameDetailsDialog = ({
AuthenticatedUserContext
);
const [currentTab, setCurrentTab] = React.useState(initialTab);
const [gameRollingMetrics, setGameMetrics] = React.useState<?GameMetrics>(
null
);
const [isLoading, setIsLoading] = React.useState<boolean>(false);
const [
gameUnregisterErrorText,
setGameUnregisterErrorText,
] = React.useState<?string>(null);
const [gameRollingMetricsError, setGameMetricsError] = React.useState<?Error>(
null
);
const [isGameMetricsLoading, setIsGameMetricsLoading] = React.useState(false);
const [isGameUpdating, setIsGameUpdating] = React.useState(false);
const yesterdayIsoDate = formatISO(subDays(new Date(), 1), {
representation: 'date',
});
const [analyticsDate, setAnalyticsDate] = React.useState(yesterdayIsoDate);
const authenticatedUser = React.useContext(AuthenticatedUserContext);
const [publicGame, setPublicGame] = React.useState<?PublicGame>(null);
const [publicGameError, setPublicGameError] = React.useState<?Error>(null);
@@ -110,38 +86,6 @@ export const GameDetailsDialog = ({
setIsPublicGamePropertiesDialogOpen,
] = React.useState(false);
const loadGameMetrics = React.useCallback(
async () => {
if (!profile) return;
const { id } = profile;
setIsGameMetricsLoading(true);
setGameMetricsError(null);
try {
const gameRollingMetrics = await getGameMetrics(
getAuthorizationHeader,
id,
game.id,
analyticsDate
);
setGameMetrics(gameRollingMetrics);
} catch (err) {
console.error(`Unable to load game rolling metrics:`, err);
setGameMetricsError(err);
}
setIsGameMetricsLoading(false);
},
[getAuthorizationHeader, profile, game, analyticsDate]
);
React.useEffect(
() => {
loadGameMetrics();
},
[loadGameMetrics]
);
const loadPublicGame = React.useCallback(
async () => {
setPublicGameError(null);
@@ -594,171 +538,7 @@ export const GameDetailsDialog = ({
/>
) : null}
{currentTab === 'analytics' ? (
gameRollingMetricsError ? (
<PlaceholderError
onRetry={() => {
loadGameMetrics();
}}
>
<Trans>There was an issue getting the game analytics.</Trans>{' '}
<Trans>
Verify your internet connection or try again later.
</Trans>
</PlaceholderError>
) : (
<ColumnStackLayout expand>
<Line noMargin alignItems="center">
<Text size="title">
<Trans>Consolidated metrics</Trans>
</Text>
<Spacer />
{!publicGame && <CircularProgress size={20} />}
</Line>
<Table>
<TableBody>
<TableRow>
<TableRowColumn>
<Trans>Last week sessions count</Trans>
</TableRowColumn>
<TableRowColumn style={styles.tableRowStatColumn}>
{publicGame && publicGame.cachedLastWeekSessionsCount
? publicGame.cachedLastWeekSessionsCount
: '-'}
</TableRowColumn>
</TableRow>
<TableRow>
<TableRowColumn>
<Trans>Last year sessions count</Trans>
</TableRowColumn>
<TableRowColumn style={styles.tableRowStatColumn}>
{publicGame && publicGame.cachedLastYearSessionsCount
? publicGame.cachedLastYearSessionsCount
: '-'}
</TableRowColumn>
</TableRow>
</TableBody>
</Table>
<Line noMargin alignItems="center">
<Text size="title">
<Trans>Daily metrics</Trans>
</Text>
<Spacer />
{isGameMetricsLoading && <CircularProgress size={20} />}
</Line>
<Line noMargin>
<SelectField
fullWidth
floatingLabelText={<Trans>Day</Trans>}
value={analyticsDate}
onChange={(_, _index, newIsoDate) => {
setAnalyticsDate(newIsoDate);
}}
>
{Array(5)
.fill('')
.map((_, index) => {
const isoDate = formatISO(
subDays(new Date(), index + 2),
{
representation: 'date',
}
);
return (
<SelectOption
key={isoDate}
value={isoDate}
primaryText={isoDate}
/>
);
})
.reverse()}
<SelectOption
value={yesterdayIsoDate}
primaryText={t`Yesterday`}
/>
<SelectOption
value={formatISO(new Date(), {
representation: 'date',
})}
primaryText={t`Today (so far, in real time)`}
/>
</SelectField>
</Line>
{!isGameMetricsLoading && !gameRollingMetrics ? (
<AlertMessage kind="warning">
<Trans>
There were no players or stored metrics for this day. Be
sure to publish your game and get players to try it to
see the collected anonymous analytics.
</Trans>
</AlertMessage>
) : null}
<Table>
<TableBody>
<TableRow>
<TableRowColumn>
<Trans>Players count</Trans>
</TableRowColumn>
<TableRowColumn style={styles.tableRowStatColumn}>
{gameRollingMetrics && gameRollingMetrics.players
? gameRollingMetrics.players.d0Players
: '-'}
</TableRowColumn>
</TableRow>
<TableRow>
<TableRowColumn>
<Trans>Sessions count</Trans>
</TableRowColumn>
<TableRowColumn style={styles.tableRowStatColumn}>
{gameRollingMetrics && gameRollingMetrics.sessions
? gameRollingMetrics.sessions.d0Sessions
: '-'}
</TableRowColumn>
</TableRow>
<TableRow>
<TableRowColumn>
<Trans>New players count</Trans>
</TableRowColumn>
<TableRowColumn style={styles.tableRowStatColumn}>
{gameRollingMetrics && gameRollingMetrics.players
? gameRollingMetrics.players.d0NewPlayers
: '-'}
</TableRowColumn>
</TableRow>
</TableBody>
</Table>
{gameRollingMetrics &&
(!gameRollingMetrics.retention ||
!gameRollingMetrics.players) ? (
<AlertMessage kind="info">
Upgrade your account with a subscription to unlock all the
metrics for your game.
</AlertMessage>
) : null}
<Table>
<TableBody>
{[1, 2, 3, 4, 5, 6, 7].map(dayIndex => (
<TableRow key={dayIndex}>
<TableRowColumn>
<Trans>Day {dayIndex} retained players</Trans>
</TableRowColumn>
<TableRowColumn style={styles.tableRowStatColumn}>
{gameRollingMetrics &&
gameRollingMetrics.retention &&
gameRollingMetrics.retention[
`d${dayIndex}RetainedPlayers`
] != null
? gameRollingMetrics.retention[
`d${dayIndex}RetainedPlayers`
]
: '-'}
</TableRowColumn>
</TableRow>
))}
</TableBody>
</Table>
</ColumnStackLayout>
)
<GameAnalyticsPanel game={game} publicGame={publicGame} />
) : null}
</Line>
{publicGame && project && isPublicGamePropertiesDialogOpen && (

View File

@@ -40,13 +40,16 @@ export const GamesShowcase = (props: Props) => {
<ResponsiveWindowMeasurer>
{windowWidth => (
<Column expand noMargin useFullHeight>
<SearchBar
value={searchText}
onChange={setSearchText}
onRequestSearch={() => {}}
aspect="add-margins-only-if-modern-theme"
placeholder={t`Search games`}
/>
<Line>
<Column expand>
<SearchBar
value={searchText}
onChange={setSearchText}
onRequestSearch={() => {}}
placeholder={t`Search games`}
/>
</Column>
</Line>
<Line
expand
overflow={

View File

@@ -30,9 +30,9 @@ const TutorialMessage = ({ tutorial }: Props) => {
<img
alt=""
style={{
maxWidth: 128,
maxHeight: 128,
width: 128,
borderRadius: 4,
aspectRatio: '16 / 9',
}}
src={tutorial.thumbnailUrl}
/>

View File

@@ -10,14 +10,7 @@ const gd: libGDevelop = global.gd;
const GROUP_DELIMITER = '/';
// Support enumerate expressions that can be string, number or both ("number|string").
// "number|string" has limited support in the codebase, enough to be used in expressions but not
// in parameters and this is not a generic feature (you can't do "string|variable" or this kind of things).
// This may be improved in the future if really necessary.
const isPotentiallyStringType = (type: string) =>
type === 'string' || type === 'number|string';
const isPotentiallyNumberType = (type: string) =>
type === 'number' || type === 'number|string';
const shouldOnlyBeNumberType = (type: string) => type === 'number';
const enumerateExpressionMetadataMap = (
prefix: string,
@@ -70,8 +63,6 @@ export const enumerateFreeExpressions = (
const allExtensions = gd
.asPlatform(gd.JsPlatform.get())
.getAllPlatformExtensions();
const returnNumber = isPotentiallyNumberType(type);
const returnString = isPotentiallyStringType(type);
return flatten(
mapVector(allExtensions, extension => {
@@ -83,20 +74,18 @@ export const enumerateFreeExpressions = (
};
return [
...(returnString
...(!shouldOnlyBeNumberType(type)
? enumerateExpressionMetadataMap(
prefix,
extension.getAllStrExpressions(),
scope
)
: []),
...(returnNumber
? enumerateExpressionMetadataMap(
prefix,
extension.getAllExpressions(),
scope
)
: []),
...enumerateExpressionMetadataMap(
prefix,
extension.getAllExpressions(),
scope
),
];
})
);
@@ -116,20 +105,18 @@ export const enumerateObjectExpressions = (
const scope = { extension, objectMetadata };
let objectsExpressions = [
...(isPotentiallyStringType(type)
...(!shouldOnlyBeNumberType(type)
? enumerateExpressionMetadataMap(
'',
extension.getAllStrExpressionsForObject(objectType),
scope
)
: []),
...(isPotentiallyNumberType(type)
? enumerateExpressionMetadataMap(
'',
extension.getAllExpressionsForObject(objectType),
scope
)
: []),
...enumerateExpressionMetadataMap(
'',
extension.getAllExpressionsForObject(objectType),
scope
),
];
const baseObjectType = ''; /* An empty string means the base object */
@@ -142,20 +129,18 @@ export const enumerateObjectExpressions = (
objectsExpressions = [
...objectsExpressions,
...(isPotentiallyStringType(type)
...(!shouldOnlyBeNumberType(type)
? enumerateExpressionMetadataMap(
'',
extension.getAllStrExpressionsForObject(baseObjectType),
scope
)
: []),
...(isPotentiallyNumberType(type)
? enumerateExpressionMetadataMap(
'',
extension.getAllExpressionsForObject(baseObjectType),
scope
)
: []),
...enumerateExpressionMetadataMap(
'',
extension.getAllExpressionsForObject(baseObjectType),
scope
),
];
}
@@ -176,20 +161,18 @@ export const enumerateBehaviorExpressions = (
const scope = { extension, behaviorMetadata };
return [
...(isPotentiallyStringType(type)
...(!shouldOnlyBeNumberType(type)
? enumerateExpressionMetadataMap(
'',
extension.getAllStrExpressionsForBehavior(behaviorType),
scope
)
: []),
...(isPotentiallyNumberType(type)
? enumerateExpressionMetadataMap(
'',
extension.getAllExpressionsForBehavior(behaviorType),
scope
)
: []),
...enumerateExpressionMetadataMap(
'',
extension.getAllExpressionsForBehavior(behaviorType),
scope
),
];
};
@@ -197,9 +180,6 @@ export const enumerateBehaviorExpressions = (
export const enumerateAllExpressions = (
type: string
): Array<EnumeratedExpressionMetadata> => {
const returnNumber = isPotentiallyNumberType(type);
const returnString = isPotentiallyStringType(type);
const objectsExpressions = [];
const behaviorsExpressions = [];
const freeExpressions = enumerateFreeExpressions(type);
@@ -215,7 +195,7 @@ export const enumerateAllExpressions = (
const objectMetadata = extension.getObjectMetadata(objectType);
const scope = { extension, objectMetadata };
if (returnString)
if (!shouldOnlyBeNumberType(type))
objectsExpressions.push.apply(
objectsExpressions,
enumerateExpressionMetadataMap(
@@ -224,15 +204,14 @@ export const enumerateAllExpressions = (
scope
)
);
if (returnNumber)
objectsExpressions.push.apply(
objectsExpressions,
enumerateExpressionMetadataMap(
prefix,
extension.getAllExpressionsForObject(objectType),
scope
)
);
objectsExpressions.push.apply(
objectsExpressions,
enumerateExpressionMetadataMap(
prefix,
extension.getAllExpressionsForObject(objectType),
scope
)
);
});
//Behaviors expressions:
@@ -240,7 +219,7 @@ export const enumerateAllExpressions = (
const behaviorMetadata = extension.getBehaviorMetadata(behaviorType);
const scope = { extension, behaviorMetadata };
if (returnString)
if (!shouldOnlyBeNumberType(type))
behaviorsExpressions.push.apply(
behaviorsExpressions,
enumerateExpressionMetadataMap(
@@ -249,15 +228,14 @@ export const enumerateAllExpressions = (
scope
)
);
if (returnNumber)
behaviorsExpressions.push.apply(
behaviorsExpressions,
enumerateExpressionMetadataMap(
prefix,
extension.getAllExpressionsForBehavior(behaviorType),
scope
)
);
behaviorsExpressions.push.apply(
behaviorsExpressions,
enumerateExpressionMetadataMap(
prefix,
extension.getAllExpressionsForBehavior(behaviorType),
scope
)
);
});
});

View File

@@ -12,7 +12,7 @@ import { type EnumeratedExpressionMetadata } from './EnumeratedInstructionOrExpr
const gd: libGDevelop = global.gd;
describe('EnumerateExpressions', () => {
it('can enumerate and filter free expressions', () => {
it('can enumerate and filter free expressions (number only)', () => {
const freeExpressions = enumerateFreeExpressions('number');
// Should find atan, atan2, atanh math function
@@ -24,8 +24,9 @@ describe('EnumerateExpressions', () => {
expect(filterExpressions(freeExpressions, 'MouseX')).toHaveLength(1);
expect(filterExpressions(freeExpressions, 'MouseY')).toHaveLength(1);
});
it('can enumerate and filter free expressions (type "number|string")', () => {
const freeExpressions = enumerateFreeExpressions('number|string');
it('can enumerate and filter free expressions', () => {
const freeExpressions = enumerateFreeExpressions('string');
// Should find ToString and LargeNumberToString:
expect(filterExpressions(freeExpressions, 'ToString')).toHaveLength(2);
@@ -40,7 +41,7 @@ describe('EnumerateExpressions', () => {
expect(filterExpressions(freeExpressions, 'MouseY')).toHaveLength(1);
});
it('can enumerate and filter object expressions', () => {
it('can enumerate and filter object expressions (number only)', () => {
const spriteObjectExpressions = enumerateObjectExpressions(
'number',
'Sprite'
@@ -58,9 +59,9 @@ describe('EnumerateExpressions', () => {
);
});
it('can enumerate and filter object expressions (type "number|string")', () => {
it('can enumerate object expressions', () => {
const spriteObjectExpressions = enumerateObjectExpressions(
'number|string',
'string',
'Sprite'
);
expect(filterExpressions(spriteObjectExpressions, 'PointX')).toHaveLength(
@@ -70,7 +71,7 @@ describe('EnumerateExpressions', () => {
filterExpressions(spriteObjectExpressions, 'AnimationName')
).toHaveLength(1);
const objectExpressions = enumerateObjectExpressions('number|string', '');
const objectExpressions = enumerateObjectExpressions('string', '');
expect(filterExpressions(objectExpressions, 'PointX')).toHaveLength(0);
expect(filterExpressions(objectExpressions, 'Layer')).toHaveLength(1);
expect(filterExpressions(objectExpressions, 'X')).toContainEqual(
@@ -80,7 +81,7 @@ describe('EnumerateExpressions', () => {
);
});
it('can enumerate and filter behavior expressions', () => {
it('can enumerate and filter behavior expressions (number only)', () => {
const platformerObjectBehaviorExpressions = enumerateBehaviorExpressions(
'number',
'PlatformBehavior::PlatformerObjectBehavior'
@@ -103,10 +104,11 @@ describe('EnumerateExpressions', () => {
})
);
});
it('can enumerate and filter behavior expressions (type "number|string")', () => {
it('can enumerate behavior expressions', () => {
makeTestExtensions(gd);
const fakeBehaviorExpressions = enumerateBehaviorExpressions(
'number|string',
'string',
'FakeBehavior::FakeBehavior'
);
@@ -145,7 +147,7 @@ describe('EnumerateExpressions', () => {
});
});
it('can enumerate all expressions (type "number")', () => {
it('can enumerate all expressions (number only)', () => {
makeTestExtensions(gd);
const allNumberExpressions: Array<EnumeratedExpressionMetadata> = enumerateAllExpressions(
'number'
@@ -173,38 +175,10 @@ describe('EnumerateExpressions', () => {
).toHaveLength(0);
});
it('can enumerate all expressions (type "string")', () => {
makeTestExtensions(gd);
const allStringExpressions: Array<EnumeratedExpressionMetadata> = enumerateAllExpressions(
'string'
);
// Check a free expression:
expect(allStringExpressions).toContainEqual(
expect.objectContaining({
type: 'ToString',
})
);
// Check a behavior expression:
expect(allStringExpressions).toContainEqual(
expect.objectContaining({
type: 'SomethingReturningStringWith1NumberParam',
})
);
// Sanity check number expressions are not there:
expect(filterExpressions(allStringExpressions, 'ToNumber')).toHaveLength(0);
expect(
filterExpressions(
allStringExpressions,
'SomethingReturningNumberWith1NumberParam'
)
).toHaveLength(0);
});
it('can enumerate all expressions (type "number|string")', () => {
it('can enumerate all expressions', () => {
makeTestExtensions(gd);
const allExpressions: Array<EnumeratedExpressionMetadata> = enumerateAllExpressions(
'number|string'
'string'
);
// Check a free expression:
expect(allExpressions).toContainEqual(

View File

@@ -44,7 +44,11 @@ const freeActionsToAddToBehavior: ExtensionsExtraInstructions = {};
const freeConditionsToAddToBehavior: ExtensionsExtraInstructions = {
Physics2: {
'Physics2::Physics2Behavior': ['Physics2::Collision'],
'Physics2::Physics2Behavior': [
'Physics2::Collision',
'Physics2::CollisionStarted',
'Physics2::CollisionStopped',
],
},
};

View File

@@ -54,6 +54,13 @@ export const loadPreferencesFromLocalStorage = (): ?PreferencesValues => {
}
}
// Migrate renamed themes.
if (values.themeName === 'GDevelop default') {
values.themeName = 'GDevelop default Light';
} else if (values.themeName === 'Dark') {
values.themeName = 'Blue Dark';
}
return values;
} catch (e) {
return null;

View File

@@ -36,6 +36,7 @@ type Props = {|
hideControls?: boolean,
initialZoom?: number,
fixedHeight?: number,
fixedWidth?: number,
|};
const AnimationPreview = ({
@@ -50,6 +51,7 @@ const AnimationPreview = ({
hideControls,
initialZoom,
fixedHeight,
fixedWidth,
}: Props) => {
const forceUdpate = useForceUpdate();
@@ -171,6 +173,7 @@ const AnimationPreview = ({
hideCheckeredBackground={hideCheckeredBackground}
hideControls={hideControls}
fixedHeight={fixedHeight}
fixedWidth={fixedWidth}
/>
{!hideControls && (
<LineStackLayout noMargin alignItems="center">

View File

@@ -48,9 +48,6 @@ const ObjectsEditorService = {
return;
}
console.info(
`Properly registered editor configuration for object "${objectType}".`
);
this.editorConfigurations[objectType] = editorConfiguration;
},
getDefaultObjectJsImplementationPropertiesEditor(options) {

View File

@@ -107,7 +107,6 @@ const ObjectsRenderingService = {
return;
}
console.info(`Properly registered renderer for object "${objectType}".`);
this.renderers[objectType] = renderer;
},
/**

View File

@@ -40,7 +40,6 @@ const styles = {
width: '100%',
height: '100%',
boxSizing: 'border-box',
overflow: 'auto',
// The container contains the image and the "overlay" that can display
// points or polygons that can be drag'n'dropped. `touch-action` must
@@ -51,7 +50,6 @@ const styles = {
spriteThumbnailImage: {
position: 'relative',
pointerEvents: 'none',
margin: MARGIN,
},
sliderContainer: {
maxWidth: 150,
@@ -68,6 +66,7 @@ type Props = {|
isImageResourceSmooth: boolean,
initialZoom?: number,
fixedHeight?: number,
fixedWidth?: number,
renderOverlay?: ({|
imageWidth: number,
imageHeight: number,
@@ -100,6 +99,7 @@ const ImagePreview = ({
imageResourceSource,
isImageResourceSmooth,
fixedHeight,
fixedWidth,
renderOverlay,
onSize,
hideCheckeredBackground,
@@ -166,37 +166,49 @@ const ImagePreview = ({
{({ contentRect, measureRef }) => {
const containerWidth = contentRect.bounds.width;
const containerHeight = contentRect.bounds.height;
// Once the image is loaded, adapt the zoom to the image size.
if (!isResizeObserverReady && !!containerWidth && !!containerHeight) {
const containerLoaded = !!containerWidth && !!containerHeight;
const imageLoaded = !!imageWidth && !!imageHeight && !errored;
// Once the container is loaded, adapt the zoom to the image size.
if (!isResizeObserverReady && containerLoaded) {
if (!initialZoom) {
adaptZoomToImage(containerHeight, containerWidth);
}
setIsResizeObserverReady(true);
}
const imageLoaded = !!imageWidth && !!imageHeight && !errored;
// Centre-align the image and overlays
const imagePositionTop = Math.max(
0,
containerHeight / 2 -
(containerHeight || 0) / 2 -
((imageHeight || 0) * imageZoomFactor) / 2 -
MARGIN
);
const imagePositionLeft = Math.max(
0,
containerWidth / 2 -
(containerWidth || 0) / 2 -
((imageWidth || 0) * imageZoomFactor) / 2 -
MARGIN
);
// We display the elements only when the image is loaded and
// the zoom is applied to avoid a shift in the image.
// We use "visibility": "hidden" instead of "display": "none"
// so that the image takes the space of the container whilst being hidden.
// TODO: handle a proper loader.
const visibility = containerLoaded ? undefined : 'hidden';
const width = imageWidth ? imageWidth * imageZoomFactor : undefined;
const height = imageHeight ? imageHeight * imageZoomFactor : undefined;
const imageStyle = {
...styles.spriteThumbnailImage,
top: imagePositionTop || 0,
left: imagePositionLeft || 0,
width: imageWidth ? imageWidth * imageZoomFactor : undefined,
height: imageHeight ? imageHeight * imageZoomFactor : undefined,
visibility: imageLoaded ? undefined : 'hidden', // TODO: Loader
// Apply margin only once the container is loaded, to avoid a shift in the image
margin: containerLoaded ? MARGIN : 0,
top: imagePositionTop,
left: imagePositionLeft,
width,
height,
visibility,
...(!isImageResourceSmooth
? styles.previewImagePixelated
: undefined),
@@ -204,11 +216,11 @@ const ImagePreview = ({
const frameStyle = {
position: 'absolute',
top: imagePositionTop + MARGIN || 0,
left: imagePositionLeft + MARGIN || 0,
width: imageWidth ? imageWidth * imageZoomFactor : undefined,
height: imageHeight ? imageHeight * imageZoomFactor : undefined,
visibility: imageLoaded ? undefined : 'hidden', // TODO: Loader
top: imagePositionTop + MARGIN,
left: imagePositionLeft + MARGIN,
width,
height,
visibility,
border: `1px solid ${frameBorderColor}`,
boxSizing: 'border-box',
};
@@ -219,7 +231,7 @@ const ImagePreview = ({
left: 0,
width: '100%',
height: '100%',
visibility: imageLoaded ? undefined : 'hidden', // TODO: Loader
visibility,
};
return (
@@ -261,6 +273,7 @@ const ImagePreview = ({
style={{
...styles.contentContainer,
height: fixedHeight || '100%',
width: fixedWidth,
}}
>
{!hideCheckeredBackground && <CheckeredBackground />}
@@ -268,7 +281,10 @@ const ImagePreview = ({
dir={
'ltr' /* Force LTR layout to avoid issues with image positioning */
}
style={styles.imagePreviewContainer}
style={{
...styles.imagePreviewContainer,
overflow: containerLoaded ? 'auto' : 'hidden',
}}
ref={measureRef}
onWheel={event => {
const { deltaY } = event;

View File

@@ -292,12 +292,25 @@ export default class ResourcesList extends React.Component<Props, State> {
{ type: 'separator' },
{
label: i18n._(t`Remove unused...`),
submenu: allResourceKindsAndMetadata.map(({ displayName, kind }) => ({
label: i18n._(displayName),
click: () => {
this.props.onRemoveUnusedResources(kind);
},
})),
submenu: allResourceKindsAndMetadata
.map(({ displayName, kind }) => ({
label: i18n._(displayName),
click: () => {
this.props.onRemoveUnusedResources(kind);
},
}))
.concat([
{
label: i18n._(t`Resources (any kind)`),
click: () => {
allResourceKindsAndMetadata.forEach(resourceKindAndMetadata => {
this.props.onRemoveUnusedResources(
resourceKindAndMetadata.kind
);
});
},
},
]),
},
{
label: i18n._(t`Remove Resources with Invalid Path`),

View File

@@ -1,8 +1,9 @@
// @flow
import * as React from 'react';
import FormControlLabel from '@material-ui/core/FormControlLabel';
import MUICheckbox from '@material-ui/core/Checkbox';
import { makeStyles } from '@material-ui/core/styles';
import FormControlLabel from '@material-ui/core/FormControlLabel';
import FormGroup from '@material-ui/core/FormGroup';
import MUICheckbox from '@material-ui/core/Checkbox';
// Reduce checkbox size to avoid overlapping with other checkboxes.
const useStyles = makeStyles({
@@ -13,6 +14,12 @@ const useStyles = makeStyles({
},
});
const useFormGroupStyles = makeStyles({
root: {
display: 'block',
},
});
// We support a subset of the props supported by Material-UI v0.x Checkbox
// They should be self descriptive - refer to Material UI docs otherwise.
type Props = {|
@@ -36,6 +43,7 @@ type Props = {|
const Checkbox = (props: Props) => {
const { onCheck } = props;
const classes = useStyles();
const formGroupClasses = useFormGroupStyles();
const checkbox = (
<MUICheckbox
className={classes.root}
@@ -51,11 +59,13 @@ const Checkbox = (props: Props) => {
/>
);
return props.label ? (
<FormControlLabel
control={checkbox}
label={props.label}
style={props.style}
/>
<FormGroup classes={formGroupClasses}>
<FormControlLabel
control={checkbox}
label={props.label}
style={props.style}
/>
</FormGroup>
) : (
checkbox
);

View File

@@ -4,10 +4,10 @@ import { type I18n as I18nType } from '@lingui/core';
import * as React from 'react';
import Close from '@material-ui/icons/Close';
import ButtonBase from '@material-ui/core/ButtonBase';
import ThemeConsumer from './Theme/ThemeConsumer';
import ContextMenu, { type ContextMenuInterface } from './Menu/ContextMenu';
import { useLongTouch } from '../Utils/UseLongTouch';
import { Spacer } from './Grid';
import GDevelopThemeContext from './Theme/ThemeContext';
const styles = {
tabContentContainer: {
@@ -76,22 +76,19 @@ type ClosableTabsProps = {|
children: React.Node,
|};
export const ClosableTabs = ({ hideLabels, children }: ClosableTabsProps) => (
<ThemeConsumer>
{muiTheme => {
const tabItemContainerStyle = {
maxWidth: '100%', // Tabs should take all width
flexShrink: 0, // Tabs height should never be reduced
display: hideLabels ? 'none' : 'flex',
flexWrap: 'nowrap', // Single line of tab...
overflowX: 'auto', // ...scroll horizontally if needed
backgroundColor: muiTheme.closableTabs.containerBackgroundColor,
};
export const ClosableTabs = ({ hideLabels, children }: ClosableTabsProps) => {
const gdevelopTheme = React.useContext(GDevelopThemeContext);
const tabItemContainerStyle = {
maxWidth: '100%', // Tabs should take all width
flexShrink: 0, // Tabs height should never be reduced
display: hideLabels ? 'none' : 'flex',
flexWrap: 'nowrap', // Single line of tab...
overflowX: 'auto', // ...scroll horizontally if needed
backgroundColor: gdevelopTheme.closableTabs.containerBackgroundColor,
};
return <div style={tabItemContainerStyle}>{children}</div>;
}}
</ThemeConsumer>
);
return <div style={tabItemContainerStyle}>{children}</div>;
};
export type ClosableTabProps = {|
id?: string,
@@ -156,90 +153,85 @@ export function ClosableTab({
)
);
return (
<ThemeConsumer>
{muiTheme => {
const textColor = !active
? muiTheme.closableTabs.textColor
: muiTheme.closableTabs.selectedTextColor;
const gdevelopTheme = React.useContext(GDevelopThemeContext);
const textColor = !active
? gdevelopTheme.closableTabs.textColor
: gdevelopTheme.closableTabs.selectedTextColor;
return (
<React.Fragment>
<span
return (
<React.Fragment>
<span
style={{
flexShrink: 0, // Tabs are never resized to fit in flex container
position: 'relative',
display: 'inline-block',
marginRight: 1,
backgroundColor: !active
? gdevelopTheme.closableTabs.backgroundColor
: gdevelopTheme.closableTabs.selectedBackgroundColor,
}}
>
<ButtonBase
onClick={onClick}
onAuxClick={closable ? closeOnMiddleClick : undefined}
onContextMenu={openContextMenu}
id={id ? `${id}-button` : undefined}
{...longTouchForContextMenuProps}
focusRipple
// If the touch ripple is not disabled, the dragged preview will
// use the size of the ripple and it will be too big.
disableTouchRipple
>
<span
style={{
...styles.tabLabelAndIcon,
height: gdevelopTheme.closableTabs.height,
color: textColor,
fontFamily: gdevelopTheme.closableTabs.fontFamily,
}}
>
{icon}
{icon && label ? <Spacer /> : null}
{label && <span style={styles.tabLabel}>{label}</span>}
</span>
</ButtonBase>
{closable && (
<ButtonBase
onClick={onClose}
onAuxClick={closeOnMiddleClick}
onContextMenu={openContextMenu}
{...longTouchForContextMenuProps}
focusRipple
>
<Close
style={{
flexShrink: 0, // Tabs are never resized to fit in flex container
position: 'relative',
display: 'inline-block',
marginRight: 1,
backgroundColor: !active
? muiTheme.closableTabs.backgroundColor
: muiTheme.closableTabs.selectedBackgroundColor,
...styles.closeButton,
width: gdevelopTheme.closableTabs.height / 2,
height: gdevelopTheme.closableTabs.height,
}}
>
<ButtonBase
onClick={onClick}
onAuxClick={closable ? closeOnMiddleClick : undefined}
onContextMenu={openContextMenu}
id={id ? `${id}-button` : undefined}
{...longTouchForContextMenuProps}
focusRipple
// If the touch ripple is not disabled, the dragged preview will
// use the size of the ripple and it will be too big.
disableTouchRipple
>
<span
style={{
...styles.tabLabelAndIcon,
height: muiTheme.closableTabs.height,
color: textColor,
fontFamily: muiTheme.closableTabs.fontFamily,
}}
>
{icon}
{icon && label ? <Spacer /> : null}
{label && <span style={styles.tabLabel}>{label}</span>}
</span>
</ButtonBase>
{closable && (
<ButtonBase
onClick={onClose}
onAuxClick={closeOnMiddleClick}
onContextMenu={openContextMenu}
{...longTouchForContextMenuProps}
focusRipple
>
<Close
style={{
...styles.closeButton,
width: muiTheme.closableTabs.height / 2,
height: muiTheme.closableTabs.height,
}}
htmlColor={textColor}
/>
</ButtonBase>
)}
</span>
<ContextMenu
ref={contextMenu}
buildMenuTemplate={(i18n: I18nType) => [
{
label: i18n._(t`Close`),
click: onClose,
enabled: closable,
},
{
label: i18n._(t`Close others`),
click: onCloseOthers,
},
{
label: i18n._(t`Close all`),
click: onCloseAll,
},
]}
htmlColor={textColor}
/>
</React.Fragment>
);
}}
</ThemeConsumer>
</ButtonBase>
)}
</span>
<ContextMenu
ref={contextMenu}
buildMenuTemplate={(i18n: I18nType) => [
{
label: i18n._(t`Close`),
click: onClose,
enabled: closable,
},
{
label: i18n._(t`Close others`),
click: onCloseOthers,
},
{
label: i18n._(t`Close all`),
click: onCloseAll,
},
]}
/>
</React.Fragment>
);
}

View File

@@ -13,7 +13,6 @@ const appBarHeight = 32;
type Props = {|
title: React.Node,
displayLeftCloseButton?: boolean,
displayRightCloseButton?: boolean,
onClose: () => void,
|};
@@ -45,13 +44,9 @@ const DrawerTopBar = (props: Props) => {
style={styles.appBar}
className="safe-area-aware-top-margin"
color="primary"
elevation={0}
>
<Toolbar style={styles.toolbar}>
{props.displayLeftCloseButton && (
<IconButton onClick={props.onClose} edge="start" color="inherit">
<Close />
</IconButton>
)}
<Tooltip
title={props.title}
placement="bottom"
@@ -63,7 +58,12 @@ const DrawerTopBar = (props: Props) => {
</Tooltip>
{props.displayRightCloseButton && (
<IconButton onClick={props.onClose} edge="end" color="inherit">
<IconButton
onClick={props.onClose}
edge="end"
color="inherit"
size="small"
>
<Close />
</IconButton>
)}

View File

@@ -1,4 +1,5 @@
import React, { Component } from 'react';
// @flow
import * as React from 'react';
import IconButton from '../IconButton';
import Close from '@material-ui/icons/Close';
import { MosaicWindowContext, MosaicContext } from 'react-mosaic-component';
@@ -15,25 +16,20 @@ const styles = {
},
};
export default class CloseButton extends Component {
render() {
return (
<MosaicContext.Consumer>
{({ mosaicActions }) => (
<MosaicWindowContext.Consumer>
{({ mosaicWindowActions }) => (
<IconButton
onClick={() => {
mosaicActions.remove(mosaicWindowActions.getPath());
}}
style={styles.container}
>
<Close htmlColor="white" style={styles.icon} />
</IconButton>
)}
</MosaicWindowContext.Consumer>
)}
</MosaicContext.Consumer>
);
}
type Props = {||};
export default function CloseButton(props: Props) {
const { mosaicActions } = React.useContext(MosaicContext);
const { mosaicWindowActions } = React.useContext(MosaicWindowContext);
return (
<IconButton
onClick={() => {
mosaicActions.remove(mosaicWindowActions.getPath());
}}
style={styles.container}
>
<Close htmlColor="inherit" style={styles.icon} />
</IconButton>
);
}

View File

@@ -1,5 +1,5 @@
// @flow
import React, { Component } from 'react';
import * as React from 'react';
import { type I18n as I18nType } from '@lingui/core';
import IconButton from '../IconButton';
import FilterList from '@material-ui/icons/FilterList';
@@ -22,17 +22,15 @@ type Props = {|
buildMenuTemplate: (i18n: I18nType) => Array<MenuItemTemplate>,
|};
export default class TagsButton extends Component<Props, {||}> {
render() {
return (
<ElementWithMenu
element={
<IconButton style={styles.container}>
<FilterList htmlColor="white" style={styles.icon} />
</IconButton>
}
buildMenuTemplate={this.props.buildMenuTemplate}
/>
);
}
export default function TagsButton(props: Props) {
return (
<ElementWithMenu
element={
<IconButton style={styles.container}>
<FilterList htmlColor="inherit" style={styles.icon} />
</IconButton>
}
buildMenuTemplate={props.buildMenuTemplate}
/>
);
}

View File

@@ -3,7 +3,6 @@ import * as React from 'react';
import Button from '@material-ui/core/Button';
import { type ButtonInterface } from './Button';
import { Spacer } from './Grid';
import GDevelopThemeContext from './Theme/ThemeContext';
// We support a subset of the props supported by Material-UI v0.x FlatButton
// They should be self descriptive - refer to Material UI docs otherwise.
@@ -49,11 +48,9 @@ const FlatButton = React.forwardRef<Props, ButtonInterface>(
// always visible to be sure we're getting focusing right.
const focusRipple = true;
const gdevelopTheme = React.useContext(GDevelopThemeContext);
return (
<Button
variant={gdevelopTheme.isModern ? 'outlined' : 'text'}
variant="outlined"
size="small"
color={primary ? 'primary' : 'default'}
autoFocus={keyboardFocused}

View File

@@ -1,9 +1,11 @@
// @flow
import * as React from 'react';
import { makeStyles } from '@material-ui/core/styles';
import FormControlLabel from '@material-ui/core/FormControlLabel';
import FormGroup from '@material-ui/core/FormGroup';
import FormHelperText from '@material-ui/core/FormHelperText';
import Checkbox from '@material-ui/core/Checkbox';
import Tooltip from '@material-ui/core/Tooltip';
import { FormGroup, FormHelperText, makeStyles } from '@material-ui/core';
const useLabelStyles = makeStyles({
root: {
@@ -11,6 +13,12 @@ const useLabelStyles = makeStyles({
},
});
const useFormGroupStyles = makeStyles({
root: {
display: 'block',
},
});
type Props = {|
label?: ?React.Node,
checked: boolean,
@@ -35,6 +43,7 @@ const InlineCheckbox = ({
tooltipOrHelperText,
}: Props) => {
const labelClasses = useLabelStyles();
const formGroupClasses = useFormGroupStyles();
const checkbox = (
<Checkbox
disabled={disabled}
@@ -48,7 +57,7 @@ const InlineCheckbox = ({
/>
);
return label ? (
<FormGroup>
<FormGroup classes={formGroupClasses}>
<FormControlLabel
control={checkbox}
label={label}

View File

@@ -163,6 +163,7 @@ type ColumnStackLayoutProps = {|
noMargin?: boolean,
children: React.Node,
noOverflowParent?: boolean,
useFullHeight?: boolean,
|};
export const ColumnStackLayout = ({
@@ -172,6 +173,7 @@ export const ColumnStackLayout = ({
noMargin,
children,
noOverflowParent,
useFullHeight,
}: ColumnStackLayoutProps) => {
let isFirstChild = true;
return (
@@ -181,6 +183,7 @@ export const ColumnStackLayout = ({
expand={expand}
noMargin={noMargin}
noOverflowParent={noOverflowParent}
useFullHeight={useFullHeight}
>
{React.Children.map(children, (child, index) => {
if (!child) return null;

View File

@@ -8,6 +8,7 @@ import PreferencesContext, {
} from '../../MainFrame/Preferences/PreferencesContext';
import { useScreenType } from '../Reponsive/ScreenTypeMeasurer';
import { isUserflowRunning } from '../../MainFrame/Onboarding/OnboardingDialog';
import GDevelopThemeContext from '../Theme/ThemeContext';
type Props = {|
identifier: AlertMessageIdentifier,
@@ -23,6 +24,7 @@ const DismissableInfoBar = ({
message,
}: Props) => {
const preferences = React.useContext(PreferencesContext);
const gdevelopTheme = React.useContext(GDevelopThemeContext);
const screenType = useScreenType();
return isUserflowRunning ? null : (
@@ -36,7 +38,9 @@ const DismissableInfoBar = ({
action={
<Button
key="undo"
color="primary"
color={
gdevelopTheme.palette.type === 'light' ? 'secondary' : 'primary'
}
size="small"
onClick={() => {
preferences.showAlertMessage(identifier, false);

View File

@@ -3,7 +3,6 @@ import * as React from 'react';
import Button from '@material-ui/core/Button';
import { type ButtonInterface } from './Button';
import { Spacer } from './Grid';
import GDevelopThemeContext from './Theme/ThemeContext';
// We support a subset of the props supported by Material-UI v0.x RaisedButton
// They should be self descriptive - refer to Material UI docs otherwise.
@@ -39,20 +38,13 @@ const RaisedButton = React.forwardRef<Props, ButtonInterface>(
// it's important to get focus right in the whole app, make the ripple
// always visible to be sure we're getting focusing right.
const focusRipple = true;
const gdevelopTheme = React.useContext(GDevelopThemeContext);
return (
<Button
variant="contained"
size="small"
disableElevation={gdevelopTheme.isModern}
color={
primary
? gdevelopTheme.isModern
? 'secondary'
: 'primary'
: 'default'
}
disableElevation
color={primary ? 'secondary' : 'default'}
focusRipple={focusRipple}
{...otherProps}
ref={ref}

View File

@@ -7,7 +7,6 @@ import ButtonGroup from '@material-ui/core/ButtonGroup';
import Button from '@material-ui/core/Button';
import ArrowDropDownIcon from '@material-ui/icons/ArrowDropDown';
import { Spacer } from './Grid';
import GDevelopThemeContext from './Theme/ThemeContext';
type Props = {|
label?: React.Node,
@@ -49,21 +48,11 @@ const RaisedButtonWithSplitMenu = (props: Props) => {
// always visible to be sure we're getting focusing right.
const focusRipple = true;
const gdevelopTheme = React.useContext(GDevelopThemeContext);
return (
<ButtonGroup
variant={primary ? 'contained' : 'outlined'}
disableElevation={gdevelopTheme.isModern}
color={
gdevelopTheme.isModern
? primary
? 'secondary'
: 'primary'
: primary
? 'primary'
: 'default'
}
disableElevation
color={primary ? 'secondary' : 'primary'}
disabled={disabled}
size="small"
style={props.style}

View File

@@ -63,8 +63,6 @@ export const BoxSearchResults = <SearchItem>({
<div style={styles.container}>
<AutoSizer>
{({ width, height }) => {
if (!width || !height) return null;
const columnCount = Math.max(Math.floor((width - 5) / baseSize), 1);
const columnWidth = Math.max(Math.floor(width / columnCount), 30);
const rowCount = Math.max(

View File

@@ -41,8 +41,8 @@ type Props = {|
onChange?: string => void,
/** Fired when the search icon is clicked. */
onRequestSearch: string => void,
/** Set if margins should be added or not. */
aspect?: 'integrated-search-bar' | 'add-margins-only-if-modern-theme',
/** Set if rounding should be applied or not. */
aspect?: 'integrated-search-bar',
/** The value of the text field. */
value: string,
/** The functions needed to interact with the list of tags displayed below search bar. */
@@ -167,10 +167,6 @@ const SearchBar = React.forwardRef<Props, SearchBarInterface>(
};
const gdevelopTheme = React.useContext(GDevelopThemeContext);
const noMargin =
aspect === 'add-margins-only-if-modern-theme' && gdevelopTheme.isModern
? false
: true;
// This variable represents the content of the input (text field)
const [value, setValue] = React.useState<string>(parentValue);
@@ -288,17 +284,15 @@ const SearchBar = React.forwardRef<Props, SearchBarInterface>(
return (
<I18n>
{({ i18n }) => (
<Column noMargin={noMargin}>
<Line noMargin={noMargin}>
<Column noMargin>
<Line noMargin>
<Paper
style={{
backgroundColor: gdevelopTheme.searchBar.backgroundColor,
...styles.root,
}}
square={
aspect === 'integrated-search-bar' || !gdevelopTheme.isModern
}
elevation={gdevelopTheme.isModern ? 0 : 1}
square={aspect === 'integrated-search-bar'}
elevation={0}
>
<div style={styles.searchContainer}>
{tags ? (

View File

@@ -2,7 +2,6 @@
import * as React from 'react';
import MUITabs from '@material-ui/core/Tabs';
import MUITab from '@material-ui/core/Tab';
import GDevelopThemeContext from './Theme/ThemeContext';
// We support a subset of the props supported by Material-UI v0.x Tabs
// They should be self descriptive - refer to Material UI docs otherwise.
@@ -16,13 +15,11 @@ type TabsProps<TabName> = {|
* Tabs based on Material-UI Tabs.
*/
export function Tabs<TabName>(props: TabsProps<TabName>) {
const gdevelopTheme = React.useContext(GDevelopThemeContext);
return (
<MUITabs
variant="fullWidth"
textColor="primary"
indicatorColor={gdevelopTheme.isModern ? 'primary' : 'secondary'}
indicatorColor={'primary'}
value={props.value}
onChange={(e, newValue) => props.onChange(newValue)}
>

View File

@@ -0,0 +1,11 @@
import { createGdevelopTheme } from '../CreateTheme';
import styles from './BlueDarkThemeVariables.json';
import './BlueDarkThemeVariables.css';
export default createGdevelopTheme({
styles,
rootClassNameIdentifier: 'BlueDarkTheme',
paletteType: 'dark',
});

View File

@@ -2,18 +2,20 @@
"theme": {
"primary": {
"color": {
"value": "{gdevelop.color.blue.value}"
"value": "#6BAFFF",
"comment": "Palette/Blue/40"
},
"text-contrast-color": {
"value": "#fff"
"value": "#FFFFFF"
}
},
"secondary": {
"color": {
"value": "#EEE"
"value": "#006DEE",
"comment": "Palette/Blue/60"
},
"text-contrast-color": {
"value": "#000"
"value": "#FFFFFF"
}
},
"surface": {
@@ -193,7 +195,7 @@
},
"tabs": {
"background-color": {
"value": "#3c4698"
"value": "#4C5361"
}
},
"event-sheet": {

View File

@@ -17,11 +17,11 @@ export function getRootClassNames(theme: string) {
}
export function getMuiOverrides(
isModern: boolean,
tabTextColor: string,
tabBackgroundColor: string,
inputBorderBottomColor: string,
appBarBackgroundColor: string,
appBarTextColor: string,
iconColor: string,
outlinedButtonBorderColor: string
) {
@@ -29,11 +29,11 @@ export function getMuiOverrides(
MuiTypography: {
h5: {
// Make h6, used in Drawer title bars, use the same weight as tabs and mosaic windows
fontWeight: isModern ? 600 : 400,
fontWeight: 600,
},
h6: {
// Make h6, used in Drawer title bars, use the same weight as tabs and mosaic windows
fontWeight: isModern ? 600 : 400,
fontWeight: 600,
},
},
MuiInput: {
@@ -49,7 +49,9 @@ export function getMuiOverrides(
},
MuiAppBar: {
colorPrimary: {
// Use some colors as mosaic titles:
backgroundColor: appBarBackgroundColor,
color: appBarTextColor,
},
},
MuiIconButton: {
@@ -87,18 +89,13 @@ export function getMuiOverrides(
},
MuiTab: {
textColorPrimary: {
color:
(isModern ? darken(tabTextColor, 0.2) : tabTextColor) + ' !important',
textTransform: isModern ? 'none' : 'uppercase',
fontWeight: isModern ? 600 : 500,
letterSpacing: isModern ? '0.3px' : '',
...(isModern
? {
'&.Mui-selected': {
color: tabTextColor + ' !important',
},
}
: {}),
color: darken(tabTextColor, 0.2) + ' !important',
textTransform: 'none',
fontWeight: 600,
letterSpacing: '0.3px',
'&.Mui-selected': {
color: tabTextColor + ' !important',
},
},
root: {
// Reduce the height of tabs to 32px
@@ -150,19 +147,17 @@ export function getMuiOverrides(
},
MuiPaper: {
rounded: {
borderRadius: isModern ? 8 : 4,
borderRadius: 8,
},
},
MuiButton: {
root: {
borderRadius: isModern ? 8 : 0,
textTransform: isModern ? 'none' : 'uppercase',
fontWeight: isModern ? 600 : 400, // Lower a bit the weight of buttons
letterSpacing: isModern ? '0.3px' : '',
borderRadius: 8,
textTransform: 'none',
fontWeight: 600,
letterSpacing: '0.3px',
},
...(isModern
? { outlined: { borderColor: outlinedButtonBorderColor } }
: {}),
outlined: { borderColor: outlinedButtonBorderColor },
},
// Make MuiAccordion much more compact than default.
// Some (or all) of these styles can be removed on MUIv5,
@@ -222,21 +217,19 @@ export function getThemeMode(color: string, contrastText: string) {
export function createGdevelopTheme({
styles,
isModern,
rootClassNameIdentifier,
paletteType,
gdevelopIconsCSSFilter,
}: {
styles: any,
isModern?: boolean,
rootClassNameIdentifier: string,
paletteType: string,
gdevelopIconsCSSFilter: ?string,
}) {
return {
gdevelopTheme: {
isModern: !!isModern,
palette: {
type: paletteType,
canvasColor: styles['ThemeSurfaceCanvasBackgroundColor'],
},
message: {
@@ -259,7 +252,7 @@ export function createGdevelopTheme({
border: styles['ThemeDropIndicatorBorderColor'],
},
closableTabs: {
fontFamily: styles['GdevelopFontFamily'],
fontFamily: styles['GdevelopModernFontFamily'],
containerBackgroundColor: styles['ThemeSurfaceWindowBackgroundColor'],
backgroundColor: styles['ThemeClosableTabsDefaultBackgroundColor'],
textColor: styles['ThemeClosableTabsDefaultColor'],
@@ -309,9 +302,7 @@ export function createGdevelopTheme({
},
muiThemeOptions: {
typography: {
fontFamily: isModern
? styles['GdevelopModernFontFamily']
: styles['GdevelopFontFamily'],
fontFamily: styles['GdevelopModernFontFamily'],
},
palette: {
type: paletteType,
@@ -339,11 +330,11 @@ export function createGdevelopTheme({
},
},
overrides: getMuiOverrides(
!!isModern,
styles['ThemeTextContrastColor'],
styles['TabsBackgroundColor'],
styles['InputBorderBottomColor'],
styles['MosaicToolbarBackgroundColor'],
styles['MosaicTitleColor'],
styles['ThemeTextDefaultColor'],
styles['ThemeTextDefaultColor']
),

View File

@@ -1,10 +0,0 @@
import { createGdevelopTheme } from '../CreateTheme';
import styles from './DarkThemeVariables.json';
import './DarkThemeVariables.css';
export default createGdevelopTheme({
styles,
rootClassNameIdentifier: 'DarkTheme',
paletteType: 'dark',
});

View File

@@ -5,7 +5,7 @@ import './DefaultDarkThemeVariables.css';
export default createGdevelopTheme({
styles,
isModern: true,
rootClassNameIdentifier: 'DefaultDarkTheme',
paletteType: 'dark',
});

View File

@@ -0,0 +1,10 @@
import { createGdevelopTheme } from '../CreateTheme';
import styles from './DefaultLightThemeVariables.json';
import './DefaultLightThemeVariables.css';
export default createGdevelopTheme({
styles,
rootClassNameIdentifier: 'DefaultLightTheme',
paletteType: 'light',
});

View File

@@ -2,18 +2,20 @@
"theme": {
"primary": {
"color": {
"value": "{gdevelop.color.light-blue.value}"
"value": "#42229B",
"comment": "Palette/Purple/70"
},
"text-contrast-color": {
"value": "#fff"
"value": "#FFFFFF"
}
},
"secondary": {
"color": {
"value": "{gdevelop.color.purple.value}"
"value": "#6A3BEB",
"comment": "Palette/Purple/60"
},
"text-contrast-color": {
"value": "#fff"
"value": "#FFFFFF"
}
},
"surface": {
@@ -24,32 +26,36 @@
},
"canvas": {
"background-color": {
"value": "#f0f0f0"
"value": "#ffffff"
}
},
"alternate-canvas": {
"background-color": {
"value": "#ffffff"
"value": "#f0f0f0"
}
}
},
"selection": {
"background-color": {
"value": "{gdevelop.color.selection.value}"
"value": "#ECE5FF",
"comment": "Palette/Purple/10"
},
"color": {
"value": "#FFFFFF"
"value": "#1D1D26",
"comment": "Text/Primary"
}
},
"text": {
"default": {
"color": {
"value": "#000"
"value": "#1D1D26",
"comment": "Text/Primary"
}
},
"secondary": {
"color": {
"value": "#000"
"value": "#1D1D26",
"comment": "Text/Primary"
}
},
"disabled": {
@@ -59,7 +65,7 @@
},
"contrast": {
"color": {
"value": "#FFFFFF"
"value": "#515151"
}
},
"highlighted": {
@@ -98,18 +104,18 @@
"closable-tabs": {
"default": {
"background-color": {
"value": "{theme.surface.canvas.background-color.value}"
"value": "#f7f7f7"
},
"color": {
"value": "#878787"
"value": "#444444"
}
},
"selected": {
"background-color": {
"value": "{gdevelop.color.light-blue.value}"
"value": "#e1e1e1"
},
"color": {
"value": "#ffffff"
"value": "#000000"
}
}
},
@@ -194,7 +200,7 @@
},
"tabs": {
"background-color": {
"value": "#4ab0e4"
"value": "#efefef"
}
},
"event-sheet": {
@@ -375,7 +381,7 @@
"mosaic": {
"title": {
"color": {
"value": "#FFFFFF"
"value": "#000000"
}
},
"layout": {
@@ -388,10 +394,10 @@
},
"toolbar": {
"background-color": {
"value": "#9100ce"
"value": "#e1e1e1"
},
"border-color-hover": {
"value": "#2b95d6"
"value": "#efefef"
}
}
},

View File

@@ -1,9 +0,0 @@
import { createGdevelopTheme } from '../CreateTheme';
import styles from './DefaultThemeVariables.json';
import './DefaultThemeVariables.css';
export default createGdevelopTheme({
styles,
rootClassNameIdentifier: 'DefaultTheme',
paletteType: 'light',
});

View File

@@ -6,6 +6,7 @@
/* Mosaic window and tile */
.mosaic-gd-theme .mosaic-window-toolbar {
background: var(--mosaic-toolbar-background-color) !important;
box-shadow: none !important;
}
.mosaic-gd-theme .mosaic-window-body {
@@ -13,9 +14,10 @@
}
.mosaic-gd-theme .mosaic-window .mosaic-window-title {
color: white !important;
padding-left: 8px; /* Follow same spacing as in the rest of the app. */
color: var(--mosaic-title-color) !important;
font-weight: 400 !important;
font-family: var(--gdevelop-classic-font-family);
font-family: var(--gdevelop-modern-font-family);
font-size: 15px; /* Same as ClosableTabs font size */
}

View File

@@ -5,6 +5,7 @@ import './NordThemeVariables.css';
export default createGdevelopTheme({
styles,
rootClassNameIdentifier: 'NordTheme',
paletteType: 'dark',
gdevelopIconsCSSFilter: 'hue-rotate(-15deg) saturate(57%) brightness(120%)',

View File

@@ -10,10 +10,10 @@
},
"secondary": {
"color": {
"value": "#EEE"
"value": "#5E81AC"
},
"text-contrast-color": {
"value": "#000"
"value": "#fff"
}
},
"surface": {
@@ -193,7 +193,7 @@
},
"tabs": {
"background-color": {
"value": "#81A1C1"
"value": "#4C566A"
}
},
"event-sheet": {

View File

@@ -5,6 +5,7 @@ import './OneDarkThemeVariables.css';
export default createGdevelopTheme({
styles,
rootClassNameIdentifier: 'OneDarkTheme',
paletteType: 'dark',
gdevelopIconsCSSFilter: 'hue-rotate(-10deg) saturate(50%)',

View File

@@ -5,6 +5,7 @@ import './SolarizedDarkThemeVariables.css';
export default createGdevelopTheme({
styles,
rootClassNameIdentifier: 'SolarizedDarkTheme',
paletteType: 'dark',
gdevelopIconsCSSFilter: 'hue-rotate(-15deg) saturate(70%) brightness(90%)',

View File

@@ -2,7 +2,7 @@
"theme": {
"primary": {
"color": {
"value": "#047AA6"
"value": "#2799b9"
},
"text-contrast-color": {
"value": "#fff"
@@ -10,10 +10,10 @@
},
"secondary": {
"color": {
"value": "#EEE"
"value": "#047AA6"
},
"text-contrast-color": {
"value": "#000"
"value": "#fff"
}
},
"surface": {
@@ -193,7 +193,7 @@
},
"tabs": {
"background-color": {
"value": "#047AA6"
"value": "#586e75"
}
},
"event-sheet": {

View File

@@ -1,10 +1,10 @@
// @flow
import * as React from 'react';
import { type GDevelopTheme } from '.';
import DefaultTheme from './DefaultTheme';
import DefaultLightTheme from './DefaultLightTheme';
const GDevelopThemeContext = React.createContext<GDevelopTheme>(
DefaultTheme.gdevelopTheme
DefaultLightTheme.gdevelopTheme
);
export default GDevelopThemeContext;

View File

@@ -1,6 +1,6 @@
// This file is generated by create-new-theme.js script
import DefaultTheme from './DefaultTheme';
import DarkTheme from './DarkTheme';
import DefaultLightTheme from './DefaultLightTheme';
import BlueDarkTheme from './BlueDarkTheme';
import NordTheme from './NordTheme';
import SolarizedDarkTheme from './SolarizedDarkTheme';
import OneDarkTheme from './OneDarkTheme';
@@ -10,9 +10,8 @@ import DefaultDarkTheme from './DefaultDarkTheme';
export const themes = {
// "Modern" themes:
['GDevelop default Dark']: DefaultDarkTheme,
// Old themes still based on the "old" components:
['GDevelop default']: DefaultTheme,
['Dark']: DarkTheme,
['GDevelop default Light']: DefaultLightTheme,
['Blue Dark']: BlueDarkTheme,
['Nord']: NordTheme,
['Solarized Dark']: SolarizedDarkTheme,
['One Dark']: OneDarkTheme,

View File

@@ -3,7 +3,7 @@ import { createMuiTheme } from '@material-ui/core/styles';
import { isLtr } from '../../Utils/i18n/RtlLanguages';
import memoize from '../../Utils/Memoize';
import DefaultTheme from './DefaultTheme';
import DefaultLightTheme from './DefaultLightTheme';
import { themes as themeList } from './ThemeRegistry';
import 'react-virtualized/styles.css';
@@ -16,13 +16,13 @@ import './Global/Mosaic.css';
import './Global/Table.css';
import './Global/Font.css';
export type Theme = $Exact<typeof DefaultTheme>;
export type Theme = $Exact<typeof DefaultLightTheme>;
export const themes = themeList;
export type GDevelopTheme = $PropertyType<Theme, 'gdevelopTheme'>;
type ActualTheme = {| gdevelopTheme: GDevelopTheme, muiTheme: Object |};
type MuiThemeOptions = $PropertyType<Theme, 'muiThemeOptions'>;
const defaultThemeName = 'GDevelop default';
const defaultThemeName = 'GDevelop default Dark';
export function getTheme({
themeName,
@@ -89,6 +89,6 @@ const rtlOverrides = {
};
export const defaultTheme: ActualTheme = {
...DefaultTheme,
muiThemeOptions: createLtrTheme(DefaultTheme.muiThemeOptions),
...DefaultLightTheme,
muiThemeOptions: createLtrTheme(DefaultLightTheme.muiThemeOptions),
};

View File

@@ -86,6 +86,7 @@ export const installAnalyticsEvents = (authentication: Authentication) => {
appLanguage,
browserLanguage,
programOpeningCount: getProgramOpeningCount(),
themeName: userPreferences ? userPreferences.themeName : 'Unknown',
...(isElectronApp ? { usedDesktopApp: true } : { usedWebApp: true }),
};

View File

@@ -63,6 +63,7 @@ export const makeFakeExactExpressionAutocompletion = () => {
extension,
expressionMetadata
),
shouldConvertToString: false,
},
];
};
@@ -137,6 +138,7 @@ export const makeFakeExpressionAutocompletions = (): Array<ExpressionAutocomplet
extension,
expressionWithoutParamsMetadata
),
shouldConvertToString: false,
},
{
kind: 'Expression',
@@ -148,6 +150,19 @@ export const makeFakeExpressionAutocompletions = (): Array<ExpressionAutocomplet
extension,
expressionMetadata
),
shouldConvertToString: false,
},
{
kind: 'Expression',
completion: 'MyFunctionThatShouldBeConvertedToString',
addParenthesis: true,
isExact: false,
enumeratedExpressionMetadata: makeFakeEnumeratedExpressionMetadata(
'MyFunction',
extension,
expressionMetadata
),
shouldConvertToString: true,
},
];
};

View File

@@ -5,342 +5,342 @@ module.exports = [
"languageCode": "af_ZA",
"languageName": "Afrikaans",
"languageNativeName": "Afrikaans",
"translationRatio": 0.048800939124601705
"translationRatio": 0.048788582807832714
},
{
"languageCode": "ar_SA",
"languageName": "Arabic",
"languageNativeName": "اللغة العربية",
"translationRatio": 0.386214992453463
"translationRatio": 0.38267507467640227
},
{
"languageCode": "az_AZ",
"languageName": "Azerbaijani",
"languageNativeName": "azərbaycan dili",
"translationRatio": 0.03588797585108172
"translationRatio": 0.0360106206438765
},
{
"languageCode": "bg_BG",
"languageName": "Bulgarian",
"languageNativeName": "български език",
"translationRatio": 0.052993459667952325
"translationRatio": 0.05310321938267504
},
{
"languageCode": "bn_BD",
"languageName": "Bengali",
"languageNativeName": "বাংলা",
"translationRatio": 0.06003689418078151
"translationRatio": 0.060073016926651124
},
{
"languageCode": "ca_ES",
"languageName": "Catalan",
"languageNativeName": "Català",
"translationRatio": 0.30957571692101293
"translationRatio": 0.30567540657152337
},
{
"languageCode": "cs_CZ",
"languageName": "Czech",
"languageNativeName": "čeština",
"translationRatio": 0.14740902230420927
"translationRatio": 0.1495187520743445
},
{
"languageCode": "da_DK",
"languageName": "Danish",
"languageNativeName": "dansk",
"translationRatio": 0.06892503773268488
"translationRatio": 0.06853634251576501
},
{
"languageCode": "de_DE",
"languageName": "German",
"languageNativeName": "Deutsch",
"translationRatio": 0.7249706523561965
"translationRatio": 0.7719880517756389
},
{
"languageCode": "el_GR",
"languageName": "Greek",
"languageNativeName": "Ελληνικά",
"translationRatio": 0.27201073285259103
"translationRatio": 0.2713242615333554
},
{
"languageCode": "en",
"languageName": "English",
"languageNativeName": "English",
"translationRatio": 0.05027932960893855
"translationRatio": 0.052026618269812475
},
{
"languageCode": "eo_UY",
"languageName": "Esperanto",
"languageNativeName": "Esperanto",
"translationRatio": 0.036223377494549736
"translationRatio": 0.03617656820444737
},
{
"languageCode": "es_ES",
"languageName": "Spanish",
"languageNativeName": "Español",
"translationRatio": 0.9882609424786182
"translationRatio": 0.9878858280783273
},
{
"languageCode": "fa_IR",
"languageName": "Persian",
"languageNativeName": "فارسی",
"translationRatio": 0.20878752305886294
"translationRatio": 0.2062728177895785
},
{
"languageCode": "fi_FI",
"languageName": "Finnish",
"languageNativeName": "suomi",
"translationRatio": 0.23629045782324332
"translationRatio": 0.2339860604049121
},
{
"languageCode": "fil_PH",
"languageName": "Filipino",
"languageNativeName": "Mga Filipino",
"translationRatio": 0.3909106154620158
"translationRatio": 0.38615997344839026
},
{
"languageCode": "fr_FR",
"languageName": "French",
"languageNativeName": "Français",
"translationRatio": 0.9667952372966627
"translationRatio": 0.957351476933289
},
{
"languageCode": "ha_HG",
"languageName": "Hausa",
"languageNativeName": "هَوُسَ",
"translationRatio": 0.019285594499413028
"translationRatio": 0.019913707268503167
},
{
"languageCode": "he_IL",
"languageName": "Hebrew",
"languageNativeName": "עברית",
"translationRatio": 0.06037229582424952
"translationRatio": 0.060073016926651124
},
{
"languageCode": "hi_IN",
"languageName": "Hindi",
"languageNativeName": "हिन्दी",
"translationRatio": 0.24601710548381683
"translationRatio": 0.24344507135745108
},
{
"languageCode": "hu_HU",
"languageName": "Hungarian",
"languageNativeName": "magyar",
"translationRatio": 0.20576890826765049
"translationRatio": 0.203285761699303
},
{
"languageCode": "id_ID",
"languageName": "Indonesian",
"languageNativeName": "Bahasa Indonesia",
"translationRatio": 0.5410028509139695
"translationRatio": 0.5393295718552937
},
{
"languageCode": "ig_NG",
"languageName": "Igbo",
"languageNativeName": "Asụsụ Igbo",
"translationRatio": 0.013583766560456123
"translationRatio": 0.014271490209093907
},
{
"languageCode": "it_IT",
"languageName": "Italian",
"languageNativeName": "Italiano",
"translationRatio": 0.8624853261780983
"translationRatio": 0.8679057417855958
},
{
"languageCode": "ja_JP",
"languageName": "Japanese",
"languageNativeName": "日本語",
"translationRatio": 0.9776957907093745
"translationRatio": 0.9797543976103551
},
{
"languageCode": "ka_GE",
"languageName": "Georgian",
"languageNativeName": "ქართული",
"translationRatio": 0.013583766560456123
"translationRatio": 0.0552605376700962
},
{
"languageCode": "km_KH",
"languageName": "Khmer",
"languageNativeName": "ខេមរភាសា",
"translationRatio": 0.019956397786349167
"translationRatio": 0.020577497510786635
},
{
"languageCode": "ko_KR",
"languageName": "Korean",
"languageNativeName": "한국어",
"translationRatio": 0.6282072782156632
"translationRatio": 0.6226352472618653
},
{
"languageCode": "lt_LT",
"languageName": "Lithuanian",
"languageNativeName": "lietuvių kalba",
"translationRatio": 0.045782324333389246
"translationRatio": 0.04580152671755722
},
{
"languageCode": "lv_LV",
"languageName": "Latvian",
"languageNativeName": "latviešu valoda",
"translationRatio": 0.013583766560456123
"translationRatio": 0.014437437769664774
},
{
"languageCode": "mr_IN",
"languageName": "Marathi",
"languageNativeName": "मराठी",
"translationRatio": 0.03286936105986915
"translationRatio": 0.03335545967474274
},
{
"languageCode": "ms_MY",
"languageName": "Malay",
"languageNativeName": "Bahasa Malaysia",
"translationRatio": 0.0430991111856448
"translationRatio": 0.043146365748423454
},
{
"languageCode": "my_MM",
"languageName": "Burmese",
"languageNativeName": "ဗမာစာ",
"translationRatio": 0.014254569847392262
"translationRatio": 0.014935280451377375
},
{
"languageCode": "nl_NL",
"languageName": "Dutch",
"languageNativeName": "Nederlands",
"translationRatio": 0.3198054670467886
"translationRatio": 0.3157982077663458
},
{
"languageCode": "no_NO",
"languageName": "Norwegian",
"languageNativeName": "Norsk",
"translationRatio": 0.1801106825423444
"translationRatio": 0.1782276800531032
},
{
"languageCode": "pl_PL",
"languageName": "Polish",
"languageNativeName": "język polski",
"translationRatio": 0.778467214489351
"translationRatio": 0.7685031530036508
},
{
"languageCode": "pseudo_LOCALE",
"languageName": "for development only",
"languageNativeName": "Pseudolocalization",
"translationRatio": 0.9962756052141527
"translationRatio": 0.9963702359346642
},
{
"languageCode": "pt_BR",
"languageName": "Brazilian Portuguese",
"languageNativeName": "Português brasileiro",
"translationRatio": 0.9480127452624518
"translationRatio": 0.9624958513109857
},
{
"languageCode": "pt_PT",
"languageName": "Portuguese",
"languageNativeName": "Português",
"translationRatio": 0.44289787019956395
"translationRatio": 0.4372718221042151
},
{
"languageCode": "ro_RO",
"languageName": "Romanian",
"languageNativeName": "Română",
"translationRatio": 0.07764548046285424
"translationRatio": 0.0769996681048789
},
{
"languageCode": "ru_RU",
"languageName": "Russian",
"languageNativeName": "Русский",
"translationRatio": 0.9387892000670803
"translationRatio": 0.943245934284766
},
{
"languageCode": "si_LK",
"languageName": "Sinhala",
"languageNativeName": "සිංහල",
"translationRatio": 0.014086869025658255
"translationRatio": 0.014769332890806508
},
{
"languageCode": "sk_SK",
"languageName": "Slovak",
"languageNativeName": "slovenčina",
"translationRatio": 0.23662585946671144
"translationRatio": 0.23365416528377037
},
{
"languageCode": "sl_SI",
"languageName": "Slovene",
"languageNativeName": "slovenski jezik",
"translationRatio": 0.9473419419755157
"translationRatio": 0.9933620975771656
},
{
"languageCode": "sq_AL",
"languageName": "Albanian",
"languageNativeName": "Shqip",
"translationRatio": 0.15160154284756
"translationRatio": 0.2097577165615665
},
{
"languageCode": "sr_CS",
"languageName": "Serbian (Latin)",
"languageNativeName": "srpski",
"translationRatio": 0.21465705181955397
"translationRatio": 0.212744772651842
},
{
"languageCode": "sr_SP",
"languageName": "Serbian",
"languageNativeName": "српски језик",
"translationRatio": 0.4527922186818716
"translationRatio": 0.44739462329903745
},
{
"languageCode": "sv_SE",
"languageName": "Swedish",
"languageNativeName": "svenska",
"translationRatio": 0.19771926882441726
"translationRatio": 0.20129439097245272
},
{
"languageCode": "th_TH",
"languageName": "Thai",
"languageNativeName": "ไทย",
"translationRatio": 0.04980714405500586
"translationRatio": 0.05011616329239965
},
{
"languageCode": "tr_TR",
"languageName": "Turkish",
"languageNativeName": "Türkçe",
"translationRatio": 0.79674660405836
"translationRatio": 0.8151344175240625
},
{
"languageCode": "uk_UA",
"languageName": "Ukrainian",
"languageNativeName": "Українська",
"translationRatio": 0.8554418916652692
"translationRatio": 0.868901427149021
},
{
"languageCode": "ur_PK",
"languageName": "Urdu",
"languageNativeName": "اردو",
"translationRatio": 0.02163340600368946
"translationRatio": 0.022236973116495196
},
{
"languageCode": "uz_UZ",
"languageName": "Uzbek",
"languageNativeName": "Ўзбек",
"translationRatio": 0.02096260271675332
"translationRatio": 0.02140723531364086
},
{
"languageCode": "vi_VN",
"languageName": "Vietnamese",
"languageNativeName": "Tiếng Việt",
"translationRatio": 0.04930404158980384
"translationRatio": 0.05741785595751747
},
{
"languageCode": "yo_NG",
"languageName": "Yoruba",
"languageNativeName": "Yorùbá",
"translationRatio": 0.4237799765218849
"translationRatio": 0.4191835380019914
},
{
"languageCode": "zh_CN",
"languageName": "Chinese Simplified",
"languageNativeName": "简化字",
"translationRatio": 0.9926211638437028
"translationRatio": 0.9925323597743113
},
{
"languageCode": "zh_TW",
"languageName": "Chinese Traditional",
"languageNativeName": "正體字",
"translationRatio": 0.26899211806137846
"translationRatio": 0.2660139395950879
}
];

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

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