Compare commits

..

30 Commits

Author SHA1 Message Date
AlexandreSi
1f9d7860cf Improve message 2021-12-10 17:59:18 +01:00
AlexandreSi
83d6c7a2a2 Move warning to Hint file 2021-12-10 16:21:30 +01:00
AlexandreSi
f7b0cfab1f Add a more precise check on object having platformer behavior 2021-12-10 16:01:04 +01:00
AlexandreSi
7e6a277335 Add warning when editing the collision mask of a platformer object 2021-12-10 15:28:01 +01:00
Clément Pasteau
834a28ddbc Improve the whole export flow
* The Preview & Publish buttons are now centred in the interface
* The automated web upload with a provided link is put forward in the Export home dialog
* The export flow has been reworked to be split into "Automated" and "Manual'
2021-12-10 12:36:48 +01:00
AlexandreS
945555a8e9 Fix switch case condition in showcased game buttons
Don't show in changelog
2021-12-10 10:32:05 +00:00
AlexandreS
ad3d1dd8c3 Change homepage to display starters, tutorials and the game showcase
Additionally:
- Add button with split menu to open recent projects
- Remove starters tab in the dialog to create a project
2021-12-09 17:46:52 +01:00
Florian Rival
fd47282456 Improve resilience of resources fetching of the desktop app (#3342)
Do more requests at the same time but retry the failing ones.

Don't show in changelog
2021-12-08 09:54:20 +00:00
Florian Rival
dff1c88ef7 Increase parallel requests when fetching resources in the desktop app
Don't show in changelog
2021-12-07 22:35:27 +00:00
Florian Rival
4ea622ff99 Fix typo 2021-12-05 13:04:40 +00:00
Florian Rival
17ea918a91 Bump newIDE version 2021-12-03 11:23:32 +00:00
Leo_Red
cc6af8979d Animate opening of lists in the Project Manager, action/condition selector and in the Debugger (#3310) 2021-12-03 11:16:56 +00:00
github-actions[bot]
132e20fd24 Update translations [skip ci] (#3289)
Co-authored-by: 4ian <4ian@users.noreply.github.com>
2021-12-03 11:11:31 +00:00
Florian Rival
fb6a88785a Add Pandako to the list of contributors (Japanese translations, extensions and blog)
Don't show in changelog
2021-12-03 09:43:02 +00:00
Clément Pasteau
8a159d7ff5 Remove unused var
Do not show in changelog
2021-12-03 09:30:50 +00:00
Florian Rival
13c85bbe45 Improve custom behavior new function dialog to show a button for making an action, condition or expression instead of a single one for action
Don't show in changelog
2021-12-02 22:20:08 +00:00
Florian Rival
ce8323e8e1 Fix crash when modifying the operator for an action or condition of a "color" property of a behavior (#3327) 2021-12-02 15:54:00 +00:00
Clément Pasteau
dbc7a74e45 Fix debugger actions not hiding properly after opening it 2021-12-02 15:53:25 +01:00
Clément Pasteau
cfb1d6888e Fix sounds sometimes not playing after the first time being played (#3325) 2021-12-02 10:48:10 +00:00
Florian Rival
816dc8cc74 Fix tweens automatically deleting the object sometimes affecting newly created objects (#3321) 2021-12-02 09:40:14 +00:00
Arthur Pacaud
106549e5fa Allow usage of custom ICE servers in the P2P extension (#3301) 2021-12-01 20:08:25 +00:00
Clément Pasteau
f8ca06d530 Fix web debugger icon not updating properly
Do not show in changelog
2021-12-01 19:32:18 +01:00
AlexandreS
34cbcdbc3a Add carousel component for new start page (don't show in changelog) 2021-12-01 15:11:56 +01:00
Clément Pasteau
3b208502ae Improve DismissableTutorialMessage story
Do not show in changelog
2021-11-30 15:50:28 +01:00
Clément Pasteau
e3654fca99 improve tutorials to be fetched from backend services
Do not show in changelog
2021-11-30 14:43:57 +01:00
Florian Rival
2a386cdcf1 Fix the layer of the created object not shown for the "Create object" action in the events sheet 2021-11-29 23:43:50 +00:00
Leo_Red
b134896687 Fix wrong mention of extensions instead of examples in a text (#3306) 2021-11-29 13:16:34 +00:00
Leo_Red
705dff43bc Move scene variables into their own category in actions/conditions/expressions (#3300) 2021-11-27 16:33:31 +00:00
Leo_Red
d9eaf71ed1 Fix tabs in preference dialog disappearing when scrolling in the dialog (#3299) 2021-11-27 16:06:20 +00:00
D8H
008b4291ab Fix the "separate" action when there are several obstacles (#3236)
* Previously an object colliding with multiple objects would have been "pushed" too far from the two colliding objects. Now, it gets properly separated, in a much more natural way, avoiding some shaking/flickering on corners or when involving multiple obstacles.
2021-11-25 17:45:38 +00:00
163 changed files with 75200 additions and 16894 deletions

View File

@@ -1197,7 +1197,7 @@ void GD_CORE_API BuiltinExtensionsImplementer::ImplementsBaseObjectExtension(
.AddAction("Create",
_("Create an object"),
_("Create an object at specified position"),
_("Create object _PARAM1_ at position _PARAM2_;_PARAM3_"),
_("Create object _PARAM1_ at position _PARAM2_;_PARAM3_ (layer: _PARAM4_)"),
_("Objects"),
"res/actions/create24.png",
"res/actions/create.png")
@@ -1215,7 +1215,7 @@ void GD_CORE_API BuiltinExtensionsImplementer::ImplementsBaseObjectExtension(
_("Among the objects of the specified group, this action will "
"create the object with the specified name."),
_("Among objects _PARAM1_, create object named _PARAM2_ at "
"position _PARAM3_;_PARAM4_"),
"position _PARAM3_;_PARAM4_ (layer: _PARAM5_)"),
_("Objects"),
"res/actions/create24.png",
"res/actions/create.png")

View File

@@ -28,7 +28,7 @@ void GD_CORE_API BuiltinExtensionsImplementer::ImplementsVariablesExtension(
_("Value of a scene variable"),
_("Compare the value of a scene variable."),
_("the scene variable _PARAM0_"),
_("Variables"),
_("Variables/Scene variables"),
"res/conditions/var24.png",
"res/conditions/var.png")
.AddParameter("scenevar", _("Variable"))
@@ -39,7 +39,7 @@ void GD_CORE_API BuiltinExtensionsImplementer::ImplementsVariablesExtension(
_("Text of a scene variable"),
_("Compare the text of a scene variable."),
_("the text of scene variable _PARAM0_"),
_("Variables"),
_("Variables/Scene variables"),
"res/conditions/var24.png",
"res/conditions/var.png")
.AddParameter("scenevar", _("Variable"))
@@ -51,7 +51,7 @@ void GD_CORE_API BuiltinExtensionsImplementer::ImplementsVariablesExtension(
_("Boolean value of a scene variable"),
_("Compare the boolean value of a scene variable."),
_("The boolean value of scene variable _PARAM0_ is _PARAM1_"),
_("Variables"),
_("Variables/Scene variables"),
"res/conditions/var24.png",
"res/conditions/var.png")
.AddParameter("scenevar", _("Variable"))
@@ -64,7 +64,7 @@ void GD_CORE_API BuiltinExtensionsImplementer::ImplementsVariablesExtension(
_("Child existence"),
_("Check if the specified child of the scene variable exists."),
_("Child _PARAM1_ of scene variable _PARAM0_ exists"),
_("Variables/Collections/Structures"),
_("Variables/Scene variables/Collections/Structures"),
"res/conditions/var24.png",
"res/conditions/var.png")
.AddParameter("scenevar", _("Variable"))
@@ -89,7 +89,7 @@ void GD_CORE_API BuiltinExtensionsImplementer::ImplementsVariablesExtension(
"Test if a scene variable is defined",
"Test if the scene variable exists.",
"Scene variable _PARAM0_ is defined",
_("Variables"),
_("Variables/Scene variables"),
"res/conditions/var24.png",
"res/conditions/var.png")
.AddCodeOnlyParameter("currentScene", "")
@@ -151,7 +151,7 @@ void GD_CORE_API BuiltinExtensionsImplementer::ImplementsVariablesExtension(
_("Value of a scene variable"),
_("Change the value of a scene variable."),
_("the scene variable _PARAM0_"),
_("Variables"),
_("Variables/Scene variables"),
"res/actions/var24.png",
"res/actions/var.png")
.AddParameter("scenevar", _("Variable"))
@@ -162,7 +162,7 @@ void GD_CORE_API BuiltinExtensionsImplementer::ImplementsVariablesExtension(
_("String of a scene variable"),
_("Modify the text of a scene variable."),
_("the text of scene variable _PARAM0_"),
_("Variables"),
_("Variables/Scene variables"),
"res/actions/var24.png",
"res/actions/var.png")
.AddParameter("scenevar", _("Variable"))
@@ -174,7 +174,7 @@ void GD_CORE_API BuiltinExtensionsImplementer::ImplementsVariablesExtension(
_("Boolean value of a scene variable"),
_("Modify the boolean value of a scene variable."),
_("Set the boolean value of scene variable _PARAM0_ to _PARAM1_"),
_("Variables"),
_("Variables/Scene variables"),
"res/conditions/var24.png",
"res/conditions/var.png")
.AddParameter("scenevar", _("Variable"))
@@ -187,7 +187,7 @@ void GD_CORE_API BuiltinExtensionsImplementer::ImplementsVariablesExtension(
_("If it was true, it will become false, and if it was "
"false it will become true."),
_("Toggle the boolean value of scene variable _PARAM0_"),
_("Variables"),
_("Variables/Scene variables"),
"res/conditions/var24.png",
"res/conditions/var.png")
.AddParameter("scenevar", _("Variable"));
@@ -245,7 +245,7 @@ void GD_CORE_API BuiltinExtensionsImplementer::ImplementsVariablesExtension(
_("Remove a child"),
_("Remove a child from a scene variable."),
_("Remove child _PARAM1_ from scene variable _PARAM0_"),
_("Variables/Collections/Structures"),
_("Variables/Scene variables/Collections/Structures"),
"res/actions/var24.png",
"res/actions/var.png")
.AddParameter("scenevar", _("Variable"))
@@ -269,7 +269,7 @@ void GD_CORE_API BuiltinExtensionsImplementer::ImplementsVariablesExtension(
_("Clear scene variable"),
_("Remove all the children from the scene variable."),
_("Clear children from scene variable _PARAM0_"),
_("Variables/Collections"),
_("Variables/Scene variables/Collections"),
"res/actions/var24.png",
"res/actions/var.png")
.AddParameter("scenevar", _("Variable"))
@@ -291,7 +291,7 @@ void GD_CORE_API BuiltinExtensionsImplementer::ImplementsVariablesExtension(
_("Append variable to a scene array"),
_("Appends a variable at the end of a scene array variable."),
_("Append variable _PARAM1_ to array variable _PARAM0_"),
_("Variables/Collections/Arrays"),
_("Variables/Scene variables/Collections/Arrays"),
"res/actions/var24.png",
"res/actions/var.png")
.AddParameter("scenevar", _("Array variable"))
@@ -304,7 +304,7 @@ void GD_CORE_API BuiltinExtensionsImplementer::ImplementsVariablesExtension(
_("Append a string to a scene array"),
_("Appends a string at the end of a scene array variable."),
_("Append string _PARAM1_ to array variable _PARAM0_"),
_("Variables/Collections/Arrays"),
_("Variables/Scene variables/Collections/Arrays"),
"res/actions/var24.png",
"res/actions/var.png")
.AddParameter("scenevar", _("Array variable"))
@@ -316,7 +316,7 @@ void GD_CORE_API BuiltinExtensionsImplementer::ImplementsVariablesExtension(
_("Append a number to a scene array"),
_("Appends a number at the end of a scene array variable."),
_("Append number _PARAM1_ to array variable _PARAM0_"),
_("Variables/Collections/Arrays"),
_("Variables/Scene variables/Collections/Arrays"),
"res/actions/var24.png",
"res/actions/var.png")
.AddParameter("scenevar", _("Array variable"))
@@ -328,7 +328,7 @@ void GD_CORE_API BuiltinExtensionsImplementer::ImplementsVariablesExtension(
_("Append a boolean to a scene array"),
_("Appends a boolean at the end of a scene array variable."),
_("Append boolean _PARAM1_ to array variable _PARAM0_"),
_("Variables/Collections/Arrays"),
_("Variables/Scene variables/Collections/Arrays"),
"res/actions/var24.png",
"res/actions/var.png")
.AddParameter("scenevar", _("Array variable"))
@@ -341,7 +341,7 @@ void GD_CORE_API BuiltinExtensionsImplementer::ImplementsVariablesExtension(
_("Remove variable from a scene array (by index)"),
_("Removes a variable at the specified index of a scene array variable."),
_("Remove variable at index _PARAM1_ from scene array variable _PARAM0_"),
_("Variables/Collections/Arrays"),
_("Variables/Scene variables/Collections/Arrays"),
"res/actions/var24.png",
"res/actions/var.png")
.AddParameter("scenevar", _("Variable"))
@@ -414,7 +414,7 @@ void GD_CORE_API BuiltinExtensionsImplementer::ImplementsVariablesExtension(
.AddExpression("GlobalVariableChildCount",
_("Number of children of a global variable"),
_("Number of children of a global variable"),
_("Variables"),
_("Variables/Global variables"),
"res/actions/var.png")
.AddParameter("globalvar", _("Variable"));
@@ -422,7 +422,7 @@ void GD_CORE_API BuiltinExtensionsImplementer::ImplementsVariablesExtension(
.AddExpression("VariableChildCount",
_("Number of children of a scene variable"),
_("Number of children of a scene variable"),
_("Variables"),
_("Variables/Scene variables"),
"res/actions/var.png")
.AddParameter("scenevar", _("Variable"));
@@ -430,7 +430,7 @@ void GD_CORE_API BuiltinExtensionsImplementer::ImplementsVariablesExtension(
.AddExpression("Variable",
_("Value of a scene variable"),
_("Value of a scene variable"),
_("Variables"),
_("Variables/Scene variables"),
"res/actions/var.png")
.AddParameter("scenevar", _("Variable"));
@@ -438,7 +438,7 @@ void GD_CORE_API BuiltinExtensionsImplementer::ImplementsVariablesExtension(
.AddStrExpression("VariableString",
_("Text of a scene variable"),
_("Text of a scene variable"),
_("Variables"),
_("Variables/Scene variables"),
"res/actions/var.png")
.AddParameter("scenevar", _("Variable"));
@@ -446,7 +446,7 @@ void GD_CORE_API BuiltinExtensionsImplementer::ImplementsVariablesExtension(
.AddExpression("GlobalVariable",
_("Value of a global variable"),
_("Value of a global variable"),
_("Variables"),
_("Variables/Global variables"),
"res/actions/var.png")
.AddParameter("globalvar", _("Name of the global variable"));
@@ -454,7 +454,7 @@ void GD_CORE_API BuiltinExtensionsImplementer::ImplementsVariablesExtension(
.AddStrExpression("GlobalVariableString",
_("Text of a global variable"),
_("Text of a global variable"),
_("Variables"),
_("Variables/Global variables"),
"res/actions/var.png")
.AddParameter("globalvar", _("Variable"));
}

View File

@@ -69,3 +69,4 @@ var t=require("./bufferbuilder").BufferBuilder,e=require("./bufferbuilder").bina
},{"eventemitter3":"JJlS","./util":"BHXf","./logger":"WOs9","./socket":"wJlv","./mediaconnection":"dbHP","./dataconnection":"GBTQ","./enums":"ZRYf","./api":"in7L"}],"iTK6":[function(require,module,exports) {
"use strict";Object.defineProperty(exports,"__esModule",{value:!0});var e=require("./util"),r=require("./peer");exports.peerjs={Peer:r.Peer,util:e.util},exports.default=r.Peer,window.peerjs=exports.peerjs,window.Peer=r.Peer;
},{"./util":"BHXf","./peer":"Hxpd"}]},{},["iTK6"], null)
//# sourceMappingURL=A_peer.js.map

File diff suppressed because one or more lines are too long

View File

@@ -375,15 +375,14 @@ namespace gdjs {
key: string,
ssl: boolean
) => {
peerConfig = {
debug: 1,
Object.assign(peerConfig, {
host,
port,
path,
secure: ssl,
// All servers have "peerjs" as default key
key: key.length === 0 ? 'peerjs' : key,
};
});
loadPeerJS();
};
@@ -394,6 +393,26 @@ namespace gdjs {
*/
export const useDefaultBrokerServer = loadPeerJS;
/**
* Adds an ICE server candidate, and removes the default ones provided by PeerJs. Must be called before connecting to a broker.
* @param urls The URL of the STUN/TURN server.
* @param username An optional username to send to the server.
* @param credential An optional password to send to the server.
*/
export const useCustomICECandidate = (
urls: string,
username?: string,
credential?: string
) => {
peerConfig.config = peerConfig.config || {};
peerConfig.config.iceServers = peerConfig.config.iceServers || [];
peerConfig.config.iceServers.push({
urls,
username,
credential,
});
};
/**
* Overrides the default peer ID. Must be called before connecting to a broker.
* Overriding the ID may have unwanted consequences. Do not use this feature

View File

@@ -154,6 +154,30 @@ module.exports = {
.addIncludeFile('Extensions/P2P/B_p2ptools.js')
.setFunctionName('gdjs.evtTools.p2p.useCustomBrokerServer');
extension
.addAction(
'UseOwnICEServer',
_('Use a custom ICE server'),
_(
'Disables the default ICE (STUN or TURN) servers list and use one of your own. ' +
'Note that it is recommended to add at least 1 self-hosted STUN and TURN server ' +
'for games that are not over LAN but over the internet. ' +
'This action can be used multiple times to add multiple servers. ' +
'This action needs to be called BEFORE connecting to the broker server.'
),
_('Use ICE server _PARAM0_ (username: _PARAM1_, password: _PARAM2_)'),
_('P2P (experimental)'),
'JsPlatform/Extensions/p2picon.svg',
'JsPlatform/Extensions/p2picon.svg'
)
.addParameter('string', _('URL to the ICE server'), '', false)
.addParameter('string', _('(Optional) Username'), '', true)
.addParameter('string', _('(Optional) Password'), '', true)
.getCodeExtraInformation()
.setIncludeFile('Extensions/P2P/A_peer.js')
.addIncludeFile('Extensions/P2P/B_p2ptools.js')
.setFunctionName('gdjs.evtTools.p2p.useCustomICECandidate');
extension
.addAction(
'UseDefaultBroker',

View File

@@ -107,6 +107,19 @@ namespace gdjs {
return true;
}
onDestroy() {
const shiftyJsScene = this._runtimeScene.shiftyJsScene;
if (!shiftyJsScene) return;
// Stop and delete all tweens of the behavior - otherwise they could:
// - continue to point to the behavior, and so to the object (memory leak),
// - affect the object in case it's recycled (wrong/hard to debug behavior).
for (const identifier in this._tweens) {
this._tweens[identifier].instance.stop();
shiftyJsScene.remove(this._tweens[identifier].instance);
}
}
private _addTween(
identifier: string,
easingValue: string,

View File

@@ -618,6 +618,7 @@ namespace gdjs {
loop,
pitch
);
this._storeSoundInArray(this._freeSounds, sound);
sound.once('play', () => {
if (this._paused) {
sound.pause();
@@ -625,7 +626,6 @@ namespace gdjs {
}
});
sound.play();
this._storeSoundInArray(this._freeSounds, sound);
}
playSoundOnChannel(
@@ -644,6 +644,7 @@ namespace gdjs {
loop,
pitch
);
this._sounds[channel] = sound;
sound.once('play', () => {
if (this._paused) {
sound.pause();
@@ -651,7 +652,6 @@ namespace gdjs {
}
});
sound.play();
this._sounds[channel] = sound;
}
getSoundOnChannel(channel: integer): HowlerSound {
@@ -666,6 +666,7 @@ namespace gdjs {
loop,
pitch
);
this._storeSoundInArray(this._freeMusics, music);
music.once('play', () => {
if (this._paused) {
music.pause();
@@ -673,7 +674,6 @@ namespace gdjs {
}
});
music.play();
this._storeSoundInArray(this._freeMusics, music);
}
playMusicOnChannel(
@@ -692,6 +692,7 @@ namespace gdjs {
loop,
pitch
);
this._musics[channel] = music;
music.once('play', () => {
if (this._paused) {
music.pause();
@@ -699,7 +700,6 @@ namespace gdjs {
}
});
music.play();
this._musics[channel] = music;
}
getMusicOnChannel(channel: integer): HowlerSound {

View File

@@ -27,6 +27,112 @@ namespace gdjs {
return Math.pow(radiusX, 2) + Math.pow(radiusY, 2);
};
/**
* Arrays and data structure that are (re)used by
* {@link RuntimeObject.separateFromObjects} to avoid any allocation.
*/
const separateFromObjectsStatics: {
moveXArray: Array<float>;
moveYArray: Array<float>;
} = {
moveXArray: [],
moveYArray: [],
};
/**
* Move the object using the results from collisionTest call.
* This moves the object according to the direction of the longest vector,
* and projects the others on the orthogonal vector.
*
* See {@link RuntimeObject.separateFromObjects}
*
* @param object The object to move.
* @param moveXArray The X coordinates of the vectors to move the object.
* @param moveYArray The Y coordinates of the vectors to move the object.
* @return true if the object was moved.
*/
const moveFollowingSeparatingVectors = (
object: gdjs.RuntimeObject,
moveXArray: Array<float>,
moveYArray: Array<float>
): boolean => {
if (moveXArray.length === 0) {
moveXArray.length = 0;
moveYArray.length = 0;
return false;
}
if (moveXArray.length === 1) {
// Move according to the results returned by the collision algorithm.
object.setPosition(
object.getX() + moveXArray[0],
object.getY() + moveYArray[0]
);
moveXArray.length = 0;
moveYArray.length = 0;
return true;
}
// Find the longest vector
let squaredDistanceMax = 0;
let distanceMaxIndex = 0;
for (let index = 0; index < moveXArray.length; index++) {
const moveX = moveXArray[index];
const moveY = moveYArray[index];
const squaredDistance = moveX * moveX + moveY * moveY;
if (squaredDistance > squaredDistanceMax) {
squaredDistanceMax = squaredDistance;
distanceMaxIndex = index;
}
}
const distanceMax = Math.sqrt(squaredDistanceMax);
// unit vector of the longest vector
const uX = moveXArray[distanceMaxIndex] / distanceMax;
const uY = moveYArray[distanceMaxIndex] / distanceMax;
// normal vector of the longest vector
const vX = -uY;
const vY = uX;
// Project other vectors on the normal
let scalarProductMin = 0;
let scalarProductMax = 0;
for (let index = 0; index < moveXArray.length; index++) {
const moveX = moveXArray[index];
const moveY = moveYArray[index];
const scalarProduct = moveX * vX + moveY * vY;
scalarProductMin = Math.min(scalarProductMin, scalarProduct);
scalarProductMax = Math.max(scalarProductMax, scalarProduct);
}
// Apply the longest vector
let deltaX = moveXArray[distanceMaxIndex];
let deltaY = moveYArray[distanceMaxIndex];
// Apply the longest projected vector if they all are in the same direction
// Some projections could have rounding errors,
// they are considered negligible under a 1 for 1,000,000 ratio.
const scalarProductMinIsNegligible =
-scalarProductMin < scalarProductMax / 1048576;
const scalarProductMaxIsNegligible =
scalarProductMax < -scalarProductMin / 1048576;
if (scalarProductMinIsNegligible !== scalarProductMaxIsNegligible) {
if (scalarProductMaxIsNegligible) {
deltaX += scalarProductMin * vX;
deltaY += scalarProductMin * vY;
} else {
deltaX += scalarProductMax * vX;
deltaY += scalarProductMax * vY;
}
}
object.setPosition(object.getX() + deltaX, object.getY() + deltaY);
moveXArray.length = 0;
moveYArray.length = 0;
return true;
};
/**
* RuntimeObject represents an object being used on a RuntimeScene.
*
@@ -1647,35 +1753,34 @@ namespace gdjs {
objects: RuntimeObject[],
ignoreTouchingEdges: boolean
): boolean {
let moved = false;
let xMove = 0;
let yMove = 0;
let moveXArray: Array<float> = separateFromObjectsStatics.moveXArray;
let moveYArray: Array<float> = separateFromObjectsStatics.moveYArray;
moveXArray.length = 0;
moveYArray.length = 0;
const hitBoxes = this.getHitBoxes();
//Check if their is a collision with each object
for (let i = 0, len = objects.length; i < len; ++i) {
if (objects[i].id != this.id) {
const otherHitBoxes = objects[i].getHitBoxes();
for (let k = 0, lenk = hitBoxes.length; k < lenk; ++k) {
for (let l = 0, lenl = otherHitBoxes.length; l < lenl; ++l) {
const result = gdjs.Polygon.collisionTest(
hitBoxes[k],
otherHitBoxes[l],
ignoreTouchingEdges
);
if (result.collision) {
xMove += result.move_axis[0];
yMove += result.move_axis[1];
moved = true;
}
// Check if their is a collision with each object
for (const otherObject of objects) {
if (otherObject.id === this.id) {
continue;
}
const otherHitBoxes = otherObject.getHitBoxes();
for (const hitBox of hitBoxes) {
for (const otherHitBox of otherHitBoxes) {
const result = gdjs.Polygon.collisionTest(
hitBox,
otherHitBox,
ignoreTouchingEdges
);
if (result.collision) {
moveXArray.push(result.move_axis[0]);
moveYArray.push(result.move_axis[1]);
}
}
}
}
//Move according to the results returned by the collision algorithm.
this.setPosition(this.getX() + xMove, this.getY() + yMove);
return moved;
return moveFollowingSeparatingVectors(this, moveXArray, moveYArray);
}
/**
@@ -1688,40 +1793,40 @@ namespace gdjs {
objectsLists: ObjectsLists,
ignoreTouchingEdges: boolean
): boolean {
let moved = false;
let xMove = 0;
let yMove = 0;
let moveXArray: Array<float> = separateFromObjectsStatics.moveXArray;
let moveYArray: Array<float> = separateFromObjectsStatics.moveYArray;
moveXArray.length = 0;
moveYArray.length = 0;
const hitBoxes = this.getHitBoxes();
for (const name in objectsLists.items) {
if (objectsLists.items.hasOwnProperty(name)) {
const objects = objectsLists.items[name];
const otherObjects = objectsLists.items[name];
//Check if their is a collision with each object
for (let i = 0, len = objects.length; i < len; ++i) {
if (objects[i].id != this.id) {
const otherHitBoxes = objects[i].getHitBoxes();
for (let k = 0, lenk = hitBoxes.length; k < lenk; ++k) {
for (let l = 0, lenl = otherHitBoxes.length; l < lenl; ++l) {
const result = gdjs.Polygon.collisionTest(
hitBoxes[k],
otherHitBoxes[l],
ignoreTouchingEdges
);
if (result.collision) {
xMove += result.move_axis[0];
yMove += result.move_axis[1];
moved = true;
}
// Check if their is a collision with each object
for (const otherObject of otherObjects) {
if (otherObject.id === this.id) {
continue;
}
const otherHitBoxes = otherObject.getHitBoxes();
for (const hitBox of hitBoxes) {
for (const otherHitBox of otherHitBoxes) {
const result = gdjs.Polygon.collisionTest(
hitBox,
otherHitBox,
ignoreTouchingEdges
);
if (result.collision) {
moveXArray.push(result.move_axis[0]);
moveYArray.push(result.move_axis[1]);
}
}
}
}
}
}
//Move according to the results returned by the collision algorithm.
this.setPosition(this.getX() + xMove, this.getY() + yMove);
return moved;
return moveFollowingSeparatingVectors(this, moveXArray, moveYArray);
}
/**

3896
GDJS/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,117 @@
// @ts-check
/**
* Common tests for gdjs game engine.
* See README.md for more information.
*/
describe('gdjs.RuntimeObject.separateFromObjects', () => {
const runtimeGame = new gdjs.RuntimeGame({
variables: [],
// @ts-ignore TODO: make a function to create an empty game and use it across tests.
properties: { windowWidth: 800, windowHeight: 600 },
resources: { resources: [] },
});
const runtimeScene = new gdjs.RuntimeScene(runtimeGame);
const object = new gdjs.TestRuntimeObject(runtimeScene, {
name: 'obj1',
type: '',
variables: [],
behaviors: [],
effects: [],
});
object.setCustomWidthAndHeight(100, 100);
object.setCustomCenter(0, 0);
const obstacle1 = new gdjs.TestRuntimeObject(runtimeScene, {
name: 'obj1',
type: '',
variables: [],
behaviors: [],
effects: [],
});
obstacle1.setCustomWidthAndHeight(100, 100);
obstacle1.setCustomCenter(0, 0);
const obstacle2 = new gdjs.TestRuntimeObject(runtimeScene, {
name: 'obj1',
type: '',
variables: [],
behaviors: [],
effects: [],
});
obstacle2.setCustomWidthAndHeight(100, 100);
obstacle2.setCustomCenter(0, 0);
it('can be separated from 2 aligned objects', () => {
object.setPosition(200, 300);
// 2 obstacles on the left
obstacle1.setPosition(290, 250);
obstacle2.setPosition(290, 350);
expect(object.separateFromObjects([obstacle1, obstacle2], true)).to.be(
true
);
expect(object.getX()).to.be(190);
expect(object.getY()).to.be(300);
});
it('can be separated from 2 not exactly aligned objects', () => {
object.setPosition(200, 300);
// 2 obstacles on the left
obstacle1.setPosition(290, 250);
obstacle2.setPosition(295, 350);
expect(object.separateFromObjects([obstacle1, obstacle2], true)).to.be(
true
);
expect(object.getX()).to.be(190);
expect(object.getY()).to.be(300);
});
it('can be separated from 2 objects that form a corner', () => {
object.setPosition(200, 300);
// 1 obstacle on the top
obstacle1.setPosition(250, 220);
// 1 obstacle on the left
obstacle2.setPosition(290, 250);
expect(object.separateFromObjects([obstacle1, obstacle2], true)).to.be(
true
);
expect(object.getX()).to.be(190);
expect(object.getY()).to.be(320);
});
it('can be separated from 2 rotated objects', () => {
object.setPosition(200, 300);
// 1 obstacle on the top left corner
obstacle1.setPosition(250, 280);
obstacle1.setAngle(-45);
// 1 obstacle on the bottom left corner
obstacle2.setPosition(250, 420);
obstacle2.setAngle(-45);
expect(object.separateFromObjects([obstacle1, obstacle2], true)).to.be(
true
);
expect(object.getX()).to.be(170);
expect(object.getY()).to.be(300);
});
it('can be separated from 2 aligned objects when everything is rotated', () => {
object.setPosition(240, 350);
object.setAngle(-45);
// 2 obstacles on the top left
obstacle1.setPosition(250, 250);
obstacle1.setAngle(-45);
obstacle2.setPosition(330, 330);
obstacle1.setAngle(-45);
expect(object.separateFromObjects([obstacle1, obstacle2], true)).to.be(
true
);
expect(object.getX()).to.be.within(224, 225);
expect(object.getY()).to.be.within(365, 366);
});
});

1
newIDE/app/.env Normal file
View File

@@ -0,0 +1 @@
EXTEND_ESLINT = true

View File

@@ -16,7 +16,6 @@ build
# misc
.DS_Store
.env
.env.local
npm-debug.log

View File

@@ -1,5 +1,7 @@
// @flow
import { configure, addDecorator } from '@storybook/react';
import i18nProviderDecorator from '../src/stories/I18nProviderDecorator';
import '../src/UI/icomoon-font.css'; // Styles for Icomoon font.
export const globalTypes = {
themeName: {

File diff suppressed because it is too large Load Diff

View File

@@ -107,7 +107,16 @@
"import-zipped-external-editors": "cd scripts && node import-zipped-editor.js piskel 5.0.0-beta82 b8e4d57b160ff93d3680168cd271af795412ea6c4c0da321aee2946345c7fb75 && node import-zipped-editor.js jfxr 5.0.0-beta55 8ac12b557c2ddba958c6f0d3e0c5df8cf3369a65262dcb90cf5c8a7a7d20bdf6 && node import-zipped-editor.js yarn 5.0.0-beta103 155f6d074dbb025b082ede0f9b6acd55ed293457441f4c55f084c2d27fbda61d"
},
"eslintConfig": {
"extends": "react-app"
"extends": "react-app",
"rules": {
"no-restricted-imports": ["error", {
"paths": [{
"name": "@lingui/react",
"importNames": ["Trans"],
"message": "Please import Trans from @lingui/macro"
}]
}]
}
},
"flow-coverage-report": {
"includeGlob": [

Binary file not shown.

After

Width:  |  Height:  |  Size: 787 B

View File

@@ -116,12 +116,12 @@ editorHasCorrectHash().then(({ isHashCorrect }) => {
);
}
},
() => {
(e) => {
shell.echo(
`❌ Can't download ` +
editor +
`-editor.zip (${
response.statusMessage
e
}), please check your internet connection`
);
shell.exit(1);

View File

@@ -24,7 +24,7 @@ import { ExampleIcon } from './ExampleIcon';
import RaisedButtonWithSplitMenu from '../../UI/RaisedButtonWithSplitMenu';
import Window from '../../Utils/Window';
import optionalRequire from '../../Utils/OptionalRequire';
import { UserPublicProfileChip } from '../../UI/UserPublicProfileChip';
import { UserPublicProfileChip } from '../../UI/User/UserPublicProfileChip';
const electron = optionalRequire('electron');
@@ -121,8 +121,7 @@ export function ExampleDialog({
<AlertMessage kind="error">
<Trans>
Unfortunately, this example requires a newer version of GDevelop
to work. Upgrade GDevelop to be able to use this extension in your
project.
to work. Update GDevelop to be able to open this example.
</Trans>
</AlertMessage>
)}

View File

@@ -25,8 +25,12 @@ export const ExampleIcon = ({ exampleShortHeader, size }: Props) => {
return (
<div style={styles.iconBackground}>
<CorsAwareImage
style={{ ...styles.icon, width: size, height: size }}
src={exampleShortHeader.previewImageUrls[0]}
style={{ ...styles.icon, height: size }}
src={
exampleShortHeader.previewImageUrls.find(url =>
url.endsWith('thumbnail.png')
) || exampleShortHeader.previewImageUrls[0]
}
alt={exampleShortHeader.name}
/>
</div>

View File

@@ -17,7 +17,7 @@ import { ExampleIcon } from './ExampleIcon';
import optionalRequire from '../../Utils/OptionalRequire';
import { showErrorBox } from '../../UI/Messages/MessageBox';
import { openExampleInWebApp } from './ExampleDialog';
import { UserPublicProfileChip } from '../../UI/UserPublicProfileChip';
import { UserPublicProfileChip } from '../../UI/User/UserPublicProfileChip';
const electron = optionalRequire('electron');

View File

@@ -15,6 +15,7 @@ type ExampleStoreState = {|
filters: ?Filters,
searchResults: ?Array<ExampleShortHeader>,
fetchExamplesAndFilters: () => void,
allExamples: ?Array<ExampleShortHeader>,
error: ?Error,
searchText: string,
setSearchText: string => void,
@@ -25,6 +26,7 @@ export const ExampleStoreContext = React.createContext<ExampleStoreState>({
filters: null,
searchResults: null,
fetchExamplesAndFilters: () => {},
allExamples: null,
error: null,
searchText: '',
setSearchText: () => {},
@@ -62,6 +64,11 @@ export const ExampleStoreStateProvider = ({
}>(null);
const [filters, setFilters] = React.useState<?Filters>(null);
const [error, setError] = React.useState<?Error>(null);
const [
allExamples,
setAllExamples,
] = React.useState<?Array<ExampleShortHeader>>(null);
const isLoading = React.useRef<boolean>(false);
const [searchText, setSearchText] = React.useState(defaultSearchText);
@@ -80,6 +87,7 @@ export const ExampleStoreStateProvider = ({
try {
const allExamples: AllExamples = await listAllExamples();
const { exampleShortHeaders, filters } = allExamples;
setAllExamples(exampleShortHeaders);
const exampleShortHeadersById = {};
exampleShortHeaders.forEach(exampleShortHeader => {
@@ -135,6 +143,7 @@ export const ExampleStoreStateProvider = ({
() => ({
searchResults,
fetchExamplesAndFilters,
allExamples,
filters,
error,
searchText,
@@ -143,6 +152,7 @@ export const ExampleStoreStateProvider = ({
}),
[
searchResults,
allExamples,
error,
filters,
searchText,

View File

@@ -20,7 +20,7 @@ import { Column, Line } from '../../UI/Grid';
import { Divider } from '@material-ui/core';
import { ColumnStackLayout } from '../../UI/Layout';
import { IconContainer } from '../../UI/IconContainer';
import { UserPublicProfileChip } from '../../UI/UserPublicProfileChip';
import { UserPublicProfileChip } from '../../UI/User/UserPublicProfileChip';
type Props = {|
extensionShortHeader: ExtensionShortHeader,
@@ -128,7 +128,7 @@ export default class ExtensionInstallDialog extends Component<Props, State> {
<AlertMessage kind="error">
<Trans>
Unfortunately, this extension requires a newer version of
GDevelop to work. Upgrade GDevelop to be able to use this
GDevelop to work. Update GDevelop to be able to use this
extension in your project.
</Trans>
</AlertMessage>

View File

@@ -6,7 +6,7 @@ import Text from '../../UI/Text';
import { Trans } from '@lingui/macro';
import { Column, Line } from '../../UI/Grid';
import { IconContainer } from '../../UI/IconContainer';
import { UserPublicProfileChip } from '../../UI/UserPublicProfileChip';
import { UserPublicProfileChip } from '../../UI/User/UserPublicProfileChip';
const styles = {
container: {

View File

@@ -9,7 +9,7 @@ import { ExtensionStore } from '.';
import EventsFunctionsExtensionsContext from '../../EventsFunctionsExtensionsLoader/EventsFunctionsExtensionsContext';
import HelpButton from '../../UI/HelpButton';
import { importExtension, installExtension } from './InstallExtension';
import InfoBar from '../../UI/Messages/InfoBar';
import DismissableInfoBar from '../../UI/Messages/DismissableInfoBar';
import { type ExtensionShortHeader } from '../../Utils/GDevelopServices/Extension';
import AuthenticatedUserContext from '../../Profile/AuthenticatedUserContext';
import {
@@ -112,7 +112,7 @@ export default function ExtensionsSearchDialog({
project={project}
showOnlyWithBehaviors={false}
/>
<InfoBar
<DismissableInfoBar
identifier="extension-installed-explanation"
message={
<Trans>

View File

@@ -15,7 +15,7 @@ import {
import HelpButton from '../UI/HelpButton';
import { getExperimentalObjects } from '../Hints';
import { Line, Column } from '../UI/Grid';
import InfoBar from '../UI/Messages/InfoBar';
import DismissableInfoBar from '../UI/Messages/DismissableInfoBar';
import { Tabs, Tab } from '../UI/Tabs';
import { AssetStore } from '.';
import { type AssetShortHeader } from '../Utils/GDevelopServices/Asset';
@@ -291,7 +291,7 @@ export default function NewObjectDialog({
/>
) : null}
{resourcesFetcher.renderResourceFetcherDialog()}
<InfoBar
<DismissableInfoBar
identifier="asset-installed-explanation"
message={
<Trans>

View File

@@ -29,7 +29,7 @@ import { ExtensionStore } from '../AssetStore/ExtensionStore';
import Window from '../Utils/Window';
import EventsFunctionsExtensionsContext from '../EventsFunctionsExtensionsLoader/EventsFunctionsExtensionsContext';
import { installExtension } from '../AssetStore/ExtensionStore/InstallExtension';
import InfoBar from '../UI/Messages/InfoBar';
import DismissableInfoBar from '../UI/Messages/DismissableInfoBar';
import ScrollView, { type ScrollViewInterface } from '../UI/ScrollView';
const styles = {
@@ -286,7 +286,7 @@ export default function NewBehaviorDialog({
/>
)}
</Column>
<InfoBar
<DismissableInfoBar
identifier="extension-installed-explanation"
message={
<Trans>

View File

@@ -20,7 +20,6 @@ import {
type ChooseResourceFunction,
} from '../ResourcesList/ResourceSource';
import { type ResourceExternalEditor } from '../ResourcesList/ResourceExternalEditor.flow';
import { getBehaviorTutorialHints } from '../Hints';
import DismissableTutorialMessage from '../Hints/DismissableTutorialMessage';
import { ColumnStackLayout } from '../UI/Layout';
import useForceUpdate from '../Utils/UseForceUpdate';
@@ -35,6 +34,7 @@ import {
TRIVIAL_FIRST_BEHAVIOR,
} from '../Utils/GDevelopServices/Badge';
import AuthenticatedUserContext from '../Profile/AuthenticatedUserContext';
import { getBehaviorTutorialIds } from '../Utils/GDevelopServices/Tutorial';
const gd: libGDevelop = global.gd;
@@ -194,9 +194,9 @@ const BehaviorsEditor = (props: Props) => {
const BehaviorComponent = BehaviorsEditorService.getEditor(
behaviorTypeName
);
const tutorialHints = getBehaviorTutorialHints(behaviorTypeName);
const enabledTutorialHints = tutorialHints.filter(
hint => !values.hiddenTutorialHints[hint.identifier]
const tutorialIds = getBehaviorTutorialIds(behaviorTypeName);
const enabledTutorialIds = tutorialIds.filter(
tutorialId => !values.hiddenTutorialHints[tutorialId]
);
const iconUrl = behaviorMetadata.getIconFilename();
@@ -248,13 +248,13 @@ const BehaviorsEditor = (props: Props) => {
// Avoid Physics2 behavior overflow on small screens
noOverflowParent
>
{enabledTutorialHints.length ? (
{enabledTutorialIds.length ? (
<Line>
<ColumnStackLayout expand>
{tutorialHints.map(tutorialHint => (
{tutorialIds.map(tutorialId => (
<DismissableTutorialMessage
key={tutorialHint.identifier}
tutorialHint={tutorialHint}
key={tutorialId}
tutorialId={tutorialId}
/>
))}
</ColumnStackLayout>

View File

@@ -5,16 +5,19 @@ import Window from './Utils/Window';
import ExportDialog from './Export/ExportDialog';
import CreateProjectDialog from './ProjectCreation/CreateProjectDialog';
import Authentication from './Utils/GDevelopServices/Authentication';
import './UI/iconmoon-font.css'; // Styles for Iconmoon font.
import './UI/icomoon-font.css'; // Styles for Icomoon font.
// Import for browser only IDE
import BrowserExamples from './ProjectCreation/BrowserExamples';
import BrowserStarters from './ProjectCreation/BrowserStarters';
import BrowserIntroDialog from './MainFrame/BrowserIntroDialog';
import browserResourceSources from './ResourcesList/BrowserResourceSources';
import browserResourceExternalEditors from './ResourcesList/BrowserResourceExternalEditors';
import BrowserS3PreviewLauncher from './Export/BrowserExporters/BrowserS3PreviewLauncher';
import { getBrowserExporters } from './Export/BrowserExporters';
import {
browserAutomatedExporters,
browserManualExporters,
browserOnlineWebExporter,
} from './Export/BrowserExporters';
import makeExtensionsLoader from './JsExtensionsLoader/BrowserJsExtensionsLoader';
import ObjectsEditorService from './ObjectEditor/ObjectsEditorService';
import ObjectsRenderingService from './ObjectsRendering/ObjectsRenderingService';
@@ -27,6 +30,7 @@ import DownloadFileStorageProvider from './ProjectsStorage/DownloadFileStoragePr
import DropboxStorageProvider from './ProjectsStorage/DropboxStorageProvider';
import OneDriveStorageProvider from './ProjectsStorage/OneDriveStorageProvider';
import { BrowserResourceFetcher } from './ProjectsStorage/ResourceFetcher/BrowserResourceFetcher';
import { onCreateFromExampleShortHeader, onCreateBlank } from './ProjectCreation/services/BrowserCreation';
export const create = (authentication: Authentication) => {
Window.setUpContextMenu();
@@ -68,8 +72,12 @@ export const create = (authentication: Authentication) => {
)}
renderExportDialog={props => (
<ExportDialog
{...props}
exporters={getBrowserExporters()}
project={props.project}
onChangeSubscription={props.onChangeSubscription}
onClose={props.onClose}
automatedExporters={browserAutomatedExporters}
manualExporters={browserManualExporters}
onlineWebExporter={browserOnlineWebExporter}
allExportersRequireOnline
/>
)}
@@ -77,11 +85,13 @@ export const create = (authentication: Authentication) => {
<CreateProjectDialog
{...props}
examplesComponent={BrowserExamples}
startersComponent={BrowserStarters}
onCreateFromExampleShortHeader={onCreateFromExampleShortHeader}
/>
)}
introDialog={<BrowserIntroDialog />}
storageProviders={storageProviders}
onCreateFromExampleShortHeader={onCreateFromExampleShortHeader}
onCreateBlank={onCreateBlank}
getStorageProviderOperations={getStorageProviderOperations}
getStorageProvider={getStorageProvider}
resourceSources={browserResourceSources}

View File

@@ -9,7 +9,7 @@ export type CommandName =
| 'LAUNCH_DEBUG_PREVIEW'
| 'LAUNCH_NETWORK_PREVIEW'
| 'HOT_RELOAD_PREVIEW'
| 'OPEN_START_PAGE'
| 'OPEN_HOME_PAGE'
| 'CREATE_NEW_PROJECT'
| 'OPEN_PROJECT'
| 'SAVE_PROJECT'
@@ -112,7 +112,7 @@ const commandsList: { [CommandName]: CommandMetadata } = {
area: 'PROJECT',
displayText: t`Apply changes to the running preview`,
},
OPEN_START_PAGE: { area: 'IDE', displayText: t`Open start page` },
OPEN_HOME_PAGE: { area: 'IDE', displayText: t`Show Home` },
CREATE_NEW_PROJECT: {
area: 'GENERAL',
displayText: t`Create a new project`,

View File

@@ -41,7 +41,6 @@ const isUnavoidableLibraryWarning = ({ group, message }: Log): boolean =>
type Props = {|
project: gdProject,
setToolbar: React.Node => void,
isActive: boolean,
previewDebuggerServer: PreviewDebuggerServer,
|};
@@ -79,8 +78,6 @@ export default class Debugger extends React.Component<Props, State> {
_debuggerLogs: Map<number, LogsManager> = new Map();
updateToolbar() {
if (!this.props.isActive) return;
this.props.setToolbar(
<Toolbar
onPlay={() => this._play(this.state.selectedId)}
@@ -102,15 +99,7 @@ export default class Debugger extends React.Component<Props, State> {
}
componentDidMount() {
if (this.props.isActive) {
this._registerServerCallbacks();
}
}
componentWillReceiveProps(nextProps: Props) {
if (nextProps.isActive && !this.props.isActive) {
this._registerServerCallbacks();
}
this._registerServerCallbacks();
}
componentWillUnmount() {

View File

@@ -14,6 +14,9 @@ import Destroy from '../UI/CustomSvgIcons/Behaviors/Destroy';
import Function from '../UI/CustomSvgIcons/Behaviors/Function';
import Activate from '../UI/CustomSvgIcons/Behaviors/Activate';
import Deactivate from '../UI/CustomSvgIcons/Behaviors/Deactivate';
import { Line } from '../UI/Grid';
import Visibility from '@material-ui/icons/Visibility';
import VisibilityOff from '@material-ui/icons/VisibilityOff';
const gd: libGDevelop = global.gd;
type Props = {|
@@ -21,7 +24,6 @@ type Props = {|
onCancel: () => void,
onChoose: (parameters: EventsFunctionCreationParameters) => void,
|};
type State = {||};
const styles = {
icon: { width: 40, height: 40 },
@@ -59,151 +61,195 @@ const MethodListItem = ({
);
};
export default class BehaviorMethodSelectorDialog extends React.Component<
Props,
State
> {
render() {
const actions = [
<FlatButton
label={<Trans>Cancel</Trans>}
keyboardFocused={true}
onClick={() => this.props.onCancel()}
key={'close'}
/>,
];
export default function BehaviorMethodSelectorDialog({
eventsBasedBehavior,
onChoose,
onCancel,
}: Props) {
const eventsFunctions = eventsBasedBehavior.getEventsFunctions();
const [showAdvanced, setShowAdvanced] = React.useState(false);
const { eventsBasedBehavior, onChoose } = this.props;
const eventsFunctions = eventsBasedBehavior.getEventsFunctions();
return (
<Dialog
secondaryActions={[
<HelpButton
key="help"
helpPagePath="/behaviors/events-based-behaviors"
/>,
]}
actions={actions}
cannotBeDismissed={false}
open
noMargin
title={<Trans>Choose a new behavior function ("method")</Trans>}
onRequestClose={this.props.onCancel}
>
<List>
<Subheader>
<Trans>Main lifecycle methods</Trans>
</Subheader>
<MethodListItem
icon={<Create style={styles.icon} />}
name={'onCreated'}
disabled={eventsFunctions.hasEventsFunctionNamed('onCreated')}
onChoose={onChoose}
description={
<Trans>
Events that will be run once, when an object is created with
this behavior being attached to it.
</Trans>
}
return (
<Dialog
secondaryActions={[
<HelpButton
key="help"
helpPagePath="/behaviors/events-based-behaviors"
/>,
]}
actions={[
<FlatButton
label={<Trans>Cancel</Trans>}
keyboardFocused={true}
onClick={onCancel}
key={'close'}
/>,
]}
cannotBeDismissed={false}
open
noMargin
title={<Trans>Choose a new behavior function ("method")</Trans>}
onRequestClose={onCancel}
>
<List>
<ListItem
leftIcon={<Function style={styles.icon} />}
primaryText={<Trans>Action</Trans>}
secondaryText={
<Trans>
An action that can be used on objects with the behavior. You can
define the action parameters: objects, texts, numbers, layers,
etc...
</Trans>
}
onClick={() =>
onChoose({
functionType: gd.EventsFunction.Action,
name: null,
})
}
/>
<ListItem
leftIcon={<Function style={styles.icon} />}
primaryText={<Trans>Condition</Trans>}
secondaryText={
<Trans>
A condition that can be used on objects with the behavior. You can
define the condition parameters: objects, texts, numbers, layers,
etc...
</Trans>
}
onClick={() =>
onChoose({
functionType: gd.EventsFunction.Condition,
name: null,
})
}
/>
<ListItem
leftIcon={<Function style={styles.icon} />}
primaryText={<Trans>Expression</Trans>}
secondaryText={
<Trans>
An expression that can be used on objects with the behavior. Can
either return a number or a string, and take some parameters.
</Trans>
}
onClick={() =>
onChoose({
functionType: gd.EventsFunction.Expression,
name: null,
})
}
/>
<Subheader>
<Trans>Lifecycle methods</Trans>
</Subheader>
<MethodListItem
icon={<Create style={styles.icon} />}
name={'onCreated'}
disabled={eventsFunctions.hasEventsFunctionNamed('onCreated')}
onChoose={onChoose}
description={
<Trans>
Events that will be run once, when an object is created with this
behavior being attached to it.
</Trans>
}
/>
<MethodListItem
icon={<Step style={styles.icon} />}
name={'doStepPreEvents'}
disabled={eventsFunctions.hasEventsFunctionNamed('doStepPreEvents')}
onChoose={onChoose}
description={
<Trans>
Events that will be run at every frame (roughly 60 times per
second), for every object that has the behavior attached, before
the events from the events sheet are launched.
</Trans>
}
/>
<MethodListItem
icon={<Destroy style={styles.icon} />}
name={'onDestroy'}
disabled={
eventsFunctions.hasEventsFunctionNamed('onOwnerRemovedFromScene') ||
eventsFunctions.hasEventsFunctionNamed('onDestroy')
}
onChoose={onChoose}
description={
<Trans>
Events that will be run once, after the object is removed from the
scene and before it is entirely removed from memory.
</Trans>
}
/>
{showAdvanced && (
<>
<Subheader>
<Trans>Other lifecycle methods</Trans>
</Subheader>
<MethodListItem
icon={<Deactivate style={styles.icon} />}
name={'onDeActivate'}
disabled={eventsFunctions.hasEventsFunctionNamed('onDeActivate')}
onChoose={onChoose}
description={
<Trans>
Events that will be run once when the behavior is deactivated
on an object (step events won't be run until the behavior is
activated again).
</Trans>
}
/>
<MethodListItem
icon={<Activate style={styles.icon} />}
name={'onActivate'}
disabled={eventsFunctions.hasEventsFunctionNamed('onActivate')}
onChoose={onChoose}
description={
<Trans>
Events that will be run once when the behavior is re-activated
on an object (after it was previously deactivated).
</Trans>
}
/>
<MethodListItem
icon={<Step style={styles.icon} />}
name={'doStepPostEvents'}
disabled={eventsFunctions.hasEventsFunctionNamed(
'doStepPostEvents'
)}
onChoose={onChoose}
description={
<Trans>
Events that will be run at every frame (roughly 60 times per
second), for every object that has the behavior attached,
after the events from the events sheet.
</Trans>
}
/>
</>
)}
</List>
<Line justifyContent="center" alignItems="center">
{!showAdvanced ? (
<FlatButton
icon={<Visibility />}
primary={false}
onClick={() => setShowAdvanced(true)}
label={<Trans>Show other lifecycle functions (advanced)</Trans>}
/>
<MethodListItem
icon={<Step style={styles.icon} />}
name={'doStepPreEvents'}
disabled={eventsFunctions.hasEventsFunctionNamed('doStepPreEvents')}
onChoose={onChoose}
description={
<Trans>
Events that will be run at every frame (roughly 60 times per
second), for every object that has the behavior attached, before
the events from the events sheet are launched.
</Trans>
}
) : (
<FlatButton
icon={<VisibilityOff />}
primary={false}
onClick={() => setShowAdvanced(false)}
label={<Trans>Hide other lifecycle functions (advanced)</Trans>}
/>
<MethodListItem
icon={<Destroy style={styles.icon} />}
name={'onDestroy'}
disabled={
eventsFunctions.hasEventsFunctionNamed(
'onOwnerRemovedFromScene'
) || eventsFunctions.hasEventsFunctionNamed('onDestroy')
}
onChoose={onChoose}
description={
<Trans>
Events that will be run once, after the object is removed from
the scene and before it is entirely removed from memory.
</Trans>
}
/>
<Subheader>
<Trans>Other lifecycle methods</Trans>
</Subheader>
<MethodListItem
icon={<Deactivate style={styles.icon} />}
name={'onDeActivate'}
disabled={eventsFunctions.hasEventsFunctionNamed('onDeActivate')}
onChoose={onChoose}
description={
<Trans>
Events that will be run once when the behavior is deactivated on
an object (step events won't be run until the behavior is
activated again).
</Trans>
}
/>
<MethodListItem
icon={<Activate style={styles.icon} />}
name={'onActivate'}
disabled={eventsFunctions.hasEventsFunctionNamed('onActivate')}
onChoose={onChoose}
description={
<Trans>
Events that will be run once when the behavior is re-activated
on an object (after it was previously deactivated).
</Trans>
}
/>
<MethodListItem
icon={<Step style={styles.icon} />}
name={'doStepPostEvents'}
disabled={eventsFunctions.hasEventsFunctionNamed(
'doStepPostEvents'
)}
onChoose={onChoose}
description={
<Trans>
Events that will be run at every frame (roughly 60 times per
second), for every object that has the behavior attached, after
the events from the events sheet.
</Trans>
}
/>
<Subheader>
<Trans>Custom</Trans>
</Subheader>
<ListItem
leftIcon={<Function style={styles.icon} />}
primaryText={
<Trans>Custom (action, condition or expression)</Trans>
}
secondaryText={
<Trans>
An action, condition or expression that can be used on objects
that have the behavior attached to them. Use it from the events
sheet as any other action/condition/expression.
</Trans>
}
secondaryTextLines={2}
onClick={() =>
onChoose({
functionType: gd.EventsFunction.Action,
name: null,
})
}
/>
</List>
</Dialog>
);
}
)}
</Line>
</Dialog>
);
}

View File

@@ -111,7 +111,7 @@ export default function BehaviorMethodSelectorDialog({
description={
<Trans>
A condition that can be used in other events sheet. You can define
the conditions parameters: objects, texts, numbers, layers, etc...
the condition parameters: objects, texts, numbers, layers, etc...
</Trans>
}
/>

View File

@@ -19,10 +19,7 @@ import { Column, Line, Spacer } from '../../UI/Grid';
import AlertMessage from '../../UI/AlertMessage';
import DismissableAlertMessage from '../../UI/DismissableAlertMessage';
import Window from '../../Utils/Window';
import {
getExtraInstructionInformation,
getInstructionTutorialHints,
} from '../../Hints';
import { getExtraInstructionInformation } from '../../Hints';
import DismissableTutorialMessage from '../../Hints/DismissableTutorialMessage';
import { isAnEventFunctionMetadata } from '../../EventsFunctionsExtensionsLoader';
import OpenInNew from '@material-ui/icons/OpenInNew';
@@ -34,6 +31,7 @@ import { getInstructionMetadata } from './NewInstructionEditor';
import { ColumnStackLayout } from '../../UI/Layout';
import { setupInstructionParameters } from '../../InstructionOrExpression/SetupInstructionParameters';
import ScrollView from '../../UI/ScrollView';
import { getInstructionTutorialIds } from '../../Utils/GDevelopServices/Tutorial';
const gd: libGDevelop = global.gd;
const styles = {
@@ -222,7 +220,7 @@ export default class InstructionParametersEditor extends React.Component<
const instructionExtraInformation = getExtraInstructionInformation(
instructionType
);
const tutorialHints = getInstructionTutorialHints(instructionType);
const tutorialIds = getInstructionTutorialIds(instructionType);
const objectParameterIndex = objectName
? getObjectParameterIndex(instructionMetadata)
: -1;
@@ -276,13 +274,13 @@ export default class InstructionParametersEditor extends React.Component<
)}
</Line>
)}
{tutorialHints.length ? (
{tutorialIds.length ? (
<Line>
<ColumnStackLayout expand>
{tutorialHints.map(tutorialHint => (
{tutorialIds.map(tutorialId => (
<DismissableTutorialMessage
key={tutorialHint.identifier}
tutorialHint={tutorialHint}
key={tutorialId}
tutorialId={tutorialId}
/>
))}
</ColumnStackLayout>

View File

@@ -19,6 +19,7 @@ const mapTypeToOperators = {
unknown: Object.keys(operatorLabels),
number: ['=', '+', '-', '*', '/'],
string: ['=', '+'],
color: ['=', '+'],
};
export default class OperatorField extends Component<ParameterFieldProps> {
@@ -36,7 +37,8 @@ export default class OperatorField extends Component<ParameterFieldProps> {
const comparedValueType = parameterMetadata
? parameterMetadata.getExtraInfo()
: 'unknown';
const operators = mapTypeToOperators[comparedValueType || 'unknown'];
const operators =
mapTypeToOperators[comparedValueType] || mapTypeToOperators.unknown;
return (
<SelectField

View File

@@ -16,10 +16,11 @@ const operatorLabels = {
'!=': t`≠ (not equal to)`,
};
const mapTypeToOperators = {
const mapTypeToOperators: { [string]: Array<string> } = {
unknown: Object.keys(operatorLabels),
number: ['=', '<', '>', '<=', '>=', '!='],
string: ['=', '!='],
color: ['=', '!='],
};
export default class RelationalOperatorField extends Component<ParameterFieldProps> {
@@ -37,7 +38,8 @@ export default class RelationalOperatorField extends Component<ParameterFieldPro
const comparedValueType = parameterMetadata
? parameterMetadata.getExtraInfo()
: 'unknown';
const operators = mapTypeToOperators[comparedValueType || 'unknown'];
const operators =
mapTypeToOperators[comparedValueType] || mapTypeToOperators.unknown;
return (
<SelectField

View File

@@ -80,7 +80,7 @@ import {
hasClipboardConditions,
pasteInstructionsFromClipboardInInstructionsList,
} from './ClipboardKind';
import InfoBar from '../UI/Messages/InfoBar';
import DismissableInfoBar from '../UI/Messages/DismissableInfoBar';
import { useScreenType } from '../UI/Reponsive/ScreenTypeMeasurer';
import { ResponsiveWindowMeasurer } from '../UI/Reponsive/ResponsiveWindowMeasurer';
import { type UnsavedChanges } from '../MainFrame/UnsavedChangesContext';
@@ -1475,7 +1475,7 @@ export class EventsSheetComponentWithoutHandle extends React.Component<
onClose={this.closeEventTextDialog}
/>
)}
<InfoBar
<DismissableInfoBar
identifier="edit-instruction-explanation"
message={
<Trans>

View File

@@ -55,6 +55,7 @@ export const browserOnlineCordovaExportPipeline: ExportPipeline<
> = {
name: 'browser-online-cordova',
onlineBuildType: 'cordova-build',
limitedBuilds: true,
packageNameWarningType: 'mobile',
getInitialExportState: () => ({

View File

@@ -55,6 +55,7 @@ export const browserOnlineElectronExportPipeline: ExportPipeline<
> = {
name: 'browser-online-electron',
onlineBuildType: 'electron-build',
limitedBuilds: true,
packageNameWarningType: 'desktop',
getInitialExportState: () => ({

View File

@@ -22,7 +22,10 @@ import {
type ExportPipeline,
type ExportPipelineContext,
} from '../ExportPipeline.flow';
import { ExplanationHeader } from '../GenericExporters/OnlineWebExport';
import {
ExplanationHeader,
WebProjectLink,
} from '../GenericExporters/OnlineWebExport';
const gd: libGDevelop = global.gd;
type ExportState = null;
@@ -61,7 +64,11 @@ export const browserOnlineWebExportPipeline: ExportPipeline<
renderHeader: () => <ExplanationHeader />,
renderLaunchButtonLabel: () => <Trans>Publish online</Trans>,
renderLaunchButtonLabel: () => <Trans>Generate link</Trans>,
renderCustomStepsProgress: (build: ?Build, loading: boolean) => (
<WebProjectLink build={build} loading={loading} />
),
prepareExporter: (
context: ExportPipelineContext<ExportState>

View File

@@ -52,6 +52,7 @@ const setupWindowClosedPolling = () => {
);
if (!Object.keys(previewWindowAndTargetIds).length) {
clearInterval(windowClosedPollingIntervalId);
windowClosedPollingIntervalId = null;
}
}
}

View File

@@ -1,6 +1,4 @@
// @flow
import { Trans } from '@lingui/macro';
import * as React from 'react';
import { type Exporter } from '../ExportDialog';
import { browserOnlineCordovaExportPipeline } from './BrowserOnlineCordovaExport.js';
import { browserOnlineElectronExportPipeline } from './BrowserOnlineElectronExport.js';
@@ -9,109 +7,50 @@ import { browserHTML5ExportPipeline } from './BrowserHTML5Export';
import { browserCordovaExportPipeline } from './BrowserCordovaExport';
import { browserElectronExportPipeline } from './BrowserElectronExport';
import { browserFacebookInstantGamesExportPipeline } from './BrowserFacebookInstantGamesExport';
import PhoneIphone from '@material-ui/icons/PhoneIphone';
import LaptopMac from '@material-ui/icons/LaptopMac';
import Folder from '@material-ui/icons/Folder';
import Facebook from '../../UI/CustomSvgIcons/Facebook';
import Cordova from '../../UI/CustomSvgIcons/Cordova';
import Chrome from '../../UI/CustomSvgIcons/Chrome';
import { cordovaExporter } from '../GenericExporters/CordovaExport';
import { onlineWebExporter } from '../GenericExporters/OnlineWebExport';
import { html5Exporter } from '../GenericExporters/HTML5Export';
import { facebookInstantGamesExporter } from '../GenericExporters/FacebookInstantGamesExport';
import { onlineCordovaExporter } from '../GenericExporters/OnlineCordovaExport';
import { onlineElectronExporter } from '../GenericExporters/OnlineElectronExport';
import { electronExporter } from '../GenericExporters/ElectronExport';
export const getBrowserExporters = (): Array<Exporter> => [
export const browserOnlineWebExporter: Exporter = {
...onlineWebExporter,
exportPipeline: browserOnlineWebExportPipeline,
};
export const browserAutomatedExporters: Array<Exporter> = [
{
name: <Trans>Android (&amp; iOS coming soon)</Trans>,
renderIcon: props => <PhoneIphone {...props} />,
helpPage: '/publishing/android_and_ios',
description: (
<Trans>
Package your game for Android directly from GDevelop. iOS support is
coming soon!
</Trans>
),
key: 'browseronlinecordovaexport',
...html5Exporter,
exportPipeline: browserHTML5ExportPipeline,
},
{
...onlineCordovaExporter,
exportPipeline: browserOnlineCordovaExportPipeline,
},
{
name: <Trans>Web (upload online)</Trans>,
renderIcon: props => <Chrome {...props} />,
helpPage: '/publishing/web',
description: (
<Trans>
Upload your game online directly from GDevelop and share the link to
players. Play to your game using your browser on computers and mobile
phones.
</Trans>
),
key: 'browsers3export',
exportPipeline: browserOnlineWebExportPipeline,
},
{
name: <Trans>HTML5 game (zip)</Trans>,
renderIcon: props => <Folder {...props} />,
helpPage: '/publishing/html5_game_in_a_local_folder',
description: (
<Trans>
Build the game locally as a HTML5 game. You can then publish it on
website like Poki, CrazyGames, Game Jolt, itch.io, Newsground...
</Trans>
),
key: 'browserhtml5export',
exportPipeline: browserHTML5ExportPipeline,
advanced: true,
},
{
name: <Trans>Facebook Instant Games</Trans>,
renderIcon: props => <Facebook {...props} />,
helpPage: '/publishing/publishing-to-facebook-instant-games',
description: (
<Trans>
Package your game as a Facebook Instant Games that can be played on
Facebook Messenger.
</Trans>
),
key: 'browserfacebookinstantgames',
exportPipeline: browserFacebookInstantGamesExportPipeline,
advanced: true,
},
{
name: <Trans>iOS &amp; Android (manual)</Trans>,
renderIcon: props => <Cordova {...props} />,
helpPage: '/publishing/android_and_ios_with_cordova',
description: (
<Trans>
Build the game locally as a Cordova project, and export it manually then
to iOS or Android with Cordova developers tools.
</Trans>
),
key: 'browsercordovaexport',
exportPipeline: browserCordovaExportPipeline,
advanced: true,
},
{
name: <Trans>Windows/macOS/Linux</Trans>,
renderIcon: props => <LaptopMac {...props} />,
helpPage: '/publishing/windows-macos-linux',
description: (
<Trans>
Package your game as an app for Windows, macOS or Linux directly from
GDevelop.
</Trans>
),
key: 'browseronlineelectronexport',
...onlineElectronExporter,
exportPipeline: browserOnlineElectronExportPipeline,
},
{
name: <Trans>Windows/macOS/Linux (manual)</Trans>,
renderIcon: props => <LaptopMac {...props} />,
helpPage: '/publishing/windows-macos-linux-with-electron',
description: (
<Trans>
Build the game locally and export it manually to Windows, macOS or Linux
with third-party developer tools.
</Trans>
),
key: 'browserelectronexport',
...facebookInstantGamesExporter,
exportPipeline: browserFacebookInstantGamesExportPipeline,
},
];
export const browserManualExporters: Array<Exporter> = [
{
...html5Exporter,
exportPipeline: browserHTML5ExportPipeline,
},
{
...cordovaExporter,
exportPipeline: browserCordovaExportPipeline,
},
{
...electronExporter,
exportPipeline: browserElectronExportPipeline,
advanced: true,
},
];

View File

@@ -1,277 +0,0 @@
// @flow
import { Trans } from '@lingui/macro';
import * as React from 'react';
import Dialog from '../UI/Dialog';
import HelpButton from '../UI/HelpButton';
import FlatButton from '../UI/FlatButton';
import Subheader from '../UI/Subheader';
import { List, ListItem } from '../UI/List';
import Visibility from '@material-ui/icons/Visibility';
import VisibilityOff from '@material-ui/icons/VisibilityOff';
import BuildsDialog from './Builds/BuildsDialog';
import { Line } from '../UI/Grid';
import AuthenticatedUserContext, {
type AuthenticatedUser,
} from '../Profile/AuthenticatedUserContext';
import ExportLauncher from './ExportLauncher';
import { type ExportPipeline } from './ExportPipeline.flow';
import { OnlineStatus } from '../Utils/OnlineStatus';
import AlertMessage from '../UI/AlertMessage';
const styles = {
icon: { width: 40, height: 40 },
disabledItem: { opacity: 0.6 },
content: { padding: 8 },
};
export type Exporter = {|
name: React.Node,
renderIcon: (props: {|
style: {| width: number, height: number |},
|}) => React.Node,
helpPage: string,
description: React.Node,
disabled?: boolean,
advanced?: boolean,
experimental?: boolean,
key: string,
exportPipeline: ExportPipeline<any, any, any, any, any>,
|};
export type ExportDialogWithoutExportsProps = {|
project: ?gdProject,
onClose: () => void,
onChangeSubscription: () => void,
|};
type Props = {|
...ExportDialogWithoutExportsProps,
exporters: Array<Exporter>,
allExportersRequireOnline?: boolean,
|};
type State = {|
chosenExporterKey: string,
showExperimental: boolean,
buildsDialogOpen: boolean,
|};
type ExperimentalExportButtonProps = {|
showExperimental: boolean,
onClick: (value: boolean) => void,
disabled: boolean,
|};
const ExperimentalExportButton = (props: ExperimentalExportButtonProps) => {
const { showExperimental, onClick, disabled } = props;
return (
<>
{!disabled && !showExperimental && (
<FlatButton
key="toggle-experimental"
icon={<Visibility />}
primary={false}
onClick={() => onClick(true)}
label={<Trans>Show experimental exports</Trans>}
/>
)}
{!disabled && showExperimental && (
<FlatButton
key="toggle-experimental"
icon={<VisibilityOff />}
primary={false}
onClick={() => onClick(false)}
label={<Trans>Hide experimental exports</Trans>}
/>
)}
</>
);
};
export default class ExportDialog extends React.Component<Props, State> {
state = {
chosenExporterKey: '',
showExperimental: false,
buildsDialogOpen: false,
};
chooseExporter = (key: string) => {
this.setState({
chosenExporterKey: key,
});
};
_showExperimental = (show: boolean = true) => {
this.setState({
showExperimental: show,
});
};
_openBuildsDialog = (open: boolean = true) => {
this.setState({
buildsDialogOpen: open,
});
};
_renderExporterListItem = (
exporter: Exporter,
index: number,
forceDisable: boolean
) => {
return (
<ListItem
key={exporter.key}
disabled={forceDisable || exporter.disabled}
style={
forceDisable || exporter.disabled ? styles.disabledItem : undefined
}
leftIcon={exporter.renderIcon({ style: styles.icon })}
primaryText={exporter.name}
secondaryText={exporter.description}
secondaryTextLines={2}
onClick={() => this.chooseExporter(exporter.key)}
/>
);
};
render() {
const {
project,
onClose,
allExportersRequireOnline,
onChangeSubscription,
exporters,
} = this.props;
const { showExperimental, chosenExporterKey } = this.state;
if (!project) return null;
const exporter = exporters.find(
exporter => exporter.key === chosenExporterKey
);
return (
<AuthenticatedUserContext.Consumer>
{(authenticatedUser: AuthenticatedUser) => (
<OnlineStatus>
{onlineStatus => {
const cantExportBecauseOffline =
!!allExportersRequireOnline && !onlineStatus;
return (
<Dialog
title={<Trans>Export project to a standalone game</Trans>}
onRequestClose={onClose}
cannotBeDismissed={false}
actions={[
chosenExporterKey && (
<FlatButton
label={<Trans>Back</Trans>}
key="back"
primary={false}
onClick={() => this.chooseExporter('')}
/>
),
<FlatButton
label={<Trans>Close</Trans>}
key="close"
primary={false}
onClick={onClose}
/>,
]}
secondaryActions={[
<HelpButton
key="help"
helpPagePath={
(exporter && exporter.helpPage) || '/publishing'
}
/>,
<FlatButton
key="builds"
label={<Trans>See all my builds</Trans>}
onClick={() => this._openBuildsDialog(true)}
/>,
]}
open
noMargin
>
{cantExportBecauseOffline && (
<AlertMessage kind="error">
<Trans>
You must be online and have a proper internet connection
to export your game.
</Trans>
</AlertMessage>
)}
{!exporter && (
<React.Fragment>
<List>
{exporters
.filter(
exporter =>
!exporter.advanced && !exporter.experimental
)
.map((exporter, index) =>
this._renderExporterListItem(
exporter,
index,
cantExportBecauseOffline
)
)}
<Subheader>Advanced</Subheader>
{exporters
.filter(exporter => exporter.advanced)
.map((exporter, index) =>
this._renderExporterListItem(
exporter,
index,
cantExportBecauseOffline
)
)}
{showExperimental && (
<Subheader>Experimental</Subheader>
)}
{showExperimental &&
exporters
.filter(exporter => exporter.experimental)
.map((exporter, index) =>
this._renderExporterListItem(
exporter,
index,
cantExportBecauseOffline
)
)}
</List>
<Line justifyContent="center" alignItems="center">
<ExperimentalExportButton
showExperimental={showExperimental}
onClick={value => this._showExperimental(value)}
disabled
/>
</Line>
</React.Fragment>
)}
{exporter && exporter.exportPipeline && (
<div style={styles.content}>
<ExportLauncher
exportPipeline={exporter.exportPipeline}
project={project}
onChangeSubscription={onChangeSubscription}
authenticatedUser={authenticatedUser}
/>
</div>
)}
<BuildsDialog
open={this.state.buildsDialogOpen}
onClose={() => this._openBuildsDialog(false)}
authenticatedUser={authenticatedUser}
/>
</Dialog>
);
}}
</OnlineStatus>
)}
</AuthenticatedUserContext.Consumer>
);
}
}

View File

@@ -0,0 +1,128 @@
// @flow
import { Trans } from '@lingui/macro';
import * as React from 'react';
import FlatButton from '../../UI/FlatButton';
import { ColumnStackLayout, ResponsiveLineStackLayout } from '../../UI/Layout';
import RaisedButton from '../../UI/RaisedButton';
import ExportHomeSeparator from './ExportHomeSeparator';
import { type Exporter, type ExporterSection, type ExporterKey } from '.';
import Text from '../../UI/Text';
import { Column, Line, Spacer } from '../../UI/Grid';
import LaptopMac from '@material-ui/icons/LaptopMac';
import Chrome from '../../UI/CustomSvgIcons/Chrome';
import PhoneIphone from '@material-ui/icons/PhoneIphone';
import ExportLauncher from './ExportLauncher';
import { type AuthenticatedUser } from '../../Profile/AuthenticatedUserContext';
import PeopleOutline from '@material-ui/icons/PeopleOutline';
const styles = {
titleContainer: {
display: 'flex',
flex: 2,
alignItems: 'center',
minHeight: '100px',
},
iconsContainer: {
marginTop: 10,
marginBottom: 10,
},
icon: {
height: 48,
width: 48,
},
contentContainer: {
flex: 5,
},
};
type ExportHomeProps = {|
onlineWebExporter: Exporter,
setChosenExporterKey: (key: ExporterKey) => void,
setChosenExporterSection: (section: ExporterSection) => void,
cantExportBecauseOffline: boolean,
project: gdProject,
onChangeSubscription: () => void,
authenticatedUser: AuthenticatedUser,
|};
const ExportHome = ({
onlineWebExporter,
setChosenExporterKey,
setChosenExporterSection,
cantExportBecauseOffline,
project,
onChangeSubscription,
authenticatedUser,
}: ExportHomeProps) => {
return (
<ResponsiveLineStackLayout>
<ColumnStackLayout alignItems="center" expand>
<div style={styles.titleContainer}>
<Line>
<Text size="title">
<Trans>Share with friends</Trans>
</Text>
</Line>
</div>
<Line expand>
<PeopleOutline style={styles.icon} />
</Line>
<div style={styles.contentContainer}>
<ExportLauncher
exportPipeline={onlineWebExporter.exportPipeline}
project={project}
onChangeSubscription={onChangeSubscription}
authenticatedUser={authenticatedUser}
/>
</div>
</ColumnStackLayout>
<ExportHomeSeparator />
<ColumnStackLayout alignItems="center" expand>
<div style={styles.titleContainer}>
<Line>
<Text size="title">
<Trans>Publish your game</Trans>
</Text>
</Line>
</div>
<Line expand>
<div style={styles.iconsContainer}>
<Chrome style={styles.icon} />
<PhoneIphone style={styles.icon} />
<LaptopMac style={styles.icon} />
</div>
</Line>
<div style={styles.contentContainer}>
<Column alignItems="center">
<Line>
<Text align="center">
<Trans>
Export your game to mobile, desktop and web platforms.
</Trans>
</Text>
</Line>
<RaisedButton
label={<Trans>Publish on stores</Trans>}
onClick={() => {
setChosenExporterSection('automated');
setChosenExporterKey('webexport');
}}
primary
/>
<Spacer />
<FlatButton
label={<Trans>Build manually</Trans>}
primary
onClick={() => {
setChosenExporterSection('manual');
setChosenExporterKey('webexport');
}}
/>
</Column>
</div>
</ColumnStackLayout>
</ResponsiveLineStackLayout>
);
};
export default ExportHome;

View File

@@ -0,0 +1,24 @@
// @flow
import React from 'react';
import { Column } from '../../UI/Grid';
import { useResponsiveWindowWidth } from '../../UI/Reponsive/ResponsiveWindowMeasurer';
import GDevelopThemeContext from '../../UI/Theme/ThemeContext';
export default () => {
const windowWidth = useResponsiveWindowWidth();
const theme = React.useContext(GDevelopThemeContext);
return (
windowWidth !== 'small' && (
<Column justifyContent="center">
<span
style={{
height: 'calc(100% - 30px)',
borderLeftStyle: 'solid',
borderLeftWidth: 1,
borderColor: theme.toolbar.separatorColor,
}}
/>
</Column>
)
);
};

View File

@@ -2,42 +2,42 @@
import React, { Component } from 'react';
import { I18n } from '@lingui/react';
import { t } from '@lingui/macro';
import RaisedButton from '../UI/RaisedButton';
import { sendExportLaunched } from '../Utils/Analytics/EventSender';
import { t, Trans } from '@lingui/macro';
import RaisedButton from '../../UI/RaisedButton';
import { sendExportLaunched } from '../../Utils/Analytics/EventSender';
import {
type Build,
type BuildArtifactKeyName,
getBuildArtifactUrl,
} from '../Utils/GDevelopServices/Build';
import { type AuthenticatedUser } from '../Profile/AuthenticatedUserContext';
import { Column, Line } from '../UI/Grid';
import { showErrorBox } from '../UI/Messages/MessageBox';
import Window from '../Utils/Window';
import CreateProfile from '../Profile/CreateProfile';
import LimitDisplayer from '../Profile/LimitDisplayer';
} from '../../Utils/GDevelopServices/Build';
import { type AuthenticatedUser } from '../../Profile/AuthenticatedUserContext';
import { Column, Line, Spacer } from '../../UI/Grid';
import { showErrorBox } from '../../UI/Messages/MessageBox';
import Window from '../../Utils/Window';
import CreateProfile from '../../Profile/CreateProfile';
import LimitDisplayer from '../../Profile/LimitDisplayer';
import {
displayProjectErrorsBox,
getProjectPropertiesErrors,
} from '../Utils/ProjectErrorsChecker';
import { type Limit } from '../Utils/GDevelopServices/Usage';
import BuildsWatcher from './Builds/BuildsWatcher';
} from '../../Utils/ProjectErrorsChecker';
import { type Limit } from '../../Utils/GDevelopServices/Usage';
import BuildsWatcher from '../Builds/BuildsWatcher';
import BuildStepsProgress, {
type BuildStep,
} from './Builds/BuildStepsProgress';
} from '../Builds/BuildStepsProgress';
import {
registerGame,
getGame,
updateGame,
} from '../Utils/GDevelopServices/Game';
import { type ExportPipeline } from './ExportPipeline.flow';
import { GameRegistration } from '../GameDashboard/GameRegistration';
import DismissableAlertMessage from '../UI/DismissableAlertMessage';
} from '../../Utils/GDevelopServices/Game';
import { type ExportPipeline } from '../ExportPipeline.flow';
import { GameRegistration } from '../../GameDashboard/GameRegistration';
import DismissableAlertMessage from '../../UI/DismissableAlertMessage';
import {
ACHIEVEMENT_FEATURE_FLAG,
addCreateBadgePreHookIfNotClaimed,
TRIVIAL_FIRST_WEB_EXPORT,
} from '../Utils/GDevelopServices/Badge';
} from '../../Utils/GDevelopServices/Badge';
type State = {|
exportStep: BuildStep,
@@ -312,7 +312,10 @@ export default class ExportLauncher extends Component<Props, State> {
? authenticatedUser.limits[exportPipeline.onlineBuildType]
: null;
const canLaunchBuild = (authenticatedUser: AuthenticatedUser) => {
if (!errored && exportStep !== '' && exportStep !== 'done') return false;
const buildPending =
!errored && exportStep !== '' && exportStep !== 'done';
const buildFinished = !errored && exportStep === 'done';
if (buildPending || buildFinished) return false;
const limit: ?Limit = getBuildLimit(authenticatedUser);
if (limit && limit.limitReached) return false;
@@ -359,32 +362,46 @@ export default class ExportLauncher extends Component<Props, State> {
/>
</Line>
)}
{!!exportPipeline.onlineBuildType &&
authenticatedUser.authenticated && (
<LimitDisplayer
subscription={authenticatedUser.subscription}
limit={getBuildLimit(authenticatedUser)}
onChangeSubscription={this.props.onChangeSubscription}
/>
)}
<Spacer />
{!!exportPipeline.onlineBuildType &&
!authenticatedUser.authenticated && (
<CreateProfile
onLogin={authenticatedUser.onLogin}
onCreateAccount={authenticatedUser.onCreateAccount}
message={
<Trans>
Create an account or login first to publish your game.
</Trans>
}
justifyContent="center"
/>
)}
<Line expand>
<BuildStepsProgress
exportStep={exportStep}
hasBuildStep={!!exportPipeline.onlineBuildType}
build={build}
onDownload={this._downloadBuild}
stepMaxProgress={stepMaxProgress}
stepCurrentProgress={stepCurrentProgress}
errored={errored}
{authenticatedUser.authenticated &&
(exportPipeline.renderCustomStepsProgress ? (
exportPipeline.renderCustomStepsProgress(
build,
!!this.state.exportStep && this.state.exportStep !== 'done'
)
) : (
<Line expand>
<BuildStepsProgress
exportStep={exportStep}
hasBuildStep={!!exportPipeline.onlineBuildType}
build={build}
onDownload={this._downloadBuild}
stepMaxProgress={stepMaxProgress}
stepCurrentProgress={stepCurrentProgress}
errored={errored}
/>
</Line>
))}
{!!exportPipeline.limitedBuilds && authenticatedUser.authenticated && (
<LimitDisplayer
subscription={authenticatedUser.subscription}
limit={getBuildLimit(authenticatedUser)}
onChangeSubscription={this.props.onChangeSubscription}
/>
</Line>
)}
{doneFooterOpen &&
exportPipeline.renderDoneFooter &&
exportPipeline.renderDoneFooter({
@@ -393,7 +410,7 @@ export default class ExportLauncher extends Component<Props, State> {
onClose: this._closeDoneFooter,
})}
{doneFooterOpen && (
<Line>
<Line justifyContent="center">
<GameRegistration project={project} />
</Line>
)}

View File

@@ -0,0 +1,196 @@
// @flow
import { Trans } from '@lingui/macro';
import * as React from 'react';
import Dialog from '../../UI/Dialog';
import HelpButton from '../../UI/HelpButton';
import FlatButton from '../../UI/FlatButton';
import BuildsDialog from '../Builds/BuildsDialog';
import AuthenticatedUserContext from '../../Profile/AuthenticatedUserContext';
import ExportLauncher from './ExportLauncher';
import { type ExportPipeline } from '../ExportPipeline.flow';
import { useOnlineStatus } from '../../Utils/OnlineStatus';
import AlertMessage from '../../UI/AlertMessage';
import { Tab, Tabs } from '../../UI/Tabs';
import ExportHome from './ExportHome';
const styles = {
icon: { width: 40, height: 40 },
disabledItem: { opacity: 0.6 },
content: { padding: 8 },
};
export type ExporterSection = 'automated' | 'manual' | 'home';
export type ExporterKey =
| 'onlinewebexport'
| 'onlineelectronexport'
| 'onlinecordovaexport'
| 'webexport'
| 'facebookinstantgamesexport'
| 'electronexport'
| 'cordovaexport';
export type Exporter = {|
name: React.Node,
tabName: React.Node,
helpPage: string,
disabled?: boolean,
experimental?: boolean,
key: ExporterKey,
exportPipeline: ExportPipeline<any, any, any, any, any>,
|};
export type ExportDialogWithoutExportsProps = {|
project: ?gdProject,
onClose: () => void,
onChangeSubscription: () => void,
|};
type Props = {|
...ExportDialogWithoutExportsProps,
automatedExporters: Array<Exporter>,
manualExporters: Array<Exporter>,
onlineWebExporter: Exporter,
allExportersRequireOnline?: boolean,
|};
const ExportDialog = ({
project,
onClose,
allExportersRequireOnline,
onChangeSubscription,
automatedExporters,
manualExporters,
onlineWebExporter,
}: Props) => {
const [
chosenExporterSection,
setChosenExporterSection,
] = React.useState<ExporterSection>('home');
const [buildsDialogOpen, setBuildsDialogOpen] = React.useState<boolean>(
false
);
const [chosenExporterKey, setChosenExporterKey] = React.useState<ExporterKey>(
'onlinewebexport'
);
const authenticatedUser = React.useContext(AuthenticatedUserContext);
const onlineStatus = useOnlineStatus();
const cantExportBecauseOffline = !!allExportersRequireOnline && !onlineStatus;
if (!project) return null;
const exporters = [
...automatedExporters,
...manualExporters,
onlineWebExporter,
];
const exporter = exporters.find(
exporter => exporter.key === chosenExporterKey
);
if (!exporter || !exporter.exportPipeline) return null;
return (
<Dialog
title={
chosenExporterSection === 'automated' ? (
<Trans>Publish your game</Trans>
) : chosenExporterSection === 'manual' ? (
<Trans>Build manually</Trans>
) : null
}
onRequestClose={onClose}
cannotBeDismissed={false}
actions={[
chosenExporterSection !== 'home' && (
<FlatButton
label={<Trans>Back</Trans>}
key="back"
primary={false}
onClick={() => {
setChosenExporterSection('home');
setChosenExporterKey('onlinewebexport');
}}
/>
),
<FlatButton
label={<Trans>Close</Trans>}
key="close"
primary={false}
onClick={onClose}
/>,
]}
secondaryActions={[
<HelpButton key="help" helpPagePath={exporter.helpPage} />,
exporter.key !== 'onlinewebexport' && (
<FlatButton
key="builds"
label={<Trans>See all my builds</Trans>}
onClick={() => setBuildsDialogOpen(true)}
/>
),
]}
open
noMargin
>
{cantExportBecauseOffline && (
<AlertMessage kind="error">
<Trans>
You must be online and have a proper internet connection to export
your game.
</Trans>
</AlertMessage>
)}
{chosenExporterSection === 'home' && (
<ExportHome
cantExportBecauseOffline={cantExportBecauseOffline}
onlineWebExporter={onlineWebExporter}
setChosenExporterKey={setChosenExporterKey}
setChosenExporterSection={setChosenExporterSection}
project={project}
onChangeSubscription={onChangeSubscription}
authenticatedUser={authenticatedUser}
/>
)}
{chosenExporterSection === 'automated' && (
<Tabs value={chosenExporterKey} onChange={setChosenExporterKey}>
{automatedExporters.map(exporter => (
<Tab
label={exporter.tabName}
value={exporter.key}
key={exporter.key}
/>
))}
</Tabs>
)}
{chosenExporterSection === 'manual' && (
<Tabs value={chosenExporterKey} onChange={setChosenExporterKey}>
{manualExporters.map(exporter => (
<Tab
label={exporter.tabName}
value={exporter.key}
key={exporter.key}
/>
))}
</Tabs>
)}
{chosenExporterSection !== 'home' && (
<div style={styles.content}>
<ExportLauncher
exportPipeline={exporter.exportPipeline}
project={project}
onChangeSubscription={onChangeSubscription}
authenticatedUser={authenticatedUser}
key={chosenExporterKey}
/>
</div>
)}
<BuildsDialog
open={buildsDialogOpen}
onClose={() => setBuildsDialogOpen(false)}
authenticatedUser={authenticatedUser}
/>
</Dialog>
);
};
export default ExportDialog;

View File

@@ -22,6 +22,7 @@ export type ExportPipeline<
> = {|
name: string,
onlineBuildType?: string,
limitedBuilds?: boolean,
packageNameWarningType?: 'mobile' | 'desktop',
getInitialExportState: (project: gdProject) => ExportState,
@@ -33,10 +34,13 @@ export type ExportPipeline<
updater: (prevExportState: ExportState) => ExportState
) => void,
|}) => React.Node,
renderLaunchButtonLabel: () => React.Node,
canLaunchBuild: (exportState: ExportState) => boolean,
renderCustomStepsProgress?: (build: ?Build, loading: boolean) => React.Node,
prepareExporter: (
context: ExportPipelineContext<ExportState>
) => Promise<PreparedExporter>,

View File

@@ -28,3 +28,10 @@ export const DoneFooter = ({
<Line justifyContent="center">{renderGameButton()}</Line>
</Column>
);
export const cordovaExporter = {
key: 'cordovaexport',
tabName: <Trans>Mobile</Trans>,
name: <Trans>iOS &amp; Android (manual)</Trans>,
helpPage: '/publishing/android_and_ios_with_cordova',
};

View File

@@ -30,3 +30,10 @@ export const DoneFooter = ({
<Line justifyContent="center">{renderGameButton()}</Line>
</Column>
);
export const electronExporter = {
key: 'electronexport',
tabName: <Trans>Desktop</Trans>,
name: <Trans>Windows/macOS/Linux (manual)</Trans>,
helpPage: '/publishing/windows-macos-linux-with-electron',
};

View File

@@ -47,3 +47,10 @@ export const DoneFooter = ({
</Column>
);
};
export const facebookInstantGamesExporter = {
key: 'facebookinstantgamesexport',
tabName: <Trans>Instant Games</Trans>,
name: <Trans>Facebook Instant Games</Trans>,
helpPage: '/publishing/publishing-to-facebook-instant-games',
};

View File

@@ -5,19 +5,45 @@ import Text from '../../UI/Text';
import { getHelpLink } from '../../Utils/HelpLink';
import Window from '../../Utils/Window';
import FlatButton from '../../UI/FlatButton';
import { Column, Spacer } from '../../UI/Grid';
import { Column, Line, Spacer } from '../../UI/Grid';
import AlertMessage from '../../UI/AlertMessage';
import ItchIo from '../../UI/CustomSvgIcons/ItchIo';
import GameJolt from '../../UI/CustomSvgIcons/GameJolt';
import Poki from '../../UI/CustomSvgIcons/Poki';
import CrazyGames from '../../UI/CustomSvgIcons/CrazyGames';
import NewsGround from '../../UI/CustomSvgIcons/NewsGround';
import { useResponsiveWindowWidth } from '../../UI/Reponsive/ResponsiveWindowMeasurer';
export const ExplanationHeader = () => (
<Text>
<Trans>This will export your game to a folder.</Trans>
<Trans>
You can then upload it on a website/game hosting service and share it on
marketplaces and gaming portals like CrazyGames, Poki, Game Jolt, itch.io,
Newsground...
</Trans>
</Text>
);
const getIconStyle = windowWidth => ({
height: windowWidth === 'small' ? 30 : 48,
width: windowWidth === 'small' ? 30 : 48,
margin: 10,
});
export const ExplanationHeader = () => {
const windowWidth = useResponsiveWindowWidth();
const iconStyle = getIconStyle(windowWidth);
return (
<Column noMargin>
<Line>
<Text>
<Trans>
This will export your game to a folder. You can then upload it on a
website/game hosting service and share it on marketplaces and gaming
portals like CrazyGames, Poki, Game Jolt, itch.io, Newsground...
</Trans>
</Text>
</Line>
<Line justifyContent="center">
<ItchIo color="primary" style={iconStyle} />
<GameJolt color="primary" style={iconStyle} />
<Poki color="primary" style={iconStyle} />
<CrazyGames color="primary" style={iconStyle} />
<NewsGround color="primary" style={iconStyle} />
</Line>
</Column>
);
};
export const DoneFooter = ({
renderGameButton,
@@ -49,6 +75,7 @@ export const DoneFooter = ({
)
}
label={<Trans>Publish your game on Game Jolt</Trans>}
icon={<GameJolt />}
/>
<FlatButton
fullWidth
@@ -57,6 +84,7 @@ export const DoneFooter = ({
Window.openExternalURL('https://gdevelop-app.com/crazy-games')
}
label={<Trans>Publish your game on CrazyGames.com</Trans>}
icon={<CrazyGames />}
/>
<FlatButton
fullWidth
@@ -75,12 +103,14 @@ export const DoneFooter = ({
Window.openExternalURL(getHelpLink('/publishing/publishing-to-itch-io'))
}
label={<Trans>Publish your game on Itch.io</Trans>}
icon={<ItchIo />}
/>
<FlatButton
fullWidth
primary
onClick={() => Window.openExternalURL('https://gdevelop-app.com/poki')}
label={<Trans>Publish your game on Poki.com</Trans>}
icon={<Poki />}
/>
<FlatButton
fullWidth
@@ -89,3 +119,10 @@ export const DoneFooter = ({
/>
</Column>
);
export const html5Exporter = {
key: 'webexport',
tabName: <Trans>Web</Trans>,
name: <Trans>Web</Trans>,
helpPage: '/publishing/html5_game_in_a_local_folder',
};

View File

@@ -157,3 +157,10 @@ export const SetupExportHeader = ({
</Column>
);
};
export const onlineCordovaExporter = {
key: 'onlinecordovaexport',
tabName: <Trans>Mobile</Trans>,
name: <Trans>Android (&amp; iOS coming soon)</Trans>,
helpPage: '/publishing/android_and_ios',
};

View File

@@ -78,3 +78,10 @@ export const SetupExportHeader = ({
</React.Fragment>
);
};
export const onlineElectronExporter = {
key: 'onlineelectronexport',
tabName: <Trans>Desktop</Trans>,
name: <Trans>Windows/macOS/Linux</Trans>,
helpPage: '/publishing/windows-macos-linux',
};

View File

@@ -2,13 +2,80 @@
import { Trans } from '@lingui/macro';
import * as React from 'react';
import Text from '../../UI/Text';
import { Column, Line } from '../../UI/Grid';
import TextField from '../../UI/TextField';
import {
getBuildArtifactUrl,
type Build,
} from '../../Utils/GDevelopServices/Build';
import RaisedButton from '../../UI/RaisedButton';
import Window from '../../Utils/Window';
import Paste from '../../UI/CustomSvgIcons/Paste';
import InfoBar from '../../UI/Messages/InfoBar';
export const ExplanationHeader = () => (
<Text>
<Trans>
This will export your game and upload it on GDevelop games hosting. The
game will be freely accessible from the link, available for a few days and
playable from any computer browser or mobile phone (iOS, Android 5+).
</Trans>
</Text>
<Column noMargin alignItems="center" justifyContent="center">
<Line>
<Text align="center">
<Trans>
Generate a unique link to share your game, for a few days, playable
from any computer or mobile phone's browser.
</Trans>
</Text>
</Line>
</Column>
);
type WebProjectLinkProps = {|
build: ?Build,
loading: boolean,
|};
export const WebProjectLink = ({ build, loading }: WebProjectLinkProps) => {
const [showCopiedInfoBar, setShowCopiedInfoBar] = React.useState<boolean>(
false
);
if (!build && !loading) return null;
const buildPending = loading || (build && build.status !== 'complete');
const value = buildPending
? 'Just a few seconds while we generate the link...'
: getBuildArtifactUrl(build, 's3Key') || '';
const onOpen = () => {
if (buildPending) return;
Window.openExternalURL(value);
};
const onCopy = () => {
if (buildPending) return;
// TODO: use Clipboard.js, after it's been reworked to use this API and handle text.
navigator.clipboard.writeText(value);
setShowCopiedInfoBar(true);
};
return (
<Line justifyContent="center">
<TextField value={value} readOnly fullWidth />
{!buildPending && (
<>
<RaisedButton label={<Trans>Open</Trans>} onClick={onOpen} />
<RaisedButton primary icon={<Paste />} onClick={onCopy} />
</>
)}
<InfoBar
message={<Trans>Copied to clipboard!</Trans>}
visible={showCopiedInfoBar}
hide={() => setShowCopiedInfoBar(false)}
/>
</Line>
);
};
export const onlineWebExporter = {
key: 'onlinewebexport',
tabName: 'Web',
name: <Trans>Web (upload online)</Trans>,
helpPage: '/publishing/web',
};

View File

@@ -49,6 +49,7 @@ export const localOnlineCordovaExportPipeline: ExportPipeline<
> = {
name: 'local-online-cordova',
onlineBuildType: 'cordova-build',
limitedBuilds: true,
packageNameWarningType: 'mobile',
getInitialExportState: () => ({

View File

@@ -49,6 +49,7 @@ export const localOnlineElectronExportPipeline: ExportPipeline<
> = {
name: 'local-online-electron',
onlineBuildType: 'electron-build',
limitedBuilds: true,
packageNameWarningType: 'desktop',
getInitialExportState: () => ({

View File

@@ -17,7 +17,10 @@ import {
type ExportPipeline,
type ExportPipelineContext,
} from '../ExportPipeline.flow';
import { ExplanationHeader } from '../GenericExporters/OnlineWebExport';
import {
ExplanationHeader,
WebProjectLink,
} from '../GenericExporters/OnlineWebExport';
const path = optionalRequire('path');
const os = optionalRequire('os');
const gd: libGDevelop = global.gd;
@@ -55,7 +58,11 @@ export const localOnlineWebExportPipeline: ExportPipeline<
renderHeader: () => <ExplanationHeader />,
renderLaunchButtonLabel: () => <Trans>Publish online</Trans>,
renderLaunchButtonLabel: () => <Trans>Generate link</Trans>,
renderCustomStepsProgress: (build: ?Build, loading: boolean) => (
<WebProjectLink build={build} loading={loading} />
),
prepareExporter: (
context: ExportPipelineContext<ExportState>

View File

@@ -1,6 +1,4 @@
// @flow
import { Trans } from '@lingui/macro';
import * as React from 'react';
import { type Exporter } from '../ExportDialog';
import { localCordovaExportPipeline } from './LocalCordovaExport';
import { localElectronExportPipeline } from './LocalElectronExport';
@@ -9,108 +7,49 @@ import { localFacebookInstantGamesExportPipeline } from './LocalFacebookInstantG
import { localOnlineCordovaExportPipeline } from './LocalOnlineCordovaExport.js';
import { localOnlineElectronExportPipeline } from './LocalOnlineElectronExport.js';
import { localOnlineWebExportPipeline } from './LocalOnlineWebExport';
import PhoneIphone from '@material-ui/icons/PhoneIphone';
import LaptopMac from '@material-ui/icons/LaptopMac';
import Folder from '@material-ui/icons/Folder';
import Facebook from '../../UI/CustomSvgIcons/Facebook';
import Cordova from '../../UI/CustomSvgIcons/Cordova';
import Chrome from '../../UI/CustomSvgIcons/Chrome';
import { cordovaExporter } from '../GenericExporters/CordovaExport';
import { onlineWebExporter } from '../GenericExporters/OnlineWebExport';
import { html5Exporter } from '../GenericExporters/HTML5Export';
import { facebookInstantGamesExporter } from '../GenericExporters/FacebookInstantGamesExport';
import { onlineCordovaExporter } from '../GenericExporters/OnlineCordovaExport';
import { onlineElectronExporter } from '../GenericExporters/OnlineElectronExport';
import { electronExporter } from '../GenericExporters/ElectronExport';
export const getLocalExporters = (): Array<Exporter> => [
export const localOnlineWebExporter: Exporter = {
...onlineWebExporter,
exportPipeline: localOnlineWebExportPipeline,
};
export const localAutomatedExporters: Array<Exporter> = [
{
name: <Trans>Android (&amp; iOS coming soon)</Trans>,
renderIcon: props => <PhoneIphone {...props} />,
helpPage: '/publishing/android_and_ios',
description: (
<Trans>
Package your game for Android directly from GDevelop. iOS support is
coming soon!
</Trans>
),
key: 'localonlinecordovaexport',
...html5Exporter,
exportPipeline: localHTML5ExportPipeline,
},
{
...onlineCordovaExporter,
exportPipeline: localOnlineCordovaExportPipeline,
},
{
name: <Trans>Web (upload online)</Trans>,
renderIcon: props => <Chrome {...props} />,
helpPage: '/publishing/web',
description: (
<Trans>
Upload your game online directly from GDevelop and share the link to
players. Play to your game using your browser on computers and mobile
phones.
</Trans>
),
key: 'localonlinewebexport',
exportPipeline: localOnlineWebExportPipeline,
},
{
name: <Trans>Local folder</Trans>,
renderIcon: props => <Folder {...props} />,
helpPage: '/publishing/html5_game_in_a_local_folder',
description: (
<Trans>
Build the game locally as a HTML5 game. You can then publish it on
website like Poki, CrazyGames, Game Jolt, itch.io, Newsground...
</Trans>
),
key: 'localexport',
exportPipeline: localHTML5ExportPipeline,
advanced: true,
},
{
name: <Trans>Facebook Instant Games</Trans>,
renderIcon: props => <Facebook {...props} />,
helpPage: '/publishing/publishing-to-facebook-instant-games',
description: (
<Trans>
Package your game as a Facebook Instant Games that can be played on
Facebook Messenger.
</Trans>
),
key: 'localfacebookinstantgames',
exportPipeline: localFacebookInstantGamesExportPipeline,
advanced: true,
},
{
name: <Trans>iOS &amp; Android (manual)</Trans>,
renderIcon: props => <Cordova {...props} />,
helpPage: '/publishing/android_and_ios_with_cordova',
description: (
<Trans>
Build the game locally as a Cordova project, and export it manually to
iOS or Android with Cordova developers tools.
</Trans>
),
key: 'localcordovaexport',
exportPipeline: localCordovaExportPipeline,
advanced: true,
},
{
name: <Trans>Windows/macOS/Linux</Trans>,
renderIcon: props => <LaptopMac {...props} />,
helpPage: '/publishing/windows-macos-linux',
description: (
<Trans>
Package your game as an app for Windows, macOS or Linux directly from
GDevelop.
</Trans>
),
key: 'localonlineelectronexport',
...onlineElectronExporter,
exportPipeline: localOnlineElectronExportPipeline,
},
{
name: <Trans>Windows/macOS/Linux (manual)</Trans>,
renderIcon: props => <LaptopMac {...props} />,
helpPage: '/publishing/windows-macos-linux-with-electron',
description: (
<Trans>
Build the game locally and export it manually to Windows, macOS or Linux
with third-party developer tools.
</Trans>
),
key: 'localelectronexport',
exportPipeline: localElectronExportPipeline,
advanced: true,
...facebookInstantGamesExporter,
exportPipeline: localFacebookInstantGamesExportPipeline,
},
];
export const localManualExporters: Array<Exporter> = [
{
...html5Exporter,
exportPipeline: localHTML5ExportPipeline,
},
{
...cordovaExporter,
exportPipeline: localCordovaExportPipeline,
},
{
...electronExporter,
exportPipeline: localElectronExportPipeline,
},
];

View File

@@ -10,7 +10,6 @@ import { showErrorBox } from '../UI/Messages/MessageBox';
import PlaceholderError from '../UI/PlaceholderError';
import PlaceholderLoader from '../UI/PlaceholderLoader';
import RaisedButton from '../UI/RaisedButton';
import Text from '../UI/Text';
import {
type Game,
getGame,
@@ -69,13 +68,13 @@ export const GameRegistration = ({
const { id } = profile;
setError(null);
setUnavailableReason(null);
try {
const game = await getGame(
getAuthorizationHeader,
id,
project.getProjectUuid()
);
setUnavailableReason(null);
setGame(game);
} catch (err) {
console.log(err);
@@ -292,33 +291,21 @@ export const GameRegistrationWidget = ({
kind="info"
renderRightButton={() => (
<RaisedButton
label={<Trans>Get weekly game stats</Trans>}
label={<Trans>Get game stats</Trans>}
disabled={acceptGameStatsEmailInProgress}
primary
onClick={onAcceptGameStatsEmail}
/>
)}
>
<Trans>
You are not receiving game stats regularly. Click this button to
receive weekly game stats on the games you publish, like the number
of weekly sessions or the total sessions in the last year.
</Trans>
<Trans>Get stats about your game every week!</Trans>
</AlertMessage>
);
}
return (
<ColumnStackLayout noMargin>
<Text>
<Trans>
Your project is registered online. This allows you to get access to
metrics collected anonymously, like the number of daily players and
retention of the players after a few days.
</Trans>
</Text>
<Line justifyContent="center">
<RaisedButton
primary
icon={<TimelineIcon />}
label={<Trans>Analytics</Trans>}
onClick={() => {
@@ -328,7 +315,6 @@ export const GameRegistrationWidget = ({
/>
<Spacer />
<RaisedButton
primary
icon={<MonetizationOnIcon />}
label={<Trans>Monetization</Trans>}
onClick={() => {

View File

@@ -8,6 +8,7 @@ import {
type ShowcasedGame,
} from '../Utils/GDevelopServices/Game';
import { useSearchItem } from '../UI/Search/UseSearchItem';
import { shuffle } from 'lodash';
const defaultSearchText = '';
@@ -15,6 +16,7 @@ type GamesShowcaseState = {|
filters: ?Filters,
searchResults: ?Array<ShowcasedGame>,
fetchShowcasedGamesAndFilters: () => void,
allShowcasedGames: ?Array<ShowcasedGame>,
error: ?Error,
searchText: string,
setSearchText: string => void,
@@ -25,6 +27,7 @@ export const GamesShowcaseContext = React.createContext<GamesShowcaseState>({
filters: null,
searchResults: null,
fetchShowcasedGamesAndFilters: () => {},
allShowcasedGames: null,
error: null,
searchText: '',
setSearchText: () => {},
@@ -59,6 +62,10 @@ export const GamesShowcaseStateProvider = ({
const [showcasedGamesByName, setShowcasedGamesByName] = React.useState<?{
[string]: ShowcasedGame,
}>(null);
const [
allShowcasedGames,
setAllShowcasedGames,
] = React.useState<?Array<ShowcasedGame>>(null);
const [filters, setFilters] = React.useState<?Filters>(null);
const [error, setError] = React.useState<?Error>(null);
const isLoading = React.useRef<boolean>(false);
@@ -79,6 +86,7 @@ export const GamesShowcaseStateProvider = ({
try {
const allShowcasedGames: AllShowcasedGames = await listAllShowcasedGames();
const { showcasedGames, filters } = allShowcasedGames;
setAllShowcasedGames(shuffle(showcasedGames));
const showcasedGamesByName = {};
showcasedGames.forEach(showcasedGame => {
@@ -117,6 +125,7 @@ export const GamesShowcaseStateProvider = ({
() => ({
searchResults,
fetchShowcasedGamesAndFilters,
allShowcasedGames,
filters,
error,
searchText,
@@ -125,6 +134,7 @@ export const GamesShowcaseStateProvider = ({
}),
[
searchResults,
allShowcasedGames,
error,
filters,
searchText,

View File

@@ -0,0 +1,146 @@
// @flow
import { Trans } from '@lingui/macro';
import * as React from 'react';
import GetAppIcon from '@material-ui/icons/GetApp';
import PlayArrowIcon from '@material-ui/icons/PlayArrow';
import YouTubeIcon from '@material-ui/icons/YouTube';
import FlashOnIcon from '@material-ui/icons/FlashOn';
import { sendShowcaseGameLinkOpened } from '../Utils/Analytics/EventSender';
import Window from '../Utils/Window';
import {
type ShowcasedGame,
type ShowcasedGameLink,
} from '../Utils/GDevelopServices/Game';
import { ColumnStackLayout, ResponsiveLineStackLayout } from '../UI/Layout';
import { Spacer } from '../UI/Grid';
import RaisedButton from '../UI/RaisedButton';
import GooglePlay from '../UI/CustomSvgIcons/GooglePlay';
import Steam from '../UI/CustomSvgIcons/Steam';
import Twitter from '../UI/CustomSvgIcons/Twitter';
import Instagram from '../UI/CustomSvgIcons/Instagram';
import Twitch from '../UI/CustomSvgIcons/Twitch';
import ItchIo from '../UI/CustomSvgIcons/ItchIo';
import Microsoft from '../UI/CustomSvgIcons/Microsoft';
import Apple from '../UI/CustomSvgIcons/Apple';
import { useResponsiveWindowWidth } from '../UI/Reponsive/ResponsiveWindowMeasurer';
const getFullUrl = url =>
url.startsWith('/') ? 'https://gdevelop-app.com' + url : url;
const getLinkIconAndLabel = (url: string, type: string) => {
switch (type) {
// Supported links:
case '':
return {
icon: <PlayArrowIcon />,
label: <Trans>Play or download</Trans>,
};
case 'learn-more':
return { icon: null, label: <Trans>Learn more</Trans> };
case 'download-win-mac-linux':
case 'download':
return { icon: <GetAppIcon />, label: <Trans>Download</Trans> };
case 'play':
return { icon: <PlayArrowIcon />, label: <Trans>Play</Trans> };
// Officially supported stores/websites/social medias:
case 'App Store':
return { icon: <Apple />, label: 'iOS' };
case 'Play Store':
return { icon: <GooglePlay />, label: 'Android' };
case 'Steam':
return { icon: <Steam />, label: 'Steam' };
case 'Trailer':
return { icon: <YouTubeIcon />, label: <Trans>Trailer</Trans> };
case 'itch.io':
return { icon: <ItchIo />, label: 'itch.io' };
case 'Game Jolt':
return { icon: <FlashOnIcon />, label: 'Game Jolt' };
case 'Twitter':
return { icon: <Twitter />, label: 'Twitter' };
case 'Microsoft Store':
return { icon: <Microsoft />, label: 'Microsoft Store' };
case 'Instagram':
return { icon: <Instagram />, label: 'Instagram' };
case 'Twitch':
return { icon: <Twitch />, label: 'Twitch' };
default:
return { icon: null, label: type };
}
};
const LinkButton = ({
link,
showcasedGame,
}: {|
link: ShowcasedGameLink,
showcasedGame: ShowcasedGame,
|}) => {
const { url, type } = link;
return (
<RaisedButton
key={type + '-' + url}
primary
{...getLinkIconAndLabel(url, type)}
onClick={() => {
sendShowcaseGameLinkOpened(showcasedGame.title, type);
Window.openExternalURL(getFullUrl(url));
}}
/>
);
};
const renderLayout = (
children: Array<React.Node>,
forceColumn: boolean
): React.Node =>
forceColumn ? (
<ColumnStackLayout justifyContent="flex-end" noMargin>
{children}
</ColumnStackLayout>
) : (
<ResponsiveLineStackLayout justifyContent="flex-end" noColumnMargin>
{children}
</ResponsiveLineStackLayout>
);
const ShowcasedGameButton = ({
showcasedGame,
forceColumn = false,
}: {|
showcasedGame: ShowcasedGame,
forceColumn?: boolean,
|}): React.Node => {
const { links } = showcasedGame;
const windowWidth = useResponsiveWindowWidth();
const firstLinks = links.slice(0, 3);
const otherLinks = links.slice(3);
return (
<>
{renderLayout(
firstLinks.map((link, index) => (
<LinkButton key={index} link={link} showcasedGame={showcasedGame} />
)),
forceColumn
)}
{otherLinks.length > 0 && (
<>
{(windowWidth === 'small' || forceColumn) && <Spacer />}
{renderLayout(
otherLinks.map((link, index) => (
<LinkButton
key={index}
link={link}
showcasedGame={showcasedGame}
/>
)),
forceColumn
)}
</>
)}
</>
);
};
export default ShowcasedGameButton;

View File

@@ -0,0 +1,62 @@
// @flow
import * as React from 'react';
import { Trans } from '@lingui/macro';
import Text from '../UI/Text';
import { type ShowcasedGame } from '../Utils/GDevelopServices/Game';
import Dialog from '../UI/Dialog';
import FlatButton from '../UI/FlatButton';
import TagChips from '../UI/TagChips';
import { MarkdownText } from '../UI/MarkdownText';
import { Column, Spacer } from '../UI/Grid';
import { CorsAwareImage } from '../UI/CorsAwareImage';
import ShowcasedGameButton from './ShowcasedGameButtons';
import ShowcasedGameTitle from './ShowcasedGameTitle';
const styles = {
image: { width: '100%', maxHeight: '300px', objectFit: 'cover' },
};
const ShowcasedGameDialog = ({
open,
onClose,
showcasedGame,
}: {|
open: boolean,
onClose: () => void,
showcasedGame: ShowcasedGame,
|}): React.Node => {
return (
<Dialog
maxWidth="xs"
open={open}
actions={
<FlatButton
label={<Trans>Close</Trans>}
key="close"
primary={false}
onClick={onClose}
/>
}
>
<Column noMargin expand>
<CorsAwareImage
style={styles.image}
src={showcasedGame.thumbnailUrl}
alt={showcasedGame.title}
/>
<ShowcasedGameTitle showcasedGame={showcasedGame} forceColumn />
{showcasedGame.genres.length ? (
<TagChips tags={showcasedGame.genres} />
) : null}
<Text size="body2" displayInlineAsSpan>
<MarkdownText source={showcasedGame.description} allowParagraphs />
</Text>
<Spacer />
<ShowcasedGameButton showcasedGame={showcasedGame} forceColumn />
</Column>
</Dialog>
);
};
export default ShowcasedGameDialog;

View File

@@ -1,33 +1,16 @@
// @flow
import * as React from 'react';
import {
type ShowcasedGame,
type ShowcasedGameLink,
} from '../Utils/GDevelopServices/Game';
import { type ShowcasedGame } from '../Utils/GDevelopServices/Game';
import { Card } from '@material-ui/core';
import Text from '../UI/Text';
import { Trans } from '@lingui/macro';
import { Column, Line, Spacer } from '../UI/Grid';
import { MarkdownText } from '../UI/MarkdownText';
import { CorsAwareImage } from '../UI/CorsAwareImage';
import RaisedButton from '../UI/RaisedButton';
import TagChips from '../UI/TagChips';
import Apple from '../UI/CustomSvgIcons/Apple';
import Window from '../Utils/Window';
import { sendShowcaseGameLinkOpened } from '../Utils/Analytics/EventSender';
import GooglePlay from '../UI/CustomSvgIcons/GooglePlay';
import Steam from '../UI/CustomSvgIcons/Steam';
import Twitter from '../UI/CustomSvgIcons/Twitter';
import Instagram from '../UI/CustomSvgIcons/Instagram';
import Twitch from '../UI/CustomSvgIcons/Twitch';
import ItchIo from '../UI/CustomSvgIcons/ItchIo';
import Microsoft from '../UI/CustomSvgIcons/Microsoft';
import GetAppIcon from '@material-ui/icons/GetApp';
import PlayArrowIcon from '@material-ui/icons/PlayArrow';
import YouTubeIcon from '@material-ui/icons/YouTube';
import FlashOnIcon from '@material-ui/icons/FlashOn';
import { ResponsiveLineStackLayout } from '../UI/Layout';
import { useResponsiveWindowWidth } from '../UI/Reponsive/ResponsiveWindowMeasurer';
import ShowcasedGameTitle from './ShowcasedGameTitle';
import ShowcasedGameButton from './ShowcasedGameButtons';
const styles = {
thumbnailImageWithDescription: {
@@ -65,67 +48,6 @@ type Props = {|
onHeightComputed: number => void,
|};
const getFullUrl = url =>
url.startsWith('/') ? 'https://gdevelop-app.com' + url : url;
const getLinkIconAndLabel = (url: string, type: string) => {
// Supported links:
if (type === '') {
return { icon: <PlayArrowIcon />, label: <Trans>Play or download</Trans> };
} else if (type === 'learn-more') {
return { icon: null, label: <Trans>Learn more</Trans> };
} else if (type === 'download' || type === 'download-win-mac-linux') {
return { icon: <GetAppIcon />, label: <Trans>Download</Trans> };
} else if (type === 'play') {
return { icon: <PlayArrowIcon />, label: <Trans>Play</Trans> };
}
// Officially supported stores/websites/social medias:
else if (type === 'App Store') {
return { icon: <Apple />, label: 'iOS' };
} else if (type === 'Play Store') {
return { icon: <GooglePlay />, label: 'Android' };
} else if (type === 'Steam') {
return { icon: <Steam />, label: 'Steam' };
} else if (type === 'Trailer') {
return { icon: <YouTubeIcon />, label: <Trans>Trailer</Trans> };
} else if (type === 'itch.io') {
return { icon: <ItchIo />, label: 'itch.io' };
} else if (type === 'Game Jolt') {
return { icon: <FlashOnIcon />, label: 'Game Jolt' };
} else if (type === 'Twitter') {
return { icon: <Twitter />, label: 'Twitter' };
} else if (type === 'Microsoft Store') {
return { icon: <Microsoft />, label: 'Microsoft Store' };
} else if (type === 'Instagram') {
return { icon: <Instagram />, label: 'Instagram' };
} else if (type === 'Twitch') {
return { icon: <Twitch />, label: 'Twitch' };
}
return { icon: null, label: type };
};
const LinkButton = ({
link,
showcasedGame,
}: {|
link: ShowcasedGameLink,
showcasedGame: ShowcasedGame,
|}) => {
const { url, type } = link;
return (
<RaisedButton
key={type + '-' + url}
primary
{...getLinkIconAndLabel(url, type)}
onClick={() => {
sendShowcaseGameLinkOpened(showcasedGame.title, type);
Window.openExternalURL(getFullUrl(url));
}}
/>
);
};
export const ShowcasedGameListItem = ({
showcasedGame,
onHeightComputed,
@@ -152,9 +74,6 @@ export const ShowcasedGameListItem = ({
const windowWidth = useResponsiveWindowWidth();
const hasDescription = !!showcasedGame.description;
const firstLinks = showcasedGame.links.slice(0, 3);
const otherLinks = showcasedGame.links.slice(3);
return (
<div
style={{
@@ -187,14 +106,7 @@ export const ShowcasedGameListItem = ({
/>
<Line expand>
<Column expand>
<ResponsiveLineStackLayout noMargin alignItems="baseline" expand>
<Text noMargin displayInlineAsSpan>
{showcasedGame.title}
</Text>
<Text noMargin size="body2" displayInlineAsSpan>
<Trans>by {showcasedGame.author}</Trans>
</Text>
</ResponsiveLineStackLayout>
<ShowcasedGameTitle showcasedGame={showcasedGame} />
{showcasedGame.genres.length ? (
<TagChips tags={showcasedGame.genres} />
) : null}
@@ -205,30 +117,7 @@ export const ShowcasedGameListItem = ({
/>
</Text>
<Spacer />
<ResponsiveLineStackLayout
justifyContent="flex-end"
noColumnMargin
>
{firstLinks.map((link, index) => (
<LinkButton
key={index}
link={link}
showcasedGame={showcasedGame}
/>
))}
</ResponsiveLineStackLayout>
<ResponsiveLineStackLayout
justifyContent="flex-end"
noColumnMargin
>
{otherLinks.map((link, index) => (
<LinkButton
key={index}
link={link}
showcasedGame={showcasedGame}
/>
))}
</ResponsiveLineStackLayout>
<ShowcasedGameButton showcasedGame={showcasedGame} />
</Column>
</Line>
</ResponsiveLineStackLayout>

View File

@@ -0,0 +1,31 @@
// @flow
import { Trans } from '@lingui/macro';
import * as React from 'react';
import Text from '../UI/Text';
import { type ShowcasedGame } from '../Utils/GDevelopServices/Game';
import { ColumnStackLayout, ResponsiveLineStackLayout } from '../UI/Layout';
const ShowcasedGameTitle = ({
showcasedGame,
forceColumn = false,
}: {|
showcasedGame: ShowcasedGame,
forceColumn?: boolean,
|}): React.Node => {
const { title, author } = showcasedGame;
const StackLayout = forceColumn
? ColumnStackLayout
: ResponsiveLineStackLayout;
return (
<StackLayout noMargin alignItems="baseline" expand>
<Text noMargin displayInlineAsSpan>
{title}
</Text>
<Text noMargin size="body2" displayInlineAsSpan>
<Trans>by {author}</Trans>
</Text>
</StackLayout>
);
};
export default ShowcasedGameTitle;

View File

@@ -8,28 +8,39 @@ import Window from '../Utils/Window';
import RaisedButton from '../UI/RaisedButton';
import YouTubeIcon from '@material-ui/icons/YouTube';
import MenuBookIcon from '@material-ui/icons/MenuBook';
import { type TutorialHint } from '.';
import { TutorialContext } from '../Tutorial/TutorialContext';
import { type Tutorial } from '../Utils/GDevelopServices/Tutorial';
type Props = {|
tutorialHint: TutorialHint,
tutorialId: string,
|};
/**
* Show a link to a tutorial that can be permanently hidden. Hidden tutorials
* will be stored in preferences.
*/
const DismissableTutorialMessage = ({ tutorialHint }: Props) => {
const DismissableTutorialMessage = ({ tutorialId }: Props) => {
const preferences = React.useContext(PreferencesContext);
const { values, showTutorialHint } = preferences;
const { tutorials } = React.useContext(TutorialContext);
if (values.hiddenTutorialHints[tutorialHint.identifier]) return null;
if (values.hiddenTutorialHints[tutorialId]) return null;
if (!tutorials) return null; // Loading or errored, do not display the tutorial.
const tutorial: ?Tutorial = tutorials.find(
tutorial => tutorial.id === tutorialId
);
if (!tutorial) {
console.warn(`Tutorial ${tutorialId} not found`);
return null;
}
return (
<I18n>
{({ i18n }) => (
<AlertMessage
kind={'info'}
children={i18n._(tutorialHint.message)}
children={tutorial.title}
renderLeftIcon={() => (
<img
alt=""
@@ -37,32 +48,28 @@ const DismissableTutorialMessage = ({ tutorialHint }: Props) => {
maxWidth: 128,
maxHeight: 128,
}}
src={tutorialHint.iconSrc}
src={tutorial.thumbnailUrl}
/>
)}
renderRightButton={() => (
<RaisedButton
icon={
tutorialHint.kind === 'video-tutorial' ? (
<YouTubeIcon />
) : (
<MenuBookIcon />
)
tutorial.type === 'video' ? <YouTubeIcon /> : <MenuBookIcon />
}
label={
tutorialHint.kind === 'video-tutorial' ? (
tutorial.type === 'video' ? (
<Trans>Watch the tutorial</Trans>
) : (
<Trans>Read the tutorial</Trans>
)
}
onClick={() => {
Window.openExternalURL(tutorialHint.link);
Window.openExternalURL(tutorial.link);
}}
/>
)}
onHide={() => {
showTutorialHint(tutorialHint.identifier, false);
showTutorialHint(tutorialId, false);
}}
/>
)}

View File

@@ -2,22 +2,12 @@
import { t } from '@lingui/macro';
import { type MessageDescriptor } from '../Utils/i18n/MessageDescriptor.flow';
import { type AlertMessageIdentifier } from '../MainFrame/Preferences/PreferencesContext';
import { getHelpLink } from '../Utils/HelpLink';
export type Hint = {|
kind: 'warning' | 'info',
message: MessageDescriptor,
identifier?: AlertMessageIdentifier,
|};
export type TutorialHint = {|
kind: 'tutorial' | 'video-tutorial',
name: string,
message: MessageDescriptor,
iconSrc: ?string,
link: string,
identifier: string,
featuredForGettingStarted?: boolean,
|};
export const getDeprecatedBehaviorsInformation = (): {
[string]: {| warning: MessageDescriptor |},
@@ -160,303 +150,7 @@ export const getExtraInstructionInformation = (type: string): ?Hint => {
return null;
};
const tutorialHints = {
'geometry-monster': {
kind: 'tutorial',
iconSrc: 'res/tutorial_icons/geometry-monster.png',
name: 'Geometry Monster Tutorial',
message: t`Make a hyper-casual mobile game where the player must grab shapes and avoid bombs.`,
link: getHelpLink('/tutorials/geometry-monster'),
identifier: 'geometry-monster',
featuredForGettingStarted: true,
},
platformer: {
kind: 'tutorial',
iconSrc: 'res/tutorial_icons/platformer.png',
name: 'Platformer Tutorial',
message: t`Make a platform game from scratch.`,
link: getHelpLink('/tutorials/platformer/start'),
identifier: 'platformer',
featuredForGettingStarted: true,
},
'space-shooter': {
kind: 'tutorial',
iconSrc: 'res/tutorial_icons/space-shooter.png',
name: 'Space Shooter Tutorial',
message: t`Make a space shooter game from scratch.`,
link: getHelpLink('/tutorials/space-shooter'),
identifier: 'space-shooter',
featuredForGettingStarted: true,
},
'simple-game-physics-particles': {
kind: 'video-tutorial',
iconSrc: 'res/tutorial_icons/simple-game-physics-particles.jpg',
name: 'How to Create a Simple Game with Physics and Particles',
message: t`Create a game from scratch using physics and particles.`,
link: 'https://www.youtube.com/watch?v=w8B84Dpgkjc',
identifier: 'simple-game-physics-particles',
featuredForGettingStarted: true,
},
'tank-shooter': {
kind: 'tutorial',
iconSrc: null,
name: 'Tank Shooter Tutorial',
message: t`Make a simple tank shooter game from scratch.`,
link: getHelpLink('/tutorials/tank-shooter'),
identifier: 'tank-shooter',
},
'endless-runner': {
kind: 'tutorial',
iconSrc: null,
name: 'Endless Runner Tutorial',
message: t`Make a simple game where the player must jump on platforms for as long as possible.`,
link: getHelpLink('/tutorials/endless-runner'),
identifier: 'endless-runner',
},
'endless-car-game': {
kind: 'tutorial',
iconSrc: null,
name: 'Endless Car Game Tutorial',
message: t`Create a simple game where you must dodge the cars on the road.`,
link: getHelpLink('/tutorials/roadrider'),
identifier: 'endless-car-game',
},
'breakout-tutorial': {
kind: 'tutorial',
iconSrc: null,
name: 'Breakout Tutorial',
message: t`Create a simple breakout game where you must destroy all the bricks on the screen.`,
link: getHelpLink('/tutorials/breakout'),
identifier: 'breakout-tutorial',
},
'screen-shake-timer-variables': {
kind: 'video-tutorial',
iconSrc: 'res/tutorial_icons/screen-shake-timer-variables.jpg',
name: 'Screen Shake Effect with Timers and Variables',
message: t`Learn how to add a screen shake effect when the player falls from a very high platform in a platformer.`,
link: 'https://www.youtube.com/watch?v=0w0NGuj4OFQ',
identifier: 'screen-shake-timer-variables',
},
'ghost-enemy-following-player': {
kind: 'video-tutorial',
iconSrc: 'res/tutorial_icons/ghost-enemy-following-player.jpg',
name: 'Ghost Enemy Following the Player',
message: t`Make a ghost like enemy floating toward the player.`,
link: 'https://www.youtube.com/watch?v=SLUlnhKuuqE',
identifier: 'ghost-enemy-following-player',
},
'melee-sword-attack': {
kind: 'video-tutorial',
iconSrc: 'res/tutorial_icons/melee-sword-attack.jpg',
name: 'Melee/Sword Attack',
message: t`Learn how to make a melee/sword attack with a randomly triggered animation each time a key is pressed.`,
link: 'https://www.youtube.com/watch?v=3XT40kDRp8g',
identifier: 'melee-sword-attack',
},
'physics-engine-platformer-game': {
kind: 'video-tutorial',
iconSrc: 'res/tutorial_icons/physics-engine-platformer-game.jpg',
name: 'Platformer with the physics engine',
message: t`Learn how to make a platformer game using the physics engine.`,
link: 'https://www.youtube.com/watch?v=96gNCmnQwaE',
identifier: 'physics-engine-platformer-game',
},
'tween-behavior': {
kind: 'video-tutorial',
iconSrc: 'res/tutorial_icons/tween-behavior.jpg',
name: 'Tween Behavior',
message: t`Learn how to use the Tween Behavior and how it can be used to add more life and animation to you projects.`,
link: 'https://www.youtube.com/watch?v=SLqnwC9D5Q4',
identifier: 'tween-behavior',
},
'responsive-ui': {
kind: 'video-tutorial',
iconSrc: 'res/tutorial_icons/responsive-ui.jpg',
name: 'Reponsive UI',
message: t`Learn how to add responsive UI using anchors.`,
link: 'https://www.youtube.com/watch?v=VgrEhg0esCg',
identifier: 'responsive-ui',
},
'smooth-camera-movement': {
kind: 'video-tutorial',
iconSrc: 'res/tutorial_icons/smooth-camera-movement.jpg',
name: 'Smooth Camera Movement',
message: t`Learn how to make the camera follow the player in a smooth movement.`,
link: 'https://www.youtube.com/watch?v=yUNisggNh7s',
identifier: 'smooth-camera-movement',
},
'pause-menu': {
kind: 'video-tutorial',
iconSrc: 'res/tutorial_icons/pause-menu.jpg',
name: 'Pause Menu',
message: t`Learn how to stop the time and make a pause menu.`,
link: 'https://www.youtube.com/watch?v=k2J784esdkc',
identifier: 'pause-menu',
},
'character-selection-feature': {
kind: 'video-tutorial',
iconSrc: 'res/tutorial_icons/character-selection-feature.jpg',
name: 'Character Selection',
message: t`Learn how to add a selector to choose a character (or anything else) in your game.`,
link: 'https://www.youtube.com/watch?v=8DpsjXHd4ro',
identifier: 'character-selection-feature',
},
'push-objects': {
kind: 'video-tutorial',
iconSrc: 'res/tutorial_icons/push-objects.jpg',
name: 'Push Objects',
message: t`Learn how to push objects, like a box, in a platform game.`,
link: 'https://www.youtube.com/watch?v=11tjJ0JgYuk',
identifier: 'push-objects',
},
'save-and-load': {
kind: 'video-tutorial',
iconSrc: 'res/tutorial_icons/save-and-load.jpg',
name: 'Save and Load',
message: t`Learn how to save the player progress, and other information, and to load them again later.`,
link: 'https://www.youtube.com/watch?v=bXUGJqHhuCo',
identifier: 'save-and-load',
},
'particle-effects': {
kind: 'video-tutorial',
iconSrc: 'res/tutorial_icons/particle-effects.jpg',
name: 'Particle Effects',
message: t`Learn how to use particle emitters in GDevelop to create effects like fire, explosion, magic beam, etc...`,
link: 'https://www.youtube.com/watch?v=7sqMmTntvKs',
identifier: 'particle-effects',
},
'opening-chest': {
kind: 'video-tutorial',
iconSrc: 'res/tutorial_icons/opening-chest.jpg',
name: 'Open a Loot Chest',
message: t`How to open a loot chest with a key that the player can find in the level`,
link: 'https://www.youtube.com/watch?v=1qsCgwFtYfg',
identifier: 'opening-chest',
},
'health-bar-and-health-potion': {
kind: 'video-tutorial',
iconSrc: 'res/tutorial_icons/health-bar-and-health-potion.jpg',
name: 'Create a Health Bar and Health Potion',
message: t`How to show a health bar on screen and a potion to give back health to the player.`,
link: 'https://www.youtube.com/watch?v=P-scQW7PeVg',
identifier: 'health-bar-and-health-potion',
},
'touch-360-joystick-controller': {
kind: 'video-tutorial',
iconSrc: 'res/tutorial_icons/touch-360-joystick-controller.jpg',
name: 'Create a Touch 360 Joystick Controller',
message: t`How to create a joystick displayed on screen, useful to control the player in mobile games.`,
link: 'https://www.youtube.com/watch?v=-k-bVU3QrfA',
identifier: 'touch-360-joystick-controller',
},
'flickering-dynamic-light-effect': {
kind: 'video-tutorial',
iconSrc: 'res/tutorial_icons/flickering-dynamic-light-effect.jpg',
name: 'Create a Flickering Dynamic Light Effect',
message: t`Learn how to create a dynamic light following the player, with a flickering effect.`,
link: 'https://www.youtube.com/watch?v=HolCWx4E0TU',
identifier: 'flickering-dynamic-light-effect',
},
'2d-platformer-shooter': {
kind: 'video-tutorial',
iconSrc: 'res/tutorial_icons/2d-platformer-shooter.jpg',
name: 'Create a 2D Platformer Shooter',
message: t`Create a 2D platform game where the player can shoot at enemies chasing him.`,
link: 'https://www.youtube.com/watch?v=OOw3Sh6rga8',
identifier: '2d-platformer-shooter',
},
'animated-buttons': {
kind: 'video-tutorial',
iconSrc: 'res/tutorial_icons/animated-buttons.jpg',
name: 'Create Animated Buttons',
message: t`Create animated buttons that can be shown in your game menus (main menu, selection screen, etc...).`,
link: 'https://www.youtube.com/watch?v=7_oLY_x4vEk',
identifier: 'animated-buttons',
},
'simple-trampoline-platformer': {
kind: 'video-tutorial',
iconSrc: 'res/tutorial_icons/simple-trampoline-platformer.jpg',
name: 'Make a Simple Trampoline/Jump Pad',
message: t`Create a trampoline in your platformer game, making the player jump very high when stepped on.`,
link: 'https://www.youtube.com/watch?v=p42i4omA7j8',
identifier: 'simple-trampoline-platformer',
},
'2d-car-physics-movement': {
kind: 'video-tutorial',
iconSrc: 'res/tutorial_icons/2d-car-physics-movement.jpg',
name: 'How to Make a 2D Car or Bike Movement With Physics Engine',
message: t`Learn how to create a physics based car movement.`,
link: 'https://www.youtube.com/watch?v=_-fX755cctU',
identifier: '2d-car-physics-movement',
},
};
const allTutorialHints = Object.keys(tutorialHints).map(
identifier => tutorialHints[identifier]
);
export const getObjectTutorialHints = (type: string): Array<TutorialHint> => {
if (type === 'ParticleSystem::ParticleEmitter') {
return [tutorialHints['particle-effects']];
}
if (type === 'Lighting::LightObject') {
return [tutorialHints['flickering-dynamic-light-effect']];
}
return [];
};
export const getBehaviorTutorialHints = (type: string): Array<TutorialHint> => {
if (type === 'Tween::TweenBehavior') {
return [tutorialHints['tween-behavior']];
}
if (type === 'AnchorBehavior::AnchorBehavior') {
return [tutorialHints['responsive-ui']];
}
if (type === 'Physics2::Physics2Behavior') {
return [
tutorialHints['physics-engine-platformer-game'],
tutorialHints['2d-car-physics-movement'],
];
}
return [];
};
export const getInstructionTutorialHints = (
type: string
): Array<TutorialHint> => {
if (
[
'CameraX',
'CameraY',
'RotateCamera',
'ZoomCamera',
'FixCamera',
'CentreCamera',
].includes(type)
) {
return [tutorialHints['smooth-camera-movement']];
}
if (type === 'ChangeTimeScale') {
return [tutorialHints['pause-menu']];
}
if (
[
'EcrireFichierExp',
'EcrireFichierTxt',
'LireFichierExp',
'LireFichierTxt',
].includes(type)
) {
return [tutorialHints['save-and-load']];
}
if (type === 'PlatformBehavior::SimulateJumpKey') {
return [tutorialHints['simple-trampoline-platformer']];
}
return [];
};
export const getAllTutorialHints = (): Array<TutorialHint> => allTutorialHints;
export const getWarningAboutPlatformerCollisionMaskEditing = (): ?Hint => ({
kind: 'warning',
message: t`You currently use a custom collision mask for an object that has the Platformer behavior. A platformer with a custom collision mask could experience issues if it is flipped horizontally near a wall. To avoid that, make sure your collision mask does not move when flipping it by centering it.`,
});

View File

@@ -10,7 +10,7 @@ const defaultShortcuts: ShortcutMap = {
LAUNCH_DEBUG_PREVIEW: 'F6',
HOT_RELOAD_PREVIEW: 'F5',
LAUNCH_NETWORK_PREVIEW: 'F8',
OPEN_START_PAGE: '',
OPEN_HOME_PAGE: '',
CREATE_NEW_PROJECT: 'CmdOrCtrl+Alt+KeyN',
OPEN_PROJECT: 'CmdOrCtrl+KeyO',
SAVE_PROJECT: 'CmdOrCtrl+KeyS',

View File

@@ -5,15 +5,18 @@ import Window from './Utils/Window';
import ExportDialog from './Export/ExportDialog';
import CreateProjectDialog from './ProjectCreation/CreateProjectDialog';
import Authentication from './Utils/GDevelopServices/Authentication';
import './UI/iconmoon-font.css'; // Styles for Iconmoon font.
import './UI/icomoon-font.css'; // Styles for Icomoon font.
// Import for Electron powered IDE.
import LocalExamples from './ProjectCreation/LocalExamples';
import LocalStarters from './ProjectCreation/LocalStarters';
import localResourceSources from './ResourcesList/LocalResourceSources';
import localResourceExternalEditors from './ResourcesList/LocalResourceExternalEditors';
import LocalPreviewLauncher from './Export/LocalExporters/LocalPreviewLauncher';
import { getLocalExporters } from './Export/LocalExporters';
import {
localAutomatedExporters,
localManualExporters,
localOnlineWebExporter,
} from './Export/LocalExporters';
import ElectronMainMenu from './MainFrame/ElectronMainMenu';
import makeExtensionsLoader from './JsExtensionsLoader/LocalJsExtensionsLoader';
import { makeLocalEventsFunctionCodeWriter } from './EventsFunctionsExtensionsLoader/CodeWriters/LocalEventsFunctionCodeWriter';
@@ -26,6 +29,7 @@ import ProjectStorageProviders from './ProjectsStorage/ProjectStorageProviders';
import LocalFileStorageProvider from './ProjectsStorage/LocalFileStorageProvider';
import { LocalGDJSDevelopmentWatcher } from './GameEngineFinder/LocalGDJSDevelopmentWatcher';
import { LocalResourceFetcher } from './ProjectsStorage/ResourceFetcher/LocalResourceFetcher';
import { onCreateFromExampleShortHeader, onCreateBlank } from './ProjectCreation/services/LocalCreation';
const gd: libGDevelop = global.gd;
@@ -49,7 +53,8 @@ export const create = (authentication: Authentication) => {
appArguments={appArguments}
storageProviders={
// Add Url provider
[LocalFileStorageProvider]}
[LocalFileStorageProvider]
}
defaultStorageProvider={LocalFileStorageProvider}
>
{({
@@ -65,18 +70,29 @@ export const create = (authentication: Authentication) => {
<LocalPreviewLauncher {...props} ref={ref} />
)}
renderExportDialog={props => (
<ExportDialog {...props} exporters={getLocalExporters()} />
<ExportDialog
project={props.project}
onChangeSubscription={props.onChangeSubscription}
onClose={props.onClose}
automatedExporters={localAutomatedExporters}
manualExporters={localManualExporters}
onlineWebExporter={localOnlineWebExporter}
/>
)}
renderCreateDialog={props => (
<CreateProjectDialog
{...props}
examplesComponent={LocalExamples}
startersComponent={LocalStarters}
onCreateFromExampleShortHeader={
onCreateFromExampleShortHeader
}
/>
)}
renderGDJSDevelopmentWatcher={
isDev ? () => <LocalGDJSDevelopmentWatcher /> : null
}
onCreateFromExampleShortHeader={onCreateFromExampleShortHeader}
onCreateBlank={onCreateBlank}
storageProviders={storageProviders}
getStorageProviderOperations={getStorageProviderOperations}
getStorageProvider={getStorageProvider}

View File

@@ -231,6 +231,11 @@ const contributors = [
description: 'The GDevelop embassador in Brazil',
link: 'https://twitter.com/ittaloxd',
},
{
name: 'PANDAKO',
description: 'Translations in Japanese, extensions and blog',
link: 'https://gdevelop-jp.blogspot.com',
},
];
export default class AboutDialog extends PureComponent<Props, State> {

View File

@@ -1,5 +1,6 @@
// @flow
import * as React from 'react';
import { type I18n as I18nType } from '@lingui/core';
import { type PreviewOptions } from '../../Export/PreviewLauncher.flow';
import { type UnsavedChanges } from '../UnsavedChangesContext';
import {
@@ -9,6 +10,17 @@ import {
import { type PreviewDebuggerServer } from '../../Export/PreviewLauncher.flow';
import { type HotReloadPreviewButtonProps } from '../../HotReload/HotReloadPreviewButton';
import { type ResourceExternalEditor } from '../../ResourcesList/ResourceExternalEditor.flow';
import { type ExampleShortHeader } from '../../Utils/GDevelopServices/Example';
import {
type OnCreateFromExampleShortHeaderFunction,
type OnCreateBlankFunction,
type OnOpenProjectAfterCreationFunction,
} from '../../ProjectCreation/CreateProjectDialog';
import {
type StorageProvider,
type FileMetadata,
type FileMetadataAndStorageProviderName,
} from '../../ProjectsStorage';
export type RenderEditorContainerProps = {|
isActive: boolean,
@@ -52,16 +64,18 @@ export type RenderEditorContainerProps = {|
// Project opening
canOpen: boolean,
onOpen: () => void,
onCreate: () => void,
onOpenRecentFile: (file: FileMetadataAndStorageProviderName) => void,
onOpenProjectManager: () => void,
onCloseProject: () => Promise<void>,
// Other dialogs opening:
onOpenTutorials: () => void,
onOpenGamesShowcase: () => void,
onOpenExamples: () => void,
onOpenHelpFinder: () => void,
onOpenLanguageDialog: () => void,
onChangeSubscription: () => void,
onOpenProfile: () => void,
// Resources handling
onDeleteResource: (resource: gdResource, cb: (boolean) => void) => void,
@@ -70,6 +84,11 @@ export type RenderEditorContainerProps = {|
newName: string,
cb: (boolean) => void
) => void,
// Project creation
onCreateFromExampleShortHeader: OnCreateFromExampleShortHeaderFunction,
onCreateBlank: OnCreateBlankFunction,
onOpenProjectAfterCreation: OnOpenProjectAfterCreationFunction,
|};
export type RenderEditorContainerPropsWithRef = {|

View File

@@ -24,9 +24,10 @@ export class DebuggerEditorContainer extends React.Component<
};
shouldComponentUpdate(nextProps: RenderEditorContainerProps) {
// Prevent any update to the editor if the editor is not active,
// and so not visible to the user.
return nextProps.isActive;
// We stop updates when the component is inactive.
// If it's active, was active or becoming active again we let update propagate.
// Especially important to note that when becoming inactive, a "last" update is allowed.
return this.props.isActive || nextProps.isActive;
}
getProject(): ?gdProject {
@@ -75,7 +76,6 @@ export class DebuggerEditorContainer extends React.Component<
<Debugger
project={project}
setToolbar={this.props.setToolbar}
isActive={this.props.isActive}
previewDebuggerServer={previewDebuggerServer}
ref={editor => (this.editor = editor)}
/>

View File

@@ -0,0 +1,28 @@
// @flow
import * as React from 'react';
import { Paper } from '@material-ui/core';
const styles = {
scrollContainer: {
flex: 1,
display: 'flex',
overflowY: 'scroll',
},
};
type Props = {|
children: React.Node,
|};
const ScrollBackground = ({ children }: Props) => (
<Paper
style={{
...styles.scrollContainer,
}}
square
>
{children}
</Paper>
);
export default ScrollBackground;

View File

@@ -0,0 +1,565 @@
// @flow
import * as React from 'react';
import { I18n } from '@lingui/react';
import { type I18n as I18nType } from '@lingui/core';
import { Trans, t } from '@lingui/macro';
import Language from '@material-ui/icons/Language';
import ForumIcon from '@material-ui/icons/Forum';
import HelpIcon from '@material-ui/icons/Help';
import FlatButton from '../../../UI/FlatButton';
import IconButton from '../../../UI/IconButton';
import { Line, Spacer } from '../../../UI/Grid';
import RaisedButton from '../../../UI/RaisedButton';
import Carousel from '../../../UI/Carousel';
import { ResponsiveLineStackLayout } from '../../../UI/Layout';
import Window from '../../../Utils/Window';
import { TutorialContext } from '../../../Tutorial/TutorialContext';
import { type RenderEditorContainerPropsWithRef } from '../BaseEditor';
import ScrollBackground from './ScrollBackground';
import { GamesShowcaseContext } from '../../../GamesShowcase/GamesShowcaseContext';
import { type ShowcasedGame } from '../../../Utils/GDevelopServices/Game';
import ShowcasedGameDialog from '../../../GamesShowcase/ShowcasedGameDialog';
import { type ExampleShortHeader } from '../../../Utils/GDevelopServices/Example';
import { ExampleStoreContext } from '../../../AssetStore/ExampleStore/ExampleStoreContext';
import UserChip from '../../../UI/User/UserChip';
import AuthenticatedUserContext from '../../../Profile/AuthenticatedUserContext';
import { useResponsiveWindowWidth } from '../../../UI/Reponsive/ResponsiveWindowMeasurer';
import { ExampleDialog } from '../../../AssetStore/ExampleStore/ExampleDialog';
import optionalRequire from '../../../Utils/OptionalRequire';
import { findEmptyPath } from '../../../ProjectCreation/LocalPathFinder';
import LocalProjectPreCreationDialog from '../../../ProjectCreation/LocalProjectPreCreationDialog';
import {
type OnCreateFromExampleShortHeaderFunction,
type OnCreateBlankFunction,
type OnOpenProjectAfterCreationFunction,
} from '../../../ProjectCreation/CreateProjectDialog';
import RaisedButtonWithSplitMenu from '../../../UI/RaisedButtonWithSplitMenu';
import PreferencesContext from '../../Preferences/PreferencesContext';
import { type FileMetadataAndStorageProviderName } from '../../../ProjectsStorage';
const electron = optionalRequire('electron');
const path = optionalRequire('path');
const app = electron ? electron.remote.app : null;
const styles = {
container: {
display: 'flex',
flexDirection: 'column',
width: '100%',
},
content: {
display: 'flex',
flexDirection: 'column',
flex: 1,
maxWidth: 1200,
marginTop: 10,
marginBottom: 20,
padding: 0,
alignSelf: 'center',
width: '100%',
},
};
type Props = {|
project: ?gdProject,
isActive: boolean,
projectItemName: ?string,
project: ?gdProject,
setToolbar: (?React.Node) => void,
// Project opening
canOpen: boolean,
onOpen: () => void,
onOpenRecentFile: (file: FileMetadataAndStorageProviderName) => void,
onOpenExamples: () => void,
onOpenProjectManager: () => void,
onCloseProject: () => Promise<void>,
// Other dialogs opening:
onOpenTutorials: () => void,
onOpenGamesShowcase: () => void,
onOpenHelpFinder: () => void,
onOpenLanguageDialog: () => void,
onOpenProfile: () => void,
// Project creation
onCreateFromExampleShortHeader: OnCreateFromExampleShortHeaderFunction,
onCreateBlank: OnCreateBlankFunction,
onOpenProjectAfterCreation: OnOpenProjectAfterCreationFunction,
|};
type HomePageEditorInterface = {|
getProject: () => void,
updateToolbar: () => void,
forceUpdateEditor: () => void,
|};
const betweenCarouselSpacerCount = 6;
const renderBetweenCarouselSpace = (offset: number = 0) =>
Array(betweenCarouselSpacerCount)
.fill(0)
.map((e, index) => <Spacer key={`spacer${index + offset}`} />);
export const HomePage = React.memo<Props>(
React.forwardRef<Props, HomePageEditorInterface>(
(
{
project,
canOpen,
onOpen,
onOpenRecentFile,
onCreateFromExampleShortHeader,
onCreateBlank,
onOpenProjectAfterCreation,
onOpenExamples,
onOpenProjectManager,
onCloseProject,
onOpenTutorials,
onOpenGamesShowcase,
onOpenHelpFinder,
onOpenLanguageDialog,
onOpenProfile,
setToolbar,
}: Props,
ref
) => {
const getProject = () => {
return undefined;
};
const updateToolbar = () => {
if (setToolbar) setToolbar(null);
};
const forceUpdateEditor = () => {
// No updates to be done
};
React.useImperativeHandle(ref, () => ({
getProject,
updateToolbar,
forceUpdateEditor,
}));
const windowWidth = useResponsiveWindowWidth();
const authenticatedUser = React.useContext(AuthenticatedUserContext);
const { getRecentProjectFiles } = React.useContext(PreferencesContext);
const {
tutorials,
fetchTutorials,
error: tutorialLoadingError,
} = React.useContext(TutorialContext);
const {
allShowcasedGames: showcasedGames,
fetchShowcasedGamesAndFilters,
error: showcaseLoadingError,
} = React.useContext(GamesShowcaseContext);
const {
allExamples: examples,
fetchExamplesAndFilters,
error: exampleLoadingError,
} = React.useContext(ExampleStoreContext);
React.useEffect(
() => {
fetchShowcasedGamesAndFilters();
fetchExamplesAndFilters();
fetchTutorials();
},
[
fetchExamplesAndFilters,
fetchShowcasedGamesAndFilters,
fetchTutorials,
getRecentProjectFiles,
]
);
const computeDefaultProjectPath = (): string =>
app && path
? findEmptyPath(
path.join(app.getPath('documents'), 'GDevelop projects')
)
: '';
const [outputPath, setOutputPath] = React.useState<string>(
computeDefaultProjectPath()
);
const [
preCreationDialogOpen,
setPreCreationDialogOpen,
] = React.useState<boolean>(false);
const [isOpening, setIsOpening] = React.useState<boolean>(false);
const [
selectedShowcasedGame,
setSelectedShowcasedGame,
] = React.useState<?ShowcasedGame>(null);
const [
selectedExample,
setSelectedExample,
] = React.useState<?ExampleShortHeader>(null);
const buildRecentProjectFilesMenuTemplate = React.useCallback(
(i18n: I18nType) => {
const recentFiles = getRecentProjectFiles();
if (!recentFiles.length) {
return [
{
label: i18n._(t`No project opened recently`),
disabled: true,
},
];
}
return recentFiles.map(file => ({
label: file.fileMetadata.fileIdentifier,
click: () => onOpenRecentFile(file),
}));
},
[getRecentProjectFiles, onOpenRecentFile]
);
const prepareExamples = React.useCallback(
(examples: Array<ExampleShortHeader>) =>
examples
.filter(
example =>
example.previewImageUrls.length &&
example.tags.includes('Starter')
)
.slice(0, 16)
.map(example => ({
id: example.id,
title: example.name,
thumbnailUrl:
example.previewImageUrls.find(url =>
url.endsWith('preview.png')
) || example.previewImageUrls[0],
onClick: () => setSelectedExample(example),
})),
[]
);
const prepareShowcasedGames = React.useCallback(
(games: Array<ShowcasedGame>) =>
games.slice(0, 16).map(game => ({
id: game.title,
title: game.title,
thumbnailUrl: game.thumbnailUrl,
onClick: () => setSelectedShowcasedGame(game),
})),
[]
);
const createBlankProject = async (i18n: I18nType) => {
const projectMetadata = await onCreateBlank({
i18n,
outputPath,
});
if (!projectMetadata) return;
const { project, storageProvider, fileMetadata } = projectMetadata;
setPreCreationDialogOpen(false);
setOutputPath(computeDefaultProjectPath());
onOpenProjectAfterCreation({ project, storageProvider, fileMetadata });
};
const createProjectFromExample = async (i18n: I18nType) => {
if (!selectedExample) return;
setIsOpening(true);
const projectMetadata = await onCreateFromExampleShortHeader({
i18n,
outputPath,
exampleShortHeader: selectedExample,
});
if (projectMetadata) {
const { storageProvider, fileMetadata } = projectMetadata;
setPreCreationDialogOpen(false);
setSelectedExample(null);
setOutputPath(computeDefaultProjectPath());
onOpenProjectAfterCreation({ storageProvider, fileMetadata });
}
setIsOpening(false);
};
return (
<I18n>
{({ i18n }) => (
<>
<ScrollBackground>
<div style={styles.container}>
<div style={styles.content}>
<div
style={{
margin: `0px ${windowWidth === 'small' ? 15 : 35}px`,
}}
>
<ResponsiveLineStackLayout justifyContent="space-between">
<UserChip
profile={authenticatedUser.profile}
onClick={onOpenProfile}
/>
<ResponsiveLineStackLayout
justifyContent="flex-end"
noColumnMargin
>
{!project && (
<FlatButton
label={<Trans>Create a blank project</Trans>}
onClick={() => {
electron
? setPreCreationDialogOpen(true)
: createBlankProject(i18n);
}}
primary
/>
)}
{!project && canOpen && (
<RaisedButtonWithSplitMenu
label={<Trans>Open a project</Trans>}
onClick={onOpen}
primary
buildMenuTemplate={
buildRecentProjectFilesMenuTemplate
}
/>
)}
{!!project && (
<>
<RaisedButton
label={<Trans>Open Project Manager</Trans>}
onClick={onOpenProjectManager}
primary
/>
<Spacer />
<FlatButton
label={<Trans>Close project</Trans>}
onClick={() => {
onCloseProject();
}}
/>
</>
)}
</ResponsiveLineStackLayout>
</ResponsiveLineStackLayout>
</div>
<Carousel
title={<Trans>Start from a template</Trans>}
items={examples ? prepareExamples(examples) : null}
displayItemTitles
onBrowseAllClick={onOpenExamples}
error={
exampleLoadingError && (
<>
<Trans>
An error ocurred while loading examples.
</Trans>{' '}
<Trans>
Please check your internet connection or try again
later.
</Trans>
</>
)
}
/>
{renderBetweenCarouselSpace()}
<Carousel
title={<Trans>Our latest tutorials</Trans>}
items={tutorials ? tutorials.slice(0, 16) : null}
displayItemTitles={false}
onBrowseAllClick={onOpenTutorials}
error={
tutorialLoadingError && (
<>
<Trans>
An error ocurred while loading tutorials.
</Trans>{' '}
<Trans>
Please check your internet connection or try again
later.
</Trans>
</>
)
}
/>
{renderBetweenCarouselSpace(betweenCarouselSpacerCount)}
<Carousel
title={<Trans>Games Showcase</Trans>}
items={
showcasedGames
? prepareShowcasedGames(showcasedGames)
: null
}
displayItemTitles
onBrowseAllClick={onOpenGamesShowcase}
error={
showcaseLoadingError && (
<>
<Trans>
An error ocurred while loading showcased games.
</Trans>{' '}
<Trans>
Please check your internet connection or try again
later.
</Trans>
</>
)
}
/>
</div>
<Line noMargin>
<ResponsiveLineStackLayout
alignItems="center"
justifyContent="space-between"
expand
>
<ResponsiveLineStackLayout
noMargin
justifyContent="center"
>
<FlatButton
icon={<ForumIcon />}
label={<Trans>Community Forums</Trans>}
onClick={() =>
Window.openExternalURL(
'https://forum.gdevelop-app.com'
)
}
/>
<FlatButton
icon={<HelpIcon />}
label={<Trans>Help and documentation</Trans>}
onClick={onOpenHelpFinder}
/>
</ResponsiveLineStackLayout>
<Line
noMargin
alignItems="center"
justifyContent="center"
>
<IconButton
className="icon-youtube"
onClick={() =>
Window.openExternalURL(
'https://www.youtube.com/c/GDevelopApp'
)
}
tooltip={t`Tutorials on YouTube`}
/>
<IconButton
className="icon-discord"
onClick={() =>
Window.openExternalURL(
'https://discord.gg/gdevelop'
)
}
tooltip={t`GDevelop on Discord`}
/>
<IconButton
className="icon-reddit"
onClick={() =>
Window.openExternalURL(
'https://www.reddit.com/r/gdevelop'
)
}
tooltip={t`GDevelop on Reddit`}
/>
<IconButton
className="icon-twitter"
onClick={() =>
Window.openExternalURL(
'https://twitter.com/GDevelopApp'
)
}
tooltip={t`GDevelop on Twitter`}
/>
<IconButton
className="icon-facebook"
onClick={() =>
Window.openExternalURL(
'https://www.facebook.com/GDevelopApp'
)
}
tooltip={t`GDevelop on Facebook`}
/>
<FlatButton
label={i18n.language}
onClick={onOpenLanguageDialog}
icon={<Language />}
/>
</Line>
</ResponsiveLineStackLayout>
</Line>
</div>
</ScrollBackground>
{selectedShowcasedGame && (
<ShowcasedGameDialog
open
onClose={() => setSelectedShowcasedGame(null)}
showcasedGame={selectedShowcasedGame}
/>
)}
{selectedExample && (
<ExampleDialog
isOpening={isOpening}
onClose={() => setSelectedExample(null)}
exampleShortHeader={selectedExample}
onOpen={() => {
electron
? setPreCreationDialogOpen(true)
: createProjectFromExample(i18n);
}}
/>
)}
{preCreationDialogOpen && (
<LocalProjectPreCreationDialog
open
onClose={() => setPreCreationDialogOpen(false)}
onCreate={() =>
selectedExample
? createProjectFromExample(i18n)
: createBlankProject(i18n)
}
outputPath={outputPath}
onChangeOutputPath={setOutputPath}
/>
)}
</>
)}
</I18n>
);
}
),
// Prevent any update to the editor if the editor is not active,
// and so not visible to the user.
(prevProps, nextProps) => prevProps.isActive || nextProps.isActive
);
export const renderHomePageContainer = (
props: RenderEditorContainerPropsWithRef
) => (
<HomePage
ref={props.ref}
project={props.project}
isActive={props.isActive}
projectItemName={props.projectItemName}
setToolbar={props.setToolbar}
canOpen={props.canOpen}
onOpen={props.onOpen}
onOpenRecentFile={props.onOpenRecentFile}
onOpenExamples={props.onOpenExamples}
onCreateFromExampleShortHeader={props.onCreateFromExampleShortHeader}
onCreateBlank={props.onCreateBlank}
onOpenProjectAfterCreation={props.onOpenProjectAfterCreation}
onOpenProjectManager={props.onOpenProjectManager}
onCloseProject={props.onCloseProject}
onOpenTutorials={props.onOpenTutorials}
onOpenGamesShowcase={props.onOpenGamesShowcase}
onOpenHelpFinder={props.onOpenHelpFinder}
onOpenLanguageDialog={props.onOpenLanguageDialog}
onOpenProfile={props.onOpenProfile}
/>
);

View File

@@ -1,32 +0,0 @@
// @flow
import * as React from 'react';
import ThemeConsumer from '../../../UI/Theme/ThemeConsumer';
const styles = {
scrollContainer: {
flex: 1,
display: 'flex',
overflowY: 'scroll',
},
};
type Props = {|
children: React.Node,
|};
const ScrollBackground = ({ children }: Props) => (
<ThemeConsumer>
{muiTheme => (
<div
style={{
...styles.scrollContainer,
backgroundColor: muiTheme.palette.canvasColor,
}}
>
{children}
</div>
)}
</ThemeConsumer>
);
export default ScrollBackground;

View File

@@ -1,270 +0,0 @@
// @flow
import { Trans, t } from '@lingui/macro';
import { I18n } from '@lingui/react';
import * as React from 'react';
import FlatButton from '../../../UI/FlatButton';
import Paper from '@material-ui/core/Paper';
import IconButton from '../../../UI/IconButton';
import Language from '@material-ui/icons/Language';
import { type RenderEditorContainerPropsWithRef } from '../BaseEditor';
import Window from '../../../Utils/Window';
import { Line } from '../../../UI/Grid';
import GDevelopLogo from './GDevelopLogo';
import ScrollBackground from './ScrollBackground';
import RaisedButton from '../../../UI/RaisedButton';
import Text from '../../../UI/Text';
import {
ColumnStackLayout,
ResponsiveLineStackLayout,
} from '../../../UI/Layout';
import ForumIcon from '@material-ui/icons/Forum';
import HelpIcon from '@material-ui/icons/Help';
import SportsEsportsIcon from '@material-ui/icons/SportsEsports';
const styles = {
innerContainer: {
display: 'flex',
flexDirection: 'column',
width: '100%',
minHeight: 400,
},
centerContainer: {
display: 'flex',
flexDirection: 'column',
alignItems: 'stretch',
justifyContent: 'center',
flexShrink: 0,
maxWidth: 400,
},
logoPaper: {
marginBottom: 10,
width: '100%',
textAlign: 'center',
},
};
type Props = {|
project: ?gdProject,
isActive: boolean,
projectItemName: ?string,
project: ?gdProject,
setToolbar: (?React.Node) => void,
// Project opening
canOpen: boolean,
onOpen: () => void,
onCreate: () => void,
onOpenProjectManager: () => void,
onCloseProject: () => Promise<void>,
// Other dialogs opening:
onOpenTutorials: () => void,
onOpenGamesShowcase: () => void,
onOpenHelpFinder: () => void,
onOpenLanguageDialog: () => void,
|};
export class StartPage extends React.Component<Props, {||}> {
shouldComponentUpdate(nextProps: Props) {
// Prevent any update to the editor if the editor is not active,
// and so not visible to the user.
return nextProps.isActive;
}
getProject() {
return undefined;
}
updateToolbar() {
if (this.props.setToolbar) this.props.setToolbar(null);
}
forceUpdateEditor() {
// No updates to be done
}
render() {
const {
project,
canOpen,
onOpen,
onCreate,
onOpenProjectManager,
onCloseProject,
onOpenTutorials,
onOpenGamesShowcase,
onOpenHelpFinder,
onOpenLanguageDialog,
} = this.props;
return (
<I18n>
{({ i18n }) => (
<ScrollBackground>
<div style={styles.innerContainer}>
<Line expand justifyContent="center">
<div style={styles.centerContainer}>
<Paper
elevation={2}
style={{
...styles.logoPaper,
}}
>
<GDevelopLogo />
<Text>
<Trans>
GDevelop is an easy-to-use game creator with no
programming language to learn.
</Trans>
</Text>
</Paper>
<ColumnStackLayout noMargin>
{
<RaisedButton
label={<Trans>Getting Started and Tutorials</Trans>}
fullWidth
onClick={onOpenTutorials}
/>
}
{!project && (
<RaisedButton
label={<Trans>Create a new project</Trans>}
fullWidth
onClick={onCreate}
primary
/>
)}
{!project && canOpen && (
<RaisedButton
label={<Trans>Open a project</Trans>}
fullWidth
onClick={onOpen}
primary
/>
)}
{!!project && (
<RaisedButton
label={<Trans>Open Project Manager</Trans>}
fullWidth
onClick={onOpenProjectManager}
primary
/>
)}
{!!project && (
<FlatButton
label={<Trans>Close project</Trans>}
fullWidth
onClick={() => {
onCloseProject();
}}
/>
)}
</ColumnStackLayout>
</div>
</Line>
<Line noMargin>
<ResponsiveLineStackLayout
alignItems="center"
justifyContent="space-between"
expand
>
<Line noMargin justifyContent="center">
<FlatButton
icon={<SportsEsportsIcon />}
label={<Trans>GDevelop Games</Trans>}
onClick={onOpenGamesShowcase}
/>
<FlatButton
icon={<ForumIcon />}
label={<Trans>Community Forums</Trans>}
onClick={() =>
Window.openExternalURL('https://forum.gdevelop-app.com')
}
/>
<FlatButton
icon={<HelpIcon />}
label={<Trans>Help and documentation</Trans>}
onClick={onOpenHelpFinder}
/>
</Line>
<Line noMargin alignItems="center" justifyContent="center">
<IconButton
className="icon-youtube"
onClick={() =>
Window.openExternalURL(
'https://www.youtube.com/c/GDevelopApp'
)
}
tooltip={t`Tutorials on YouTube`}
/>
<IconButton
className="icon-discord"
onClick={() =>
Window.openExternalURL('https://discord.gg/gdevelop')
}
tooltip={t`GDevelop on Discord`}
/>
<IconButton
className="icon-reddit"
onClick={() =>
Window.openExternalURL(
'https://www.reddit.com/r/gdevelop'
)
}
tooltip={t`GDevelop on Reddit`}
/>
<IconButton
className="icon-twitter"
onClick={() =>
Window.openExternalURL(
'https://twitter.com/GDevelopApp'
)
}
tooltip={t`GDevelop on Twitter`}
/>
<IconButton
className="icon-facebook"
onClick={() =>
Window.openExternalURL(
'https://www.facebook.com/GDevelopApp'
)
}
tooltip={t`GDevelop on Facebook`}
/>
<FlatButton
label={i18n.language}
onClick={onOpenLanguageDialog}
icon={<Language />}
/>
</Line>
</ResponsiveLineStackLayout>
</Line>
</div>
</ScrollBackground>
)}
</I18n>
);
}
}
export const renderStartPageContainer = (
props: RenderEditorContainerPropsWithRef
) => (
<StartPage
ref={props.ref}
project={props.project}
isActive={props.isActive}
projectItemName={props.projectItemName}
setToolbar={props.setToolbar}
canOpen={props.canOpen}
onOpen={props.onOpen}
onCreate={props.onCreate}
onOpenProjectManager={props.onOpenProjectManager}
onCloseProject={props.onCloseProject}
onOpenTutorials={props.onOpenTutorials}
onOpenGamesShowcase={props.onOpenGamesShowcase}
onOpenHelpFinder={props.onOpenHelpFinder}
onOpenLanguageDialog={props.onOpenLanguageDialog}
/>
);

View File

@@ -20,7 +20,7 @@ type MainMenuEvent =
| 'main-menu-export'
| 'main-menu-create'
| 'main-menu-open-project-manager'
| 'main-menu-open-start-page'
| 'main-menu-open-home-page'
| 'main-menu-open-debugger'
| 'main-menu-open-about'
| 'main-menu-open-preferences'
@@ -193,8 +193,8 @@ const buildAndSendMenuTemplate = (
enabled: !!project,
},
{
label: i18n._(t`Show Start Page`),
onClickSendEvent: 'main-menu-open-start-page',
label: i18n._(t`Show Home`),
onClickSendEvent: 'main-menu-open-home-page',
},
{
label: i18n._(t`Open Debugger`),
@@ -347,7 +347,7 @@ const ElectronMainMenu = (props: MainMenuProps) => {
'main-menu-open-project-manager',
props.onOpenProjectManager
);
useIPCEventListener('main-menu-open-start-page', props.onOpenStartPage);
useIPCEventListener('main-menu-open-home-page', props.onOpenHomePage);
useIPCEventListener('main-menu-open-debugger', props.onOpenDebugger);
useIPCEventListener('main-menu-open-about', props.onOpenAbout);
useIPCEventListener('main-menu-open-preferences', props.onOpenPreferences);

View File

@@ -45,7 +45,7 @@ type CommandHandlers = {|
onLaunchNetworkPreview: () => void,
onHotReloadPreview: () => void,
allowNetworkPreview: boolean,
onOpenStartPage: () => void,
onOpenHomePage: () => void,
onCreateProject: () => void,
onOpenProject: () => void,
onSaveProject: () => Promise<void>,
@@ -103,8 +103,8 @@ const useMainFrameCommands = (handlers: CommandHandlers) => {
}
);
useCommand('OPEN_START_PAGE', true, {
handler: handlers.onOpenStartPage,
useCommand('OPEN_HOME_PAGE', true, {
handler: handlers.onOpenHomePage,
});
useCommand('CREATE_NEW_PROJECT', true, {

View File

@@ -17,7 +17,7 @@ export type MainMenuProps = {|
onExportProject: (open?: boolean) => void,
onCreateProject: (open?: boolean) => void,
onOpenProjectManager: (open?: boolean) => void,
onOpenStartPage: () => void,
onOpenHomePage: () => void,
onOpenDebugger: () => void,
onOpenAbout: (open?: boolean) => void,
onOpenPreferences: (open?: boolean) => void,

View File

@@ -9,7 +9,6 @@ import Dialog from '../../UI/Dialog';
import { Column, Line } from '../../UI/Grid';
import Window from '../../Utils/Window';
import PreferencesContext from './PreferencesContext';
import AlertMessage from '../../UI/AlertMessage';
import LocalesMetadata from '../../locales/LocalesMetadata';
import { I18n } from '@lingui/react';
@@ -112,15 +111,6 @@ export default class LanguageDialog extends Component<Props, State> {
title={<Trans>Language</Trans>}
>
<Column noMargin>
<Line>
<AlertMessage kind="info">
<Trans>
Support for translations is still new and in beta.
GDevelop needs your help to be translated in your
language!
</Trans>
</AlertMessage>
</Line>
<Line expand>
<SelectField
floatingLabelText={

View File

@@ -202,7 +202,9 @@ export type Preferences = {|
checkUpdates: (forceDownload?: boolean) => void,
setAutoDisplayChangelog: (enabled: boolean) => void,
showAlertMessage: (identifier: AlertMessageIdentifier, show: boolean) => void,
showAllAlertMessages: () => void,
showTutorialHint: (identifier: string, show: boolean) => void,
showAllTutorialHints: () => void,
verifyIfIsNewVersion: () => boolean,
setEventsSheetShowObjectThumbnails: (enabled: boolean) => void,
setAutosaveOnPreview: (enabled: boolean) => void,
@@ -287,7 +289,9 @@ export const initialPreferences = {
checkUpdates: () => {},
setAutoDisplayChangelog: () => {},
showAlertMessage: (identifier: AlertMessageIdentifier, show: boolean) => {},
showAllAlertMessages: () => {},
showTutorialHint: (identifier: string, show: boolean) => {},
showAllTutorialHints: () => {},
verifyIfIsNewVersion: () => false,
setEventsSheetShowObjectThumbnails: () => {},
setAutosaveOnPreview: () => {},

View File

@@ -13,11 +13,10 @@ import { themes } from '../../UI/Theme';
import { getAllThemes } from '../../CodeEditor/Theme';
import Window from '../../Utils/Window';
import optionalRequire from '../../Utils/OptionalRequire';
import PreferencesContext, { allAlertMessages } from './PreferencesContext';
import PreferencesContext from './PreferencesContext';
import Text from '../../UI/Text';
import { ResponsiveLineStackLayout } from '../../UI/Layout';
import { Tabs, Tab } from '../../UI/Tabs';
import { getAllTutorialHints } from '../../Hints';
import RaisedButton from '../../UI/RaisedButton';
import ShortcutsList from '../../KeyboardShortcuts/ShortcutsList';
const electron = optionalRequire('electron');
@@ -34,8 +33,8 @@ const PreferencesDialog = ({ i18n, onClose }: Props) => {
setThemeName,
setCodeEditorThemeName,
setAutoDownloadUpdates,
showAlertMessage,
showTutorialHint,
showAllAlertMessages,
showAllTutorialHints,
setAutoDisplayChangelog,
setEventsSheetShowObjectThumbnails,
setAutosaveOnPreview,
@@ -67,15 +66,16 @@ const PreferencesDialog = ({ i18n, onClose }: Props) => {
onRequestClose={onClose}
cannotBeDismissed={true}
open
title={<Trans>GDevelop Preferences</Trans>}
noTitleMargin
maxWidth="sm"
noMargin
title={
<Tabs value={currentTab} onChange={setCurrentTab}>
<Tab label={<Trans>Preferences</Trans>} value="preferences" />
<Tab label={<Trans>Keyboard Shortcuts</Trans>} value="shortcuts" />
</Tabs>
}
>
<Tabs value={currentTab} onChange={setCurrentTab}>
<Tab label={<Trans>Preferences</Trans>} value="preferences" />
<Tab label={<Trans>Hints &amp; explanations</Trans>} value="hints" />
<Tab label={<Trans>Keyboard Shortcuts</Trans>} value="shortcuts" />
</Tabs>
{currentTab === 'preferences' && (
<Column>
<Text size="title">
@@ -279,6 +279,27 @@ const PreferencesDialog = ({ i18n, onClose }: Props) => {
<SelectOption value="apply" primaryText={t`Apply changes`} />
</SelectField>
</Line>
<Text size="title">
<Trans>Embedded help and tutorials</Trans>
</Text>
<Line>
<Column noMargin>
<Line>
<RaisedButton
label={<Trans>Reset hidden embedded explanations</Trans>}
onClick={() => showAllAlertMessages()}
disabled={!Object.keys(values.hiddenAlertMessages).length}
/>
</Line>
<Line>
<RaisedButton
label={<Trans>Reset hidden embedded tutorials</Trans>}
onClick={() => showAllTutorialHints()}
disabled={!Object.keys(values.hiddenTutorialHints).length}
/>
</Line>
</Column>
</Line>
<Text size="title">
<Trans>Advanced</Trans>
</Text>
@@ -343,40 +364,6 @@ const PreferencesDialog = ({ i18n, onClose }: Props) => {
)}
</Column>
)}
{currentTab === 'hints' && (
<Column>
<Line>
<Column noMargin>
<Text>
<Trans>Warn/show explanation about:</Trans>
</Text>
{allAlertMessages.map(({ key, label }) => (
<Line key={key}>
<Toggle
onToggle={(e, check) => showAlertMessage(key, check)}
toggled={!values.hiddenAlertMessages[key]}
labelPosition="right"
label={label}
/>
</Line>
))}
<Text>
<Trans>Show link to tutorials:</Trans>
</Text>
{getAllTutorialHints().map(({ identifier, name }) => (
<Line key={identifier}>
<Toggle
onToggle={(e, check) => showTutorialHint(identifier, check)}
toggled={!values.hiddenTutorialHints[identifier]}
labelPosition="right"
label={name}
/>
</Line>
))}
</Column>
</Line>
</Column>
)}
{currentTab === 'shortcuts' && (
<Column>
<ShortcutsList

View File

@@ -40,7 +40,9 @@ export default class PreferencesProvider extends React.Component<Props, State> {
checkUpdates: this._checkUpdates.bind(this),
setAutoDisplayChangelog: this._setAutoDisplayChangelog.bind(this),
showAlertMessage: this._showAlertMessage.bind(this),
showAllAlertMessages: this._showAllAlertMessages.bind(this),
showTutorialHint: this._showTutorialHint.bind(this),
showAllTutorialHints: this._showAllTutorialHints.bind(this),
verifyIfIsNewVersion: this._verifyIfIsNewVersion.bind(this),
setEventsSheetShowObjectThumbnails: this._setEventsSheetShowObjectThumbnails.bind(
this
@@ -323,6 +325,18 @@ export default class PreferencesProvider extends React.Component<Props, State> {
);
}
_showAllAlertMessages() {
this.setState(
state => ({
values: {
...state.values,
hiddenAlertMessages: {},
},
}),
() => this._persistValuesToLocalStorage(this.state)
);
}
_showTutorialHint(identifier: string, show: boolean) {
this.setState(
state => ({
@@ -338,6 +352,18 @@ export default class PreferencesProvider extends React.Component<Props, State> {
);
}
_showAllTutorialHints() {
this.setState(
state => ({
values: {
...state.values,
hiddenTutorialHints: {},
},
}),
() => this._persistValuesToLocalStorage(this.state)
);
}
_loadValuesFromLocalStorage(): ?PreferencesValues {
try {
const persistedState = localStorage.getItem(LocalStorageItem);

View File

@@ -35,6 +35,7 @@ import {
ResourceFetcherContext,
} from '../ProjectsStorage/ResourceFetcher';
import { GamesShowcaseStateProvider } from '../GamesShowcase/GamesShowcaseContext';
import { TutorialStateProvider } from '../Tutorial/TutorialContext';
// Add the rtl plugin to the JSS instance to support RTL languages in material-ui components.
const jss = create({
@@ -107,11 +108,13 @@ export default class Providers extends React.Component<Props, {||}> {
<ExampleStoreStateProvider>
<ExtensionStoreStateProvider>
<GamesShowcaseStateProvider>
<ResourceFetcherContext.Provider
value={resourceFetcher}
>
{children({ i18n })}
</ResourceFetcherContext.Provider>
<TutorialStateProvider>
<ResourceFetcherContext.Provider
value={resourceFetcher}
>
{children({ i18n })}
</ResourceFetcherContext.Provider>
</TutorialStateProvider>
</GamesShowcaseStateProvider>
</ExtensionStoreStateProvider>
</ExampleStoreStateProvider>

View File

@@ -0,0 +1,211 @@
// @flow
import { type I18n as I18nType } from '@lingui/core';
import * as React from 'react';
import { t, Trans } from '@lingui/macro';
import ToolbarIcon from '../../UI/ToolbarIcon';
import FlatButton from '../../UI/FlatButton';
import ElementWithMenu from '../../UI/Menu/ElementWithMenu';
import { type PreviewState } from '../PreviewState';
import GDevelopThemeContext from '../../UI/Theme/ThemeContext';
export type PreviewAndPublishButtonsProps = {|
onPreviewWithoutHotReload: () => void,
onOpenDebugger: () => void,
onNetworkPreview: () => void,
onHotReloadPreview: () => void,
setPreviewOverride: ({|
isPreviewOverriden: boolean,
overridenPreviewLayoutName: ?string,
overridenPreviewExternalLayoutName: ?string,
|}) => void,
canDoNetworkPreview: boolean,
isPreviewEnabled: boolean,
hasPreviewsRunning: boolean,
previewState: PreviewState,
exportProject: () => void,
hasProject: boolean,
|};
export default function PreviewAndPublishButtons({
onPreviewWithoutHotReload,
onNetworkPreview,
onOpenDebugger,
onHotReloadPreview,
canDoNetworkPreview,
isPreviewEnabled,
hasPreviewsRunning,
previewState,
setPreviewOverride,
exportProject,
hasProject,
}: PreviewAndPublishButtonsProps) {
const theme = React.useContext(GDevelopThemeContext);
const debugBuildMenuTemplate = React.useCallback(
(i18n: I18nType) => [
{
label: i18n._(t`Start Network Preview (Preview over WiFi/LAN)`),
click: onNetworkPreview,
enabled: canDoNetworkPreview,
},
{ type: 'separator' },
{
label: i18n._(t`Start Preview with Debugger and Performance Profiler`),
click: onOpenDebugger,
},
],
[onNetworkPreview, onOpenDebugger, canDoNetworkPreview]
);
const previewBuildMenuTemplate = React.useCallback(
(i18n: I18nType) => [
{
label: i18n._(t`Launch another preview in a new window`),
click: onPreviewWithoutHotReload,
enabled: isPreviewEnabled && hasPreviewsRunning,
},
{ type: 'separator' },
...(previewState.overridenPreviewLayoutName
? [
{
type: 'checkbox',
label: previewState.overridenPreviewExternalLayoutName
? i18n._(
t`Start all previews from external layout ${
previewState.overridenPreviewExternalLayoutName
}`
)
: i18n._(
t`Start all previews from scene ${
previewState.overridenPreviewLayoutName
}`
),
checked: previewState.isPreviewOverriden,
click: () =>
setPreviewOverride({
isPreviewOverriden: !previewState.isPreviewOverriden,
overridenPreviewLayoutName:
previewState.overridenPreviewLayoutName,
overridenPreviewExternalLayoutName:
previewState.overridenPreviewExternalLayoutName,
}),
},
{ type: 'separator' },
]
: []),
{
label: previewState.previewExternalLayoutName
? i18n._(
t`Use this external layout inside this scene to start all previews`
)
: i18n._(t`Use this scene to start all previews`),
click: () =>
setPreviewOverride({
isPreviewOverriden: true,
overridenPreviewLayoutName: previewState.previewLayoutName,
overridenPreviewExternalLayoutName:
previewState.previewExternalLayoutName,
}),
enabled:
previewState.previewLayoutName !==
previewState.overridenPreviewLayoutName ||
previewState.previewExternalLayoutName !==
previewState.overridenPreviewExternalLayoutName,
},
],
[
onPreviewWithoutHotReload,
isPreviewEnabled,
hasPreviewsRunning,
setPreviewOverride,
previewState,
]
);
return (
<React.Fragment>
<ElementWithMenu
element={
<ToolbarIcon
disabled={!isPreviewEnabled}
src="res/ribbon_default/bug32.png"
tooltip={t`Advanced preview options (debugger, network preview...)`}
/>
}
buildMenuTemplate={debugBuildMenuTemplate}
/>
<ElementWithMenu
element={
<FlatButton
onClick={onHotReloadPreview}
disabled={!isPreviewEnabled}
icon={
<img
alt="Preview"
src={
hasPreviewsRunning
? 'res/ribbon_default/hotReload64.png'
: previewState.isPreviewOverriden
? 'res/ribbon_default/previewOverride32.png'
: 'res/ribbon_default/preview64.png'
}
width={32}
height={32}
style={{
filter: !isPreviewEnabled
? 'grayscale(100%)'
: theme.gdevelopIconsCSSFilter,
}}
/>
}
label={
hasPreviewsRunning ? (
<Trans>Update</Trans>
) : (
<Trans>Preview</Trans>
)
}
exceptionalTooltipForToolbar={
hasPreviewsRunning ? (
<Trans>
Apply changes to the running preview, right click for more
</Trans>
) : previewState.isPreviewOverriden ? (
<Trans>Preview is overridden, right click for more</Trans>
) : previewState.previewExternalLayoutName ? (
<Trans>
Launch a preview of the external layout inside the scene,
right click for more
</Trans>
) : (
<Trans>
Launch a preview of the scene, right click for more
</Trans>
)
}
/>
}
openMenuWithSecondaryClick
buildMenuTemplate={previewBuildMenuTemplate}
/>
<FlatButton
onClick={exportProject}
disabled={!hasProject}
icon={
<img
alt="Publish"
src={'res/ribbon_default/networkicon32.png'}
width={32}
height={32}
style={{
filter: !hasProject
? 'grayscale(100%)'
: theme.gdevelopIconsCSSFilter,
}}
/>
}
label={<Trans>Publish</Trans>}
exceptionalTooltipForToolbar={
<Trans>Export the game (Web, Android, iOS...)</Trans>
}
/>
</React.Fragment>
);
}

View File

@@ -1,145 +0,0 @@
// @flow
import { type I18n as I18nType } from '@lingui/core';
import * as React from 'react';
import { t } from '@lingui/macro';
import ToolbarIcon from '../../UI/ToolbarIcon';
import ElementWithMenu from '../../UI/Menu/ElementWithMenu';
import { type PreviewState } from '../PreviewState';
export type PreviewButtonsProps = {|
onPreviewWithoutHotReload: () => void,
onOpenDebugger: () => void,
onNetworkPreview: () => void,
onHotReloadPreview: () => void,
setPreviewOverride: ({|
isPreviewOverriden: boolean,
overridenPreviewLayoutName: ?string,
overridenPreviewExternalLayoutName: ?string,
|}) => void,
canDoNetworkPreview: boolean,
isPreviewEnabled: boolean,
hasPreviewsRunning: boolean,
previewState: PreviewState,
|};
export default function PreviewButtons({
onPreviewWithoutHotReload,
onNetworkPreview,
onOpenDebugger,
onHotReloadPreview,
canDoNetworkPreview,
isPreviewEnabled,
hasPreviewsRunning,
previewState,
setPreviewOverride,
}: PreviewButtonsProps) {
const previewIcon = (
<ToolbarIcon
onClick={onHotReloadPreview}
disabled={!isPreviewEnabled}
src={
hasPreviewsRunning
? 'res/ribbon_default/hotReload64.png'
: previewState.isPreviewOverriden
? 'res/ribbon_default/previewOverride32.png'
: 'res/ribbon_default/preview64.png'
}
tooltip={
hasPreviewsRunning
? t`Apply changes to the running preview, right click for more`
: previewState.isPreviewOverriden
? t`Preview is overridden, right click for more`
: previewState.previewExternalLayoutName
? t`Launch a preview of the external layout inside the scene, right click for more`
: t`Launch a preview of the scene, right click for more`
}
/>
);
return (
<React.Fragment>
<ElementWithMenu
element={previewIcon}
openMenuWithSecondaryClick
buildMenuTemplate={(i18n: I18nType) => [
{
label: i18n._(t`Launch another preview in a new window`),
click: onPreviewWithoutHotReload,
enabled: isPreviewEnabled && hasPreviewsRunning,
},
{ type: 'separator' },
...(previewState.overridenPreviewLayoutName
? [
{
type: 'checkbox',
label: previewState.overridenPreviewExternalLayoutName
? i18n._(
t`Start all previews from external layout ${
previewState.overridenPreviewExternalLayoutName
}`
)
: i18n._(
t`Start all previews from scene ${
previewState.overridenPreviewLayoutName
}`
),
checked: previewState.isPreviewOverriden,
click: () =>
setPreviewOverride({
isPreviewOverriden: !previewState.isPreviewOverriden,
overridenPreviewLayoutName:
previewState.overridenPreviewLayoutName,
overridenPreviewExternalLayoutName:
previewState.overridenPreviewExternalLayoutName,
}),
},
{ type: 'separator' },
]
: []),
{
label: previewState.previewExternalLayoutName
? i18n._(
t`Use this external layout inside this scene to start all previews`
)
: i18n._(t`Use this scene to start all previews`),
click: () =>
setPreviewOverride({
isPreviewOverriden: true,
overridenPreviewLayoutName: previewState.previewLayoutName,
overridenPreviewExternalLayoutName:
previewState.previewExternalLayoutName,
}),
enabled:
previewState.previewLayoutName !==
previewState.overridenPreviewLayoutName ||
previewState.previewExternalLayoutName !==
previewState.overridenPreviewExternalLayoutName,
},
]}
/>
<ElementWithMenu
element={
<ToolbarIcon
disabled={!isPreviewEnabled}
src="res/ribbon_default/bug32.png"
tooltip={t`Advanced preview options (debugger, network preview...)`}
/>
}
buildMenuTemplate={(i18n: I18nType) => [
{
label: i18n._(t`Start Network Preview (Preview over WiFi/LAN)`),
click: () => onNetworkPreview(),
enabled: canDoNetworkPreview,
},
{ type: 'separator' },
{
label: i18n._(
t`Start Preview with Debugger and Performance Profiler`
),
click: () => onOpenDebugger(),
},
]}
/>
</React.Fragment>
);
}

View File

@@ -1,14 +1,16 @@
// @flow
import { t } from '@lingui/macro';
import React, { PureComponent } from 'react';
import * as React from 'react';
import { Toolbar, ToolbarGroup } from '../../UI/Toolbar';
import ToolbarIcon from '../../UI/ToolbarIcon';
import ToolbarSeparator from '../../UI/ToolbarSeparator';
import ElementWithMenu from '../../UI/Menu/ElementWithMenu';
import Window from '../../Utils/Window';
import PreviewButtons, { type PreviewButtonsProps } from './PreviewButtons';
import PreviewAndPublishButtons, {
type PreviewAndPublishButtonsProps,
} from './PreviewAndPublishButtons';
type Props = {|
export type MainFrameToolbarProps = {|
showProjectIcons: boolean,
hasProject: boolean,
toggleProjectManager: () => void,
@@ -16,93 +18,75 @@ type Props = {|
simulateUpdateDownloaded: ?() => void,
simulateUpdateAvailable: ?() => void,
exportProject: () => void,
...PreviewButtonsProps,
...PreviewAndPublishButtonsProps,
|};
type State = {|
editorToolbar: any,
export type ToolbarInterface = {|
setEditorToolbar: any => void,
|};
export class MainFrameToolbar extends PureComponent<Props, State> {
state = {
editorToolbar: null,
};
export default React.forwardRef<MainFrameToolbarProps, ToolbarInterface>(
function MainframeToolbar(props: MainFrameToolbarProps, ref) {
const isDev = Window.isDev();
isDev = Window.isDev();
const [editorToolbar, setEditorToolbar] = React.useState<?any>(null);
React.useImperativeHandle(ref, () => ({
setEditorToolbar,
}));
setEditorToolbar(editorToolbar: any) {
this.setState({
editorToolbar,
});
}
render() {
return (
<Toolbar>
<ToolbarGroup firstChild>
{this.props.showProjectIcons && (
{props.showProjectIcons && (
<ToolbarIcon
onClick={this.props.toggleProjectManager}
onClick={props.toggleProjectManager}
src="res/ribbon_default/projectManager32.png"
disabled={!this.props.hasProject}
disabled={!props.hasProject}
tooltip={t`Project manager`}
/>
)}
{this.props.showProjectIcons && (
<ToolbarIcon
onClick={this.props.exportProject}
src="res/ribbon_default/export32.png"
disabled={!this.props.hasProject}
tooltip={t`Export the game (Web, Android, iOS...)`}
/>
)}
{this.isDev && (
{isDev && props.showProjectIcons && <ToolbarSeparator />}
{isDev && (
<ElementWithMenu
element={<ToolbarIcon src="res/ribbon_default/bug32.png" />}
buildMenuTemplate={() => [
{
label: 'Request update from external editor',
disabled: !this.props.requestUpdate,
click: () => {
this.props.requestUpdate && this.props.requestUpdate();
},
},
{
label: 'Simulate update downloaded',
disabled: !this.props.simulateUpdateDownloaded,
disabled: !props.simulateUpdateDownloaded,
click: () => {
this.props.simulateUpdateDownloaded &&
this.props.simulateUpdateDownloaded();
props.simulateUpdateDownloaded &&
props.simulateUpdateDownloaded();
},
},
{
label: 'Simulate update available',
disabled: !this.props.simulateUpdateAvailable,
disabled: !props.simulateUpdateAvailable,
click: () => {
this.props.simulateUpdateAvailable &&
this.props.simulateUpdateAvailable();
props.simulateUpdateAvailable &&
props.simulateUpdateAvailable();
},
},
]}
/>
)}
<ToolbarSeparator />
<PreviewButtons
onPreviewWithoutHotReload={this.props.onPreviewWithoutHotReload}
onOpenDebugger={this.props.onOpenDebugger}
onNetworkPreview={this.props.onNetworkPreview}
onHotReloadPreview={this.props.onHotReloadPreview}
setPreviewOverride={this.props.setPreviewOverride}
canDoNetworkPreview={this.props.canDoNetworkPreview}
isPreviewEnabled={this.props.isPreviewEnabled}
previewState={this.props.previewState}
hasPreviewsRunning={this.props.hasPreviewsRunning}
</ToolbarGroup>
<ToolbarGroup>
<PreviewAndPublishButtons
onPreviewWithoutHotReload={props.onPreviewWithoutHotReload}
onOpenDebugger={props.onOpenDebugger}
onNetworkPreview={props.onNetworkPreview}
onHotReloadPreview={props.onHotReloadPreview}
setPreviewOverride={props.setPreviewOverride}
canDoNetworkPreview={props.canDoNetworkPreview}
isPreviewEnabled={props.isPreviewEnabled}
previewState={props.previewState}
hasPreviewsRunning={props.hasPreviewsRunning}
exportProject={props.exportProject}
hasProject={props.hasProject}
/>
</ToolbarGroup>
{this.state.editorToolbar || <ToolbarGroup />}
{editorToolbar || <ToolbarGroup />}
</Toolbar>
);
}
}
export default MainFrameToolbar;
);

View File

@@ -5,7 +5,7 @@ import * as React from 'react';
import './MainFrame.css';
import Drawer from '@material-ui/core/Drawer';
import Snackbar from '@material-ui/core/Snackbar';
import Toolbar from './Toolbar';
import Toolbar, { type ToolbarInterface } from './Toolbar';
import ProjectTitlebar from './ProjectTitlebar';
import PreferencesDialog from './Preferences/PreferencesDialog';
import AboutDialog from './AboutDialog';
@@ -52,7 +52,7 @@ import { renderExternalEventsEditorContainer } from './EditorContainers/External
import { renderSceneEditorContainer } from './EditorContainers/SceneEditorContainer';
import { renderExternalLayoutEditorContainer } from './EditorContainers/ExternalLayoutEditorContainer';
import { renderEventsFunctionsExtensionEditorContainer } from './EditorContainers/EventsFunctionsExtensionEditorContainer';
import { renderStartPageContainer } from './EditorContainers/StartPage';
import { renderHomePageContainer } from './EditorContainers/HomePage';
import { renderResourcesEditorContainer } from './EditorContainers/ResourcesEditorContainer';
import ErrorBoundary from '../UI/ErrorBoundary';
import SubscriptionDialog from '../Profile/SubscriptionDialog';
@@ -85,7 +85,14 @@ import LanguageDialog from './Preferences/LanguageDialog';
import PreferencesContext from './Preferences/PreferencesContext';
import { getFunctionNameFromType } from '../EventsFunctionsExtensionsLoader';
import { type ExportDialogWithoutExportsProps } from '../Export/ExportDialog';
import { type CreateProjectDialogWithComponentsProps } from '../ProjectCreation/CreateProjectDialog';
import {
type CreateProjectDialogWithComponentsProps,
type CreateProjectDialogTabs,
} from '../ProjectCreation/CreateProjectDialog';
import {
type OnCreateFromExampleShortHeaderFunction,
type OnCreateBlankFunction,
} from '../ProjectCreation/CreateProjectDialog';
import { getStartupTimesSummary } from '../Utils/StartupTimes';
import {
type StorageProvider,
@@ -217,6 +224,8 @@ export type Props = {
requestUpdate?: () => void,
renderExportDialog?: ExportDialogWithoutExportsProps => React.Node,
renderCreateDialog?: CreateProjectDialogWithComponentsProps => React.Node,
onCreateFromExampleShortHeader: OnCreateFromExampleShortHeaderFunction,
onCreateBlank: OnCreateBlankFunction,
renderGDJSDevelopmentWatcher?: ?() => React.Node,
extensionsLoader?: JsExtensionsLoader,
initialFileMetadataToOpen: ?FileMetadata,
@@ -242,7 +251,7 @@ const MainFrame = (props: Props) => {
gdjsDevelopmentWatcherEnabled: false,
}: State)
);
const toolbar = React.useRef<?Toolbar>(null);
const toolbar = React.useRef<?ToolbarInterface>(null);
const authenticatedUser = React.useContext(AuthenticatedUserContext);
const [
@@ -314,9 +323,10 @@ const MainFrame = (props: Props) => {
EventsFunctionsExtensionsContext
);
const unsavedChanges = React.useContext(UnsavedChangesContext);
const [createDialogInitialTab, setCreateDialogInitialTab] = React.useState<
'starters' | 'tutorials' | 'games-showcase'
>('starters');
const [
createDialogInitialTab,
setCreateDialogInitialTab,
] = React.useState<CreateProjectDialogTabs>('examples');
// This is just for testing, to check if we're getting the right state
// and gives us an idea about the number of re-renders.
@@ -333,6 +343,8 @@ const MainFrame = (props: Props) => {
const {
renderExportDialog,
renderCreateDialog,
onCreateFromExampleShortHeader,
onCreateBlank,
resourceSources,
renderPreviewLauncher,
resourceExternalEditors,
@@ -348,7 +360,7 @@ const MainFrame = (props: Props) => {
React.useEffect(
() => {
if (!integratedEditor) openStartPage();
if (!integratedEditor) openHomePage();
GD_STARTUP_TIMES.push(['MainFrameComponentDidMount', performance.now()]);
_loadExtensions()
.then(() =>
@@ -772,8 +784,8 @@ const MainFrame = (props: Props) => {
[openProjectManager]
);
const setEditorToolbar = (editorToolbar: any) => {
if (!toolbar.current) return;
const setEditorToolbar = (editorToolbar: any, isCurrentTab = true) => {
if (!toolbar.current || !isCurrentTab) return;
toolbar.current.setEditorToolbar(editorToolbar);
};
@@ -1412,14 +1424,14 @@ const MainFrame = (props: Props) => {
}));
};
const openStartPage = React.useCallback(
const openHomePage = React.useCallback(
() => {
setState(state => ({
...state,
editorTabs: openEditorTab(state.editorTabs, {
label: i18n._(t`Start Page`),
label: i18n._(t`Home`),
projectItemName: null,
renderEditorContainer: renderStartPageContainer,
renderEditorContainer: renderHomePageContainer,
key: 'start page',
closable: false,
}),
@@ -1537,28 +1549,27 @@ const MainFrame = (props: Props) => {
);
};
const openCreateDialog = React.useCallback(
(open: boolean = true) => {
setCreateDialogInitialTab('starters');
const openCreateProjectDialog = React.useCallback(
(tab: CreateProjectDialogTabs) => (open: boolean = true) => {
setCreateDialogInitialTab(tab);
setState(state => ({ ...state, createDialogOpen: open }));
},
[setState]
);
const onOpenTutorials = React.useCallback(
(open: boolean = true) => {
setCreateDialogInitialTab('tutorials');
setState(state => ({ ...state, createDialogOpen: open }));
},
[setState]
const closeCreateDialog = () => {
setState(state => ({ ...state, createDialogOpen: false }));
};
const onOpenTutorials = React.useMemo(
() => openCreateProjectDialog('tutorials'),
[openCreateProjectDialog]
);
const onOpenGamesShowcase = React.useCallback(
(open: boolean = true) => {
setCreateDialogInitialTab('games-showcase');
setState(state => ({ ...state, createDialogOpen: open }));
},
[setState]
const onOpenExamples = React.useMemo(
() => openCreateProjectDialog('examples'),
[openCreateProjectDialog]
);
const onOpenGamesShowcase = React.useMemo(
() => openCreateProjectDialog('games-showcase'),
[openCreateProjectDialog]
);
const openOpenFromStorageProviderDialog = React.useCallback(
@@ -1956,6 +1967,34 @@ const MainFrame = (props: Props) => {
}
};
const onOpenProjectAfterCreation = async ({
project,
storageProvider,
fileMetadata,
shouldCloseDialog,
}: {|
project?: gdProject,
storageProvider: ?StorageProvider,
fileMetadata: ?FileMetadata,
shouldCloseDialog?: boolean,
|}) => {
if (shouldCloseDialog)
await setState(state => ({ ...state, createDialogOpen: false }));
await getStorageProviderOperations(storageProvider);
let state;
if (project) state = await loadFromProject(project, fileMetadata);
else if (!!fileMetadata) state = await openFromFileMetadata(fileMetadata);
if (state) {
if (state.currentProject) state.currentProject.resetProjectUuid();
openSceneOrProjectManager({
currentProject: state.currentProject,
editorTabs: state.editorTabs,
});
}
};
const simulateUpdateDownloaded = () =>
setUpdateStatus({
status: 'update-downloaded',
@@ -1991,8 +2030,8 @@ const MainFrame = (props: Props) => {
onHotReloadPreview: launchHotReloadPreview,
onLaunchDebugPreview: launchDebuggerAndPreview,
onLaunchNetworkPreview: launchNetworkPreview,
onOpenStartPage: openStartPage,
onCreateProject: openCreateDialog,
onOpenHomePage: openHomePage,
onCreateProject: onOpenExamples,
onOpenProject: chooseProject,
onSaveProject: saveProject,
onSaveProjectAs: saveProjectAs,
@@ -2031,9 +2070,9 @@ const MainFrame = (props: Props) => {
onCloseProject: askToCloseProject,
onCloseApp: closeApp,
onExportProject: () => openExportDialog(true),
onCreateProject: openCreateDialog,
onCreateProject: onOpenExamples,
onOpenProjectManager: () => openProjectManager(true),
onOpenStartPage: openStartPage,
onOpenHomePage: openHomePage,
onOpenDebugger: openDebugger,
onOpenAbout: () => openAboutDialog(true),
onOpenPreferences: () => openPreferencesDialog(true),
@@ -2176,7 +2215,8 @@ const MainFrame = (props: Props) => {
extraEditorProps: editorTab.extraEditorProps,
project: currentProject,
ref: editorRef => (editorTab.editorRef = editorRef),
setToolbar: setEditorToolbar,
setToolbar: editorToolbar =>
setEditorToolbar(editorToolbar, isCurrentTab),
onChangeSubscription: () => openSubscriptionDialog(true),
projectItemName: editorTab.projectItemName,
setPreviewedLayout,
@@ -2198,11 +2238,16 @@ const MainFrame = (props: Props) => {
({ hiddenInOpenDialog }) => !hiddenInOpenDialog
).length,
onOpen: () => chooseProject(),
onCreate: () => openCreateDialog(),
onOpenRecentFile: openFromFileMetadataWithStorageProvider,
onCreateFromExampleShortHeader: onCreateFromExampleShortHeader,
onCreateBlank: onCreateBlank,
onOpenProjectAfterCreation: onOpenProjectAfterCreation,
onOpenProjectManager: () => openProjectManager(true),
onCloseProject: () => askToCloseProject(),
onOpenTutorials: () => onOpenTutorials(),
onOpenGamesShowcase: () => onOpenGamesShowcase(),
onOpenExamples: () => onOpenExamples(),
onOpenProfile: () => openProfileDialogWithTab('profile'),
onOpenHelpFinder: () => openHelpFinderDialog(true),
onOpenLanguageDialog: () => openLanguageDialog(true),
onLoadEventsFunctionsExtensions: () =>
@@ -2264,31 +2309,8 @@ const MainFrame = (props: Props) => {
renderCreateDialog({
open: state.createDialogOpen,
initialTab: createDialogInitialTab,
onClose: () => openCreateDialog(false),
onOpen: async (storageProvider, fileMetadata) => {
await setState(state => ({ ...state, createDialogOpen: false }));
await getStorageProviderOperations(storageProvider);
const state = await openFromFileMetadata(fileMetadata);
if (state) {
if (state.currentProject) state.currentProject.resetProjectUuid();
openSceneOrProjectManager({
currentProject: state.currentProject,
editorTabs: state.editorTabs,
});
}
},
onCreate: async (project, storageProvider, fileMetadata) => {
await setState(state => ({ ...state, createDialogOpen: false }));
await getStorageProviderOperations(storageProvider);
const state = await loadFromProject(project, fileMetadata);
if (state.currentProject) state.currentProject.resetProjectUuid();
openSceneOrProjectManager({
currentProject: state.currentProject,
editorTabs: state.editorTabs,
});
},
onClose: closeCreateDialog,
onOpen: onOpenProjectAfterCreation,
})}
{!!introDialog &&
introDialogOpen &&
@@ -2390,7 +2412,7 @@ const MainFrame = (props: Props) => {
}}
onCreateNewProject={() => {
openOpenFromStorageProviderDialog(false);
openCreateDialog(true);
closeCreateDialog();
}}
/>
)}

View File

@@ -8,10 +8,8 @@ import propertiesMapToSchema from '../../PropertiesEditor/PropertiesMapToSchema'
import EmptyMessage from '../../UI/EmptyMessage';
import { type EditorProps } from './EditorProps.flow';
import { Column, Line } from '../../UI/Grid';
import {
getExtraObjectsInformation,
getObjectTutorialHints,
} from '../../Hints';
import { getExtraObjectsInformation } from '../../Hints';
import { getObjectTutorialIds } from '../../Utils/GDevelopServices/Tutorial';
import AlertMessage from '../../UI/AlertMessage';
import { ColumnStackLayout } from '../../UI/Layout';
import DismissableTutorialMessage from '../../Hints/DismissableTutorialMessage';
@@ -20,80 +18,81 @@ const gd: libGDevelop = global.gd;
type Props = EditorProps;
export default class ObjectPropertiesEditor extends React.Component<Props> {
render() {
const {
object,
project,
resourceSources,
onChooseResource,
resourceExternalEditors,
} = this.props;
const ObjectPropertiesEditor = (props: Props) => {
const {
object,
project,
resourceSources,
onChooseResource,
resourceExternalEditors,
unsavedChanges,
} = props;
// TODO: Workaround a bad design of ObjectJsImplementation. When getProperties
// and associated methods are redefined in JS, they have different arguments (
// see ObjectJsImplementation C++ implementation). If called directly here from JS,
// the arguments will be mismatched. To workaround this, always case the object to
// a base gdObject to ensure C++ methods are called.
const objectAsGdObject = gd.castObject(object, gd.gdObject);
const properties = objectAsGdObject.getProperties();
// TODO: Workaround a bad design of ObjectJsImplementation. When getProperties
// and associated methods are redefined in JS, they have different arguments (
// see ObjectJsImplementation C++ implementation). If called directly here from JS,
// the arguments will be mismatched. To workaround this, always case the object to
// a base gdObject to ensure C++ methods are called.
const objectAsGdObject = gd.castObject(object, gd.gdObject);
const properties = objectAsGdObject.getProperties();
const propertiesSchema = propertiesMapToSchema(
properties,
object => object.getProperties(),
(object, name, value) => object.updateProperty(name, value)
);
const propertiesSchema = propertiesMapToSchema(
properties,
object => object.getProperties(),
(object, name, value) => object.updateProperty(name, value)
);
const extraInformation = getExtraObjectsInformation()[
objectAsGdObject.getType()
];
const extraInformation = getExtraObjectsInformation()[
objectAsGdObject.getType()
];
const tutorialHints = getObjectTutorialHints(objectAsGdObject.getType());
const tutorialIds = getObjectTutorialIds(objectAsGdObject.getType());
return (
<I18n>
{({ i18n }) => (
<ColumnStackLayout>
{tutorialHints.map(tutorialHint => (
<DismissableTutorialMessage
key={tutorialHint.identifier}
tutorialHint={tutorialHint}
return (
<I18n>
{({ i18n }) => (
<ColumnStackLayout>
{tutorialIds.map(tutorialId => (
<DismissableTutorialMessage
key={tutorialId}
tutorialId={tutorialId}
/>
))}
{propertiesSchema.length ? (
<React.Fragment>
{extraInformation ? (
<Line>
<Column noMargin>
{extraInformation.map(({ kind, message }, index) => (
<AlertMessage kind={kind} key={index}>
{i18n._(message)}
</AlertMessage>
))}
</Column>
</Line>
) : null}
<PropertiesEditor
unsavedChanges={unsavedChanges}
schema={propertiesSchema}
instances={[objectAsGdObject]}
project={project}
resourceSources={resourceSources}
onChooseResource={onChooseResource}
resourceExternalEditors={resourceExternalEditors}
/>
))}
{propertiesSchema.length ? (
<React.Fragment>
{extraInformation ? (
<Line>
<Column noMargin>
{extraInformation.map(({ kind, message }, index) => (
<AlertMessage kind={kind} key={index}>
{i18n._(message)}
</AlertMessage>
))}
</Column>
</Line>
) : null}
<PropertiesEditor
unsavedChanges={this.props.unsavedChanges}
schema={propertiesSchema}
instances={[objectAsGdObject]}
project={project}
resourceSources={resourceSources}
onChooseResource={onChooseResource}
resourceExternalEditors={resourceExternalEditors}
/>
</React.Fragment>
) : (
<EmptyMessage>
<Trans>
There is nothing to configure for this object. You can still
use events to interact with the object.
</Trans>
</EmptyMessage>
)}
</ColumnStackLayout>
)}
</I18n>
);
}
}
</React.Fragment>
) : (
<EmptyMessage>
<Trans>
There is nothing to configure for this object. You can still use
events to interact with the object.
</Trans>
</EmptyMessage>
)}
</ColumnStackLayout>
)}
</I18n>
);
};
export default ObjectPropertiesEditor;

View File

@@ -16,7 +16,7 @@ import { type EditorProps } from './EditorProps.flow';
import ResourceSelectorWithThumbnail from '../../ResourcesList/ResourceSelectorWithThumbnail';
import { ResponsiveLineStackLayout, ColumnStackLayout } from '../../UI/Layout';
import DismissableTutorialMessage from '../../Hints/DismissableTutorialMessage';
import { getObjectTutorialHints } from '../../Hints';
import { getObjectTutorialIds } from '../../Utils/GDevelopServices/Tutorial';
const gd = global.gd;
export default class ParticleEmitterEditor extends React.Component<
@@ -32,14 +32,14 @@ export default class ParticleEmitterEditor extends React.Component<
resourceExternalEditors,
} = this.props;
const particleEmitterObject = gd.asParticleEmitterObject(object);
const tutorialHints = getObjectTutorialHints(object.getType());
const tutorialIds = getObjectTutorialIds(object.getType());
return (
<ColumnStackLayout>
{tutorialHints.map(tutorialHint => (
{tutorialIds.map(tutorialId => (
<DismissableTutorialMessage
key={tutorialHint.identifier}
tutorialHint={tutorialHint}
key={tutorialId}
tutorialId={tutorialId}
/>
))}
<SelectField

View File

@@ -1,6 +1,8 @@
// @flow
import { Trans } from '@lingui/macro';
import { I18n } from '@lingui/react';
import React from 'react';
import every from 'lodash/every';
import FlatButton from '../../../../UI/FlatButton';
import EmptyMessage from '../../../../UI/EmptyMessage';
import { Line, Column } from '../../../../UI/Grid';
@@ -15,7 +17,6 @@ import {
} from '../Utils/SpriteObjectHelper';
import SpriteSelector from '../Utils/SpriteSelector';
import Window from '../../../../Utils/Window';
import every from 'lodash/every';
import ResourcesLoader from '../../../../ResourcesLoader';
import useForceUpdate from '../../../../Utils/UseForceUpdate';
import EditorMosaic, {
@@ -24,6 +25,8 @@ import EditorMosaic, {
} from '../../../../UI/EditorMosaic';
import { useResponsiveWindowWidth } from '../../../../UI/Reponsive/ResponsiveWindowMeasurer';
import Background from '../../../../UI/Background';
import AlertMessage from '../../../../UI/AlertMessage';
import { getWarningAboutPlatformerCollisionMaskEditing } from '../../../../Hints';
const gd: libGDevelop = global.gd;
const horizontalMosaicNodes: EditorMosaicNode = {
@@ -75,6 +78,21 @@ const CollisionMasksEditor = (props: Props) => {
spriteIndex
);
const objectHasPlatformerBehavior = props.object
.getAllBehaviorNames()
.toJSArray()
.map(behaviorName => props.object.getBehavior(behaviorName))
.some(
behavior =>
behavior.getTypeName() === 'PlatformBehavior::PlatformerObjectBehavior'
);
const warningAboutPlatformerCollisionMaskEditing =
!!sprite &&
!sprite.isCollisionMaskAutomatic() &&
objectHasPlatformerBehavior
? getWarningAboutPlatformerCollisionMaskEditing()
: null;
const updateCollisionMasks = React.useCallback(
() => {
if (animation && sprite) {
@@ -215,68 +233,86 @@ const CollisionMasksEditor = (props: Props) => {
type: 'secondary',
noTitleBar: true,
renderEditor: () => (
<Background>
<Line>
<Column expand>
<SpriteSelector
spriteObject={spriteObject}
animationIndex={animationIndex}
directionIndex={directionIndex}
spriteIndex={spriteIndex}
chooseAnimation={chooseAnimation}
chooseDirection={chooseDirection}
chooseSprite={chooseSprite}
sameForAllAnimations={sameCollisionMasksForAnimations}
sameForAllSprites={sameCollisionMasksForSprites}
setSameForAllAnimations={setSameCollisionMasksForAllAnimations}
setSameForAllSprites={setSameCollisionMasksForAllSprites}
setSameForAllAnimationsLabel={
<Trans>Share same collision masks for all animations</Trans>
}
setSameForAllSpritesLabel={
<Trans>
Share same collision masks for all sprites of this animation
</Trans>
}
/>
</Column>
</Line>
{!!sprite && !sprite.isCollisionMaskAutomatic() && (
<React.Fragment>
<PolygonsList
polygons={sprite.getCustomCollisionMask()}
onPolygonsUpdated={updateCollisionMasks}
restoreCollisionMask={() => onSetCollisionMaskAutomatic(true)}
spriteWidth={spriteWidth}
spriteHeight={spriteHeight}
/>
</React.Fragment>
)}
{!!sprite && sprite.isCollisionMaskAutomatic() && (
<React.Fragment>
<EmptyMessage>
<Trans>
This sprite uses the default collision mask, a rectangle that
is as large as the sprite.
</Trans>
</EmptyMessage>
<Line justifyContent="center">
<FlatButton
label={<Trans>Use a custom collision mask</Trans>}
primary={false}
onClick={() => onSetCollisionMaskAutomatic(false)}
/>
<I18n>
{({ i18n }) => (
<Background>
<Line>
<Column expand>
<SpriteSelector
spriteObject={spriteObject}
animationIndex={animationIndex}
directionIndex={directionIndex}
spriteIndex={spriteIndex}
chooseAnimation={chooseAnimation}
chooseDirection={chooseDirection}
chooseSprite={chooseSprite}
sameForAllAnimations={sameCollisionMasksForAnimations}
sameForAllSprites={sameCollisionMasksForSprites}
setSameForAllAnimations={
setSameCollisionMasksForAllAnimations
}
setSameForAllSprites={setSameCollisionMasksForAllSprites}
setSameForAllAnimationsLabel={
<Trans>
Share same collision masks for all animations
</Trans>
}
setSameForAllSpritesLabel={
<Trans>
Share same collision masks for all sprites of this
animation
</Trans>
}
/>
</Column>
</Line>
</React.Fragment>
{!!sprite && !sprite.isCollisionMaskAutomatic() && (
<React.Fragment>
<PolygonsList
polygons={sprite.getCustomCollisionMask()}
onPolygonsUpdated={updateCollisionMasks}
restoreCollisionMask={() =>
onSetCollisionMaskAutomatic(true)
}
spriteWidth={spriteWidth}
spriteHeight={spriteHeight}
/>
</React.Fragment>
)}
{!!sprite && sprite.isCollisionMaskAutomatic() && (
<React.Fragment>
<EmptyMessage>
<Trans>
This sprite uses the default collision mask, a rectangle
that is as large as the sprite.
</Trans>
</EmptyMessage>
<Line justifyContent="center">
<FlatButton
label={<Trans>Use a custom collision mask</Trans>}
primary={false}
onClick={() => onSetCollisionMaskAutomatic(false)}
/>
</Line>
</React.Fragment>
)}
{warningAboutPlatformerCollisionMaskEditing && (
<AlertMessage
kind={warningAboutPlatformerCollisionMaskEditing.kind}
>
{i18n._(warningAboutPlatformerCollisionMaskEditing.message)}
</AlertMessage>
)}
{!sprite && (
<EmptyMessage>
<Trans>
Choose an animation and frame to edit the collision masks
</Trans>
</EmptyMessage>
)}
</Background>
)}
{!sprite && (
<EmptyMessage>
<Trans>
Choose an animation and frame to edit the collision masks
</Trans>
</EmptyMessage>
)}
</Background>
</I18n>
),
},
};

View File

@@ -12,11 +12,17 @@ type Props = {
message?: React.Node,
onLogin: () => void,
onCreateAccount: () => void,
justifyContent?: 'center',
};
export default ({ message, onLogin, onCreateAccount }: Props) => (
export default ({
message,
onLogin,
onCreateAccount,
justifyContent,
}: Props) => (
<Column noMargin>
<Line>
<Line justifyContent={justifyContent}>
<Text>
{message || (
<Trans>

View File

@@ -1,55 +1,50 @@
// @flow
import { t } from '@lingui/macro';
import { I18n } from '@lingui/react';
import * as React from 'react';
import { sendNewGameCreated } from '../Utils/Analytics/EventSender';
import { I18n } from '@lingui/react';
import { type I18n as I18nType } from '@lingui/core';
import { ExampleStore } from '../AssetStore/ExampleStore';
import UrlStorageProvider from '../ProjectsStorage/UrlStorageProvider';
import { showErrorBox } from '../UI/Messages/MessageBox';
import { type ExampleShortHeader } from '../Utils/GDevelopServices/Example';
import {
getExample,
type ExampleShortHeader,
} from '../Utils/GDevelopServices/Example';
import { type StorageProvider, type FileMetadata } from '../ProjectsStorage';
type OnCreateFromExampleShortHeaderFunction,
type OnOpenProjectAfterCreationFunction,
} from '../ProjectCreation/CreateProjectDialog';
type Props = {|
onOpen: (
storageProvider: StorageProvider,
fileMetadata: FileMetadata
) => Promise<void>,
onOpen: OnOpenProjectAfterCreationFunction,
onCreateFromExampleShortHeader: OnCreateFromExampleShortHeaderFunction,
|};
export default function BrowserExamples(props: Props) {
export default function BrowserExamples({
onOpen,
onCreateFromExampleShortHeader,
}: Props) {
const [isOpening, setIsOpening] = React.useState(false);
const createProjectFromExample = async (
i18n: I18nType,
exampleShortHeader: ExampleShortHeader
) => {
setIsOpening(true);
const projectMetadata = await onCreateFromExampleShortHeader({
i18n,
exampleShortHeader: exampleShortHeader,
});
if (projectMetadata) {
const { storageProvider, fileMetadata } = projectMetadata;
onOpen({ storageProvider, fileMetadata, shouldCloseDialog: true });
}
setIsOpening(false);
};
return (
<I18n>
{({ i18n }) => (
<ExampleStore
isOpening={isOpening}
onOpen={async (exampleShortHeader: ExampleShortHeader) => {
try {
setIsOpening(true);
const example = await getExample(exampleShortHeader);
props.onOpen(UrlStorageProvider, {
fileIdentifier: example.projectFileUrl,
});
sendNewGameCreated(example.projectFileUrl);
} catch (error) {
showErrorBox({
message:
i18n._(t`Unable to fetch the example.`) +
' ' +
i18n._(
t`Verify your internet connection or try again later.`
),
rawError: error,
errorId: 'browser-example-load-error',
});
} finally {
setIsOpening(false);
}
}}
onOpen={(exampleShortHeader: ExampleShortHeader) =>
createProjectFromExample(i18n, exampleShortHeader)
}
/>
)}
</I18n>

View File

@@ -1,256 +0,0 @@
// @flow
import { Trans } from '@lingui/macro';
import React, { Component } from 'react';
import { List, ListItem } from '../UI/List';
import Subheader from '../UI/Subheader';
import RaisedButton from '../UI/RaisedButton';
import { sendNewGameCreated } from '../Utils/Analytics/EventSender';
import { Column, Line } from '../UI/Grid';
import ListIcon from '../UI/ListIcon';
import Text from '../UI/Text';
import { type StorageProvider, type FileMetadata } from '../ProjectsStorage';
import UrlStorageProvider from '../ProjectsStorage/UrlStorageProvider';
const gd: libGDevelop = global.gd;
type Props = {|
onOpen: (
storageProvider: StorageProvider,
fileMetadata: FileMetadata
) => void,
onCreate: (
gdProject,
storageProvider: ?StorageProvider,
fileMetadata: ?FileMetadata
) => void,
onShowExamples: () => void,
|};
export default class BrowserStarters extends Component<Props> {
render() {
return (
<Column noMargin>
<Line>
<Column>
<Text>
<Trans>Choose a project to use as a starter:</Trans>
</Text>
</Column>
</Line>
<Line>
<Column expand noMargin>
<List>
<ListItem
leftIcon={
<ListIcon
iconSize={40}
src="res/starters_icons/platformer.png"
/>
}
primaryText={<Trans>Platformer</Trans>}
secondaryText={
<Trans>
A simple platform game, with coins to collect, moving
platforms and enemies.
</Trans>
}
secondaryTextLines={2}
onClick={() => {
sendNewGameCreated('platformer');
this.props.onOpen(UrlStorageProvider, {
fileIdentifier:
'https://resources.gdevelop-app.com/examples/platformer/platformer.json',
});
}}
/>
<ListItem
leftIcon={
<ListIcon
iconSize={40}
src="res/starters_icons/space-shooter.png"
/>
}
primaryText={<Trans>8-bit Space Shooter</Trans>}
secondaryText={
<Trans>
A beautiful, retro side-scrolling shooter where you must
defeat incoming enemies with your mecha transforming
spaceship. Huge boss included!
</Trans>
}
secondaryTextLines={2}
onClick={() => {
sendNewGameCreated('space-shooter');
this.props.onOpen(UrlStorageProvider, {
fileIdentifier:
'https://resources.gdevelop-app.com/examples/space-shooter/space-shooter.json',
});
}}
/>
<ListItem
leftIcon={
<ListIcon
iconSize={40}
src="res/starters_icons/geometry-monster.png"
/>
}
primaryText={<Trans>Geometry Monster</Trans>}
secondaryText={
<Trans>
A hyper casual endless game where you have to collect shapes
and avoid bombs, with a progressively increasing difficulty.
</Trans>
}
secondaryTextLines={2}
onClick={() => {
sendNewGameCreated('geometry-monster');
this.props.onOpen(UrlStorageProvider, {
fileIdentifier:
'https://resources.gdevelop-app.com/examples/geometry-monster/geometry-monster.json',
});
}}
/>
<ListItem
leftIcon={
<ListIcon
iconSize={40}
src="res/starters_icons/isometric-game.png"
/>
}
primaryText={<Trans>Isometric game</Trans>}
secondaryText={
<Trans>
An example of an isometric game where you can explore a map
with your character.
</Trans>
}
secondaryTextLines={2}
onClick={() => {
sendNewGameCreated('isometric-game');
this.props.onOpen(UrlStorageProvider, {
fileIdentifier:
'https://resources.gdevelop-app.com/examples/isometric-game/isometric-game.json',
});
}}
/>
<ListItem
leftIcon={
<ListIcon
iconSize={40}
src="res/starters_icons/downhill-bike-physics-demo.png"
/>
}
primaryText="Downhill Bike Racing"
secondaryText={
<Trans>
An example of a 2D physics based game, where players have to
reach the end as fast as possible.
</Trans>
}
secondaryTextLines={2}
onClick={() => {
sendNewGameCreated('downhill-bike-physics-demo');
this.props.onOpen(UrlStorageProvider, {
fileIdentifier:
'https://resources.gdevelop-app.com/examples/downhill-bike-physics-demo/downhill-bike-physics-demo.json',
});
}}
/>
<ListItem
leftIcon={
<ListIcon iconSize={40} src="res/starters_icons/pairs.png" />
}
primaryText="Pairs"
secondaryText={
<Trans>
Find all matching pairs of cards in this relaxing game. Use
tweens to create smooth, natural animations with a few
events.
</Trans>
}
secondaryTextLines={2}
onClick={() => {
sendNewGameCreated('pairs');
this.props.onOpen(UrlStorageProvider, {
fileIdentifier:
'https://resources.gdevelop-app.com/examples/pairs/pairs.json',
});
}}
/>
<ListItem
leftIcon={
<ListIcon iconSize={40} src="res/starters_icons/new.png" />
}
primaryText={<Trans>Empty project</Trans>}
secondaryText={<Trans>Start a new project from scratch.</Trans>}
secondaryTextLines={2}
onClick={() => {
sendNewGameCreated('');
const project = gd.ProjectHelper.createNewGDJSProject();
this.props.onCreate(project, null, null);
}}
/>
<Subheader>
<Trans>Advanced</Trans>
</Subheader>
<ListItem
leftIcon={
<ListIcon
iconSize={40}
src="res/starters_icons/particle-effects-demo.png"
/>
}
primaryText={<Trans>Particle Effects Demo</Trans>}
secondaryText={
<Trans>
A demo of various high quality particle effects (fire,
magic, snow, rune spell...) that you can try and use in your
game.
</Trans>
}
secondaryTextLines={2}
onClick={() => {
sendNewGameCreated('particle-effects-demo');
this.props.onOpen(UrlStorageProvider, {
fileIdentifier:
'https://resources.gdevelop-app.com/examples/particle-effects-demo/particle-effects-demo.json',
});
}}
/>
<ListItem
leftIcon={
<ListIcon
iconSize={40}
src="res/starters_icons/game-feel-demo.png"
/>
}
primaryText={<Trans>Game Feel Demo</Trans>}
secondaryText={
<Trans>
A demo showing how to enhance the "game feel" of your
project: VFX, shot trail, ambient sounds and SFX,
screenshake, wobble...
</Trans>
}
secondaryTextLines={2}
onClick={() => {
sendNewGameCreated('game-feel-demo');
this.props.onOpen(UrlStorageProvider, {
fileIdentifier:
'https://resources.gdevelop-app.com/examples/game-feel-demo/game-feel-demo.json',
});
}}
/>
</List>
<Line alignItems="center" justifyContent="center">
<RaisedButton
label={<Trans>See examples</Trans>}
onClick={() => this.props.onShowExamples()}
/>
</Line>
</Column>
</Line>
</Column>
);
}
}

View File

@@ -1,15 +1,16 @@
// @flow
import { Trans } from '@lingui/macro';
import { type I18n as I18nType } from '@lingui/core';
import * as React from 'react';
import Dialog from '../UI/Dialog';
import FlatButton from '../UI/FlatButton';
import ScrollView from '../UI/ScrollView';
import { Tabs, Tab } from '../UI/Tabs';
import Tutorials from './Tutorials';
import { TutorialsList } from '../Tutorial';
import { Column } from '../UI/Grid';
import { type StorageProvider, type FileMetadata } from '../ProjectsStorage';
import { GamesShowcase } from '../GamesShowcase';
import { type ExampleShortHeader } from '../Utils/GDevelopServices/Example';
import Window from '../Utils/Window';
import PublishIcon from '@material-ui/icons/Publish';
import { findEmptyPath } from './LocalPathFinder';
@@ -18,30 +19,52 @@ const path = optionalRequire('path');
const electron = optionalRequire('electron');
const app = electron ? electron.remote.app : null;
export type CreateProjectDialogTabs =
| 'examples'
| 'tutorials'
| 'games-showcase';
type State = {|
currentTab: 'starters' | 'examples' | 'tutorials' | 'games-showcase',
currentTab: CreateProjectDialogTabs,
outputPath: string,
|};
export type OnOpenProjectAfterCreationFunction = ({|
project?: gdProject,
storageProvider: ?StorageProvider,
fileMetadata: ?FileMetadata,
shouldCloseDialog?: boolean,
|}) => Promise<void>;
export type CreateProjectDialogWithComponentsProps = {|
open: boolean,
onClose: () => void,
onOpen: (
storageProvider: StorageProvider,
fileMetadata: FileMetadata
) => Promise<void>,
onCreate: (
gdProject,
storageProvider: ?StorageProvider,
fileMetadata: ?FileMetadata
) => Promise<void>,
initialTab: 'starters' | 'tutorials' | 'games-showcase',
onOpen: OnOpenProjectAfterCreationFunction,
initialTab: CreateProjectDialogTabs,
|};
export type OnCreateBlankFunction = ({|
i18n: I18nType,
outputPath?: string,
|}) => Promise<?{|
project: gdProject,
storageProvider: ?StorageProvider,
fileMetadata: ?FileMetadata,
|}>;
export type OnCreateFromExampleShortHeaderFunction = ({|
i18n: I18nType,
exampleShortHeader: ExampleShortHeader,
outputPath?: string,
|}) => Promise<?{|
storageProvider: StorageProvider,
fileMetadata: FileMetadata,
|}>;
type Props = {|
...CreateProjectDialogWithComponentsProps,
startersComponent: any,
examplesComponent: any,
onCreateFromExampleShortHeader: OnCreateFromExampleShortHeaderFunction,
|};
export default class CreateProjectDialog extends React.Component<Props, State> {
@@ -52,9 +75,7 @@ export default class CreateProjectDialog extends React.Component<Props, State> {
: '',
};
_onChangeTab = (
newTab: 'starters' | 'examples' | 'tutorials' | 'games-showcase'
) => {
_onChangeTab = (newTab: CreateProjectDialogTabs) => {
this.setState({
currentTab: newTab,
});
@@ -63,11 +84,15 @@ export default class CreateProjectDialog extends React.Component<Props, State> {
_showExamples = () => this._onChangeTab('examples');
render() {
const { open, onClose, onOpen, onCreate } = this.props;
const {
open,
onClose,
onOpen,
onCreateFromExampleShortHeader,
} = this.props;
if (!open) return null;
const ExamplesComponent = this.props.examplesComponent;
const StartersComponent = this.props.startersComponent;
return (
<Dialog
@@ -117,34 +142,19 @@ export default class CreateProjectDialog extends React.Component<Props, State> {
>
<Column expand noMargin>
<Tabs value={this.state.currentTab} onChange={this._onChangeTab}>
<Tab label={<Trans>Starters</Trans>} value="starters" />
<Tab label={<Trans>Examples</Trans>} value="examples" />
<Tab label={<Trans>Tutorials</Trans>} value="tutorials" />
<Tab label={<Trans>Games showcase</Trans>} value="games-showcase" />
</Tabs>
{this.state.currentTab === 'starters' && (
<ScrollView>
<StartersComponent
onOpen={onOpen}
onCreate={onCreate}
onChangeOutputPath={outputPath => this.setState({ outputPath })}
onShowExamples={this._showExamples}
outputPath={this.state.outputPath}
/>
</ScrollView>
)}
{this.state.currentTab === 'examples' && (
<ExamplesComponent
onOpen={onOpen}
onChangeOutputPath={outputPath => this.setState({ outputPath })}
outputPath={this.state.outputPath}
onCreateFromExampleShortHeader={onCreateFromExampleShortHeader}
/>
)}
{this.state.currentTab === 'tutorials' && (
<ScrollView>
<Tutorials />
</ScrollView>
)}
{this.state.currentTab === 'tutorials' && <TutorialsList />}
{this.state.currentTab === 'games-showcase' && <GamesShowcase />}
</Column>
</Dialog>

View File

@@ -3,29 +3,22 @@ import { t } from '@lingui/macro';
import { I18n } from '@lingui/react';
import { type I18n as I18nType } from '@lingui/core';
import * as React from 'react';
import { ExampleStore } from '../AssetStore/ExampleStore';
import { getExample } from '../Utils/GDevelopServices/Example';
import Divider from '@material-ui/core/Divider';
import LocalFolderPicker from '../UI/LocalFolderPicker';
import { sendNewGameCreated } from '../Utils/Analytics/EventSender';
import { ExampleStore } from '../AssetStore/ExampleStore';
import { type ExampleShortHeader } from '../Utils/GDevelopServices/Example';
import { Column, Line } from '../UI/Grid';
import optionalRequire from '../Utils/OptionalRequire.js';
import { Column } from '../UI/Grid';
import { showErrorBox } from '../UI/Messages/MessageBox';
import { type StorageProvider, type FileMetadata } from '../ProjectsStorage';
import LocalFileStorageProvider from '../ProjectsStorage/LocalFileStorageProvider';
import { writeAndCheckFile } from '../ProjectsStorage/LocalFileStorageProvider/LocalProjectWriter';
import axios from 'axios';
const path = optionalRequire('path');
var fs = optionalRequire('fs-extra');
import LocalProjectPreCreationDialog from './LocalProjectPreCreationDialog';
import {
type OnCreateFromExampleShortHeaderFunction,
type OnOpenProjectAfterCreationFunction,
} from '../ProjectCreation/CreateProjectDialog';
type Props = {|
onOpen: (
storageProvider: StorageProvider,
fileMetadata: FileMetadata
) => void,
onOpen: OnOpenProjectAfterCreationFunction,
onChangeOutputPath: (outputPath: string) => void,
outputPath: string,
onCreateFromExampleShortHeader: OnCreateFromExampleShortHeaderFunction,
|};
export const showGameFileCreationError = (
@@ -46,70 +39,54 @@ export default function LocalExamples({
outputPath,
onChangeOutputPath,
onOpen,
onCreateFromExampleShortHeader,
}: Props) {
const [isOpening, setIsOpening] = React.useState(false);
const [isOpening, setIsOpening] = React.useState<boolean>(false);
const [
selectedExampleShortHeader,
setSelectedExampleShortShortHeader,
] = React.useState<?ExampleShortHeader>(null);
const createProjectFromExample = async (i18n: I18nType) => {
if (!selectedExampleShortHeader) return;
setIsOpening(true);
const projectMetadata = await onCreateFromExampleShortHeader({
i18n,
outputPath,
exampleShortHeader: selectedExampleShortHeader,
});
if (!!projectMetadata) {
const { storageProvider, fileMetadata } = projectMetadata;
onOpen({ storageProvider, fileMetadata, shouldCloseDialog: true });
}
setIsOpening(false);
};
return (
<I18n>
{({ i18n }) => (
<Column noMargin expand useFullHeight>
<Line>
<Column expand>
<LocalFolderPicker
fullWidth
value={outputPath}
onChange={onChangeOutputPath}
type="create-game"
/>
</Column>
</Line>
<Divider />
<ExampleStore
isOpening={isOpening}
onOpen={async (exampleShortHeader: ExampleShortHeader) => {
if (!fs || !outputPath) return;
try {
setIsOpening(true);
const example = await getExample(exampleShortHeader);
// Prepare the folder for the example.
fs.mkdirsSync(outputPath);
// Download the project file and save it.
const response = await axios.get(example.projectFileUrl, {
responseType: 'text',
// Required to properly get the response as text, and not as JSON:
transformResponse: [data => data],
});
const projectFileContent = response.data;
const localFilePath = path.join(outputPath, 'game.json');
await writeAndCheckFile(projectFileContent, localFilePath);
// Open the project file. Note that resources that are URLs will be downloaded
// thanks to the LocalResourceFetcher.
onOpen(LocalFileStorageProvider, {
fileIdentifier: localFilePath,
});
sendNewGameCreated(example.projectFileUrl);
} catch (error) {
showErrorBox({
message:
i18n._(t`Unable to load the example or save it on disk.`) +
' ' +
i18n._(
t`Verify your internet connection or try again later.`
),
rawError: error,
errorId: 'local-example-load-error',
});
} finally {
setIsOpening(false);
<>
<Column noMargin expand useFullHeight>
<Divider />
<ExampleStore
isOpening={isOpening}
onOpen={async (example: ?ExampleShortHeader) =>
setSelectedExampleShortShortHeader(example)
}
}}
/>
</Column>
/>
</Column>
{selectedExampleShortHeader && (
<LocalProjectPreCreationDialog
open
onClose={() => setSelectedExampleShortShortHeader(null)}
onCreate={() => createProjectFromExample(i18n)}
outputPath={outputPath}
onChangeOutputPath={onChangeOutputPath}
/>
)}
</>
)}
</I18n>
);

View File

@@ -0,0 +1,56 @@
// @flow
import { Trans } from '@lingui/macro';
import * as React from 'react';
import Dialog from '../UI/Dialog';
import FlatButton from '../UI/FlatButton';
import RaisedButton from '../UI/RaisedButton';
import { Column } from '../UI/Grid';
import LocalFolderPicker from '../UI/LocalFolderPicker';
type Props = {|
open: boolean,
onClose: () => void,
onCreate: () => void | Promise<void>,
outputPath: string,
onChangeOutputPath: (outputPath: string) => void,
|};
const LocalProjectPreCreationDialog = ({
open,
onClose,
onCreate,
outputPath,
onChangeOutputPath,
}: Props): React.Node => {
return (
<Dialog
title={<Trans>Project settings</Trans>}
maxWidth="sm"
open={open}
actions={[
<FlatButton
label={<Trans>Cancel</Trans>}
onClick={onClose}
key="cancel"
/>,
<RaisedButton
primary
key="create"
label={<Trans>Create project</Trans>}
onClick={onCreate}
/>,
]}
>
<Column>
<LocalFolderPicker
fullWidth
value={outputPath}
onChange={onChangeOutputPath}
type="create-game"
/>
</Column>
</Dialog>
);
};
export default LocalProjectPreCreationDialog;

View File

@@ -1,292 +0,0 @@
// @flow
import { Trans } from '@lingui/macro';
import { I18n } from '@lingui/react';
import { type I18n as I18nType } from '@lingui/core';
import React, { Component } from 'react';
import Divider from '@material-ui/core/Divider';
import RaisedButton from '../UI/RaisedButton';
import LocalFolderPicker from '../UI/LocalFolderPicker';
import Text from '../UI/Text';
import { sendNewGameCreated } from '../Utils/Analytics/EventSender';
import { Column, Line } from '../UI/Grid';
import { List, ListItem } from '../UI/List';
import Subheader from '../UI/Subheader';
import { findExamples } from './LocalExamplesFinder';
import optionalRequire from '../Utils/OptionalRequire.js';
import ListIcon from '../UI/ListIcon';
import { showGameFileCreationError } from './LocalExamples';
import { type StorageProvider, type FileMetadata } from '../ProjectsStorage';
import LocalFileStorageProvider from '../ProjectsStorage/LocalFileStorageProvider';
const path = optionalRequire('path');
var fs = optionalRequire('fs-extra');
const gd: libGDevelop = global.gd;
type Props = {|
onOpen: (
storageProvider: StorageProvider,
fileMetadata: FileMetadata
) => void,
onCreate: (
gdProject,
storageProvider: ?StorageProvider,
fileMetadata: ?FileMetadata
) => void,
onChangeOutputPath: (outputPath: string) => void,
onShowExamples: () => void,
outputPath: string,
|};
type State = {||};
export default class LocalStarters extends Component<Props, State> {
createFromExample(i18n: I18nType, exampleName: string) {
const { outputPath } = this.props;
if (!fs || !outputPath) return;
findExamples(examplesPath => {
try {
fs.mkdirsSync(outputPath);
fs.copySync(path.join(examplesPath, exampleName), outputPath);
} catch (error) {
showGameFileCreationError(i18n, outputPath, error);
return;
}
this.props.onOpen(LocalFileStorageProvider, {
fileIdentifier: path.join(outputPath, exampleName + '.json'),
});
sendNewGameCreated(exampleName);
});
}
createEmptyGame(i18n: I18nType) {
const { outputPath } = this.props;
if (!fs || !outputPath) return;
try {
fs.mkdirsSync(outputPath);
} catch (error) {
showGameFileCreationError(i18n, outputPath, error);
return;
}
const project: gdProject = gd.ProjectHelper.createNewGDJSProject();
const filePath = path.join(outputPath, 'game.json');
project.setProjectFile(filePath);
this.props.onCreate(project, LocalFileStorageProvider, {
fileIdentifier: filePath,
});
sendNewGameCreated('');
}
render() {
return (
<I18n>
{({ i18n }) => (
<Column noMargin>
<Line expand>
<Column expand>
<LocalFolderPicker
fullWidth
value={this.props.outputPath}
onChange={this.props.onChangeOutputPath}
type="create-game"
/>
</Column>
</Line>
<Divider />
<Line>
<Column>
<Text>
<Trans>Choose a game to use as a starter:</Trans>
</Text>
</Column>
</Line>
<Line>
<Column expand noMargin>
<List>
<ListItem
leftIcon={
<ListIcon
iconSize={40}
src="res/starters_icons/platformer.png"
/>
}
primaryText={<Trans>Platformer</Trans>}
secondaryText={
<Trans>
A simple platform game, with coins to collect, moving
platforms and enemies.
</Trans>
}
secondaryTextLines={2}
onClick={() => this.createFromExample(i18n, 'platformer')}
/>
<ListItem
leftIcon={
<ListIcon
iconSize={40}
src="res/starters_icons/space-shooter.png"
/>
}
primaryText={<Trans>8-bit Space Shooter</Trans>}
secondaryText={
<Trans>
A beautiful, retro side-scrolling shooter where you must
defeat incoming enemies with your mecha transforming
spaceship. Huge boss included!
</Trans>
}
secondaryTextLines={2}
onClick={() =>
this.createFromExample(i18n, 'space-shooter')
}
/>
<ListItem
leftIcon={
<ListIcon
iconSize={40}
src="res/starters_icons/geometry-monster.png"
/>
}
primaryText={<Trans>Geometry Monster</Trans>}
secondaryText={
<Trans>
A hyper casual endless game where you have to collect
shapes and avoid bombs, with a progressively increasing
difficulty.
</Trans>
}
secondaryTextLines={2}
onClick={() =>
this.createFromExample(i18n, 'geometry-monster')
}
/>
<ListItem
leftIcon={
<ListIcon
iconSize={40}
src="res/starters_icons/isometric-game.png"
/>
}
primaryText={<Trans>Isometric game</Trans>}
secondaryText={
<Trans>
An example of an isometric game where you can explore a
map with your character.
</Trans>
}
secondaryTextLines={2}
onClick={() =>
this.createFromExample(i18n, 'isometric-game')
}
/>
<ListItem
leftIcon={
<ListIcon
iconSize={40}
src="res/starters_icons/downhill-bike-physics-demo.png"
/>
}
primaryText="Downhill Bike Racing"
secondaryText={
<Trans>
An example of a 2D physics based game, where players
have to reach the end as fast as possible.
</Trans>
}
secondaryTextLines={2}
onClick={() =>
this.createFromExample(i18n, 'downhill-bike-physics-demo')
}
/>
<ListItem
leftIcon={
<ListIcon
iconSize={40}
src="res/starters_icons/pairs.png"
/>
}
primaryText="Pairs"
secondaryText={
<Trans>
Find all matching pairs of cards in this relaxing game.
Use tweens to create smooth, natural animations with a
few events.
</Trans>
}
secondaryTextLines={2}
onClick={() => this.createFromExample(i18n, 'pairs')}
/>
<ListItem
leftIcon={
<ListIcon
iconSize={40}
src="res/starters_icons/new.png"
/>
}
primaryText={<Trans>Empty game</Trans>}
secondaryText={
<Trans>Start a new game from scratch.</Trans>
}
secondaryTextLines={2}
onClick={() => this.createEmptyGame(i18n)}
/>
<Subheader>
<Trans>Advanced</Trans>
</Subheader>
<ListItem
leftIcon={
<ListIcon
iconSize={40}
src="res/starters_icons/particle-effects-demo.png"
/>
}
primaryText={<Trans>Particle Effects Demo</Trans>}
secondaryText={
<Trans>
A demo of various high quality particle effects (fire,
magic, snow, rune spell...) that you can try and use in
your game.
</Trans>
}
secondaryTextLines={2}
onClick={() =>
this.createFromExample(i18n, 'particle-effects-demo')
}
/>
<ListItem
leftIcon={
<ListIcon
iconSize={40}
src="res/starters_icons/game-feel-demo.png"
/>
}
primaryText={<Trans>Game Feel Demo</Trans>}
secondaryText={
<Trans>
A demo showing how to enhance the "game feel" of your
project: VFX, shot trail, ambient sounds and SFX,
screenshake, wobble...
</Trans>
}
secondaryTextLines={2}
onClick={() =>
this.createFromExample(i18n, 'game-feel-demo')
}
/>
</List>
<Line alignItems="center" justifyContent="center">
<RaisedButton
label={<Trans>See examples</Trans>}
onClick={() => this.props.onShowExamples()}
/>
</Line>
</Column>
</Line>
</Column>
)}
</I18n>
);
}
}

View File

@@ -1,151 +0,0 @@
// @flow
import { Trans } from '@lingui/macro';
import { type I18n as I18nType } from '@lingui/core';
import * as React from 'react';
import { Column, Line } from '../UI/Grid';
import { sendTutorialOpened } from '../Utils/Analytics/EventSender';
import Window from '../Utils/Window';
import { List, ListItem } from '../UI/List';
import { ColumnStackLayout } from '../UI/Layout';
import Text from '../UI/Text';
import { getAllTutorialHints, type TutorialHint } from '../Hints';
import { I18n } from '@lingui/react';
import ListIcon from '../UI/ListIcon';
import RaisedButton from '../UI/RaisedButton';
import { getHelpLink } from '../Utils/HelpLink';
const TutorialListItem = ({
tutorialHint,
i18n,
}: {|
tutorialHint: TutorialHint,
i18n: I18nType,
|}) => {
return (
<ListItem
leftIcon={
tutorialHint.iconSrc ? (
<ListIcon
iconWidth={120}
iconHeight={60}
src={tutorialHint.iconSrc}
/>
) : null
}
primaryText={tutorialHint.name}
secondaryText={i18n._(tutorialHint.message)}
onClick={() => {
sendTutorialOpened(tutorialHint.identifier);
Window.openExternalURL(tutorialHint.link);
}}
/>
);
};
export default function Tutorials() {
const allTutorials = getAllTutorialHints();
const featuredForGettingStartedTutorials = allTutorials.filter(
tutorialHint => {
return !!tutorialHint.featuredForGettingStarted;
}
);
const videoTutorials = allTutorials.filter(tutorialHint => {
return (
!tutorialHint.featuredForGettingStarted &&
tutorialHint.kind === 'video-tutorial'
);
});
const nonVideoTutorials = allTutorials.filter(tutorialHint => {
return (
!tutorialHint.featuredForGettingStarted &&
tutorialHint.kind !== 'video-tutorial'
);
});
return (
<I18n>
{({ i18n }) => (
<ColumnStackLayout noMargin>
<Column>
<Text>
<Trans>
Get started by following a tutorial, the best way to understand
how GDevelop works.
</Trans>
</Text>
</Column>
<Column expand noMargin>
<Column>
<Text size="title" noMargin>
<Trans>Getting Started</Trans>
</Text>
</Column>
<List>
<List>
{featuredForGettingStartedTutorials.map(tutorialHint => (
<TutorialListItem
key={tutorialHint.identifier}
tutorialHint={tutorialHint}
i18n={i18n}
/>
))}
</List>
</List>
</Column>
<Line justifyContent="center" noMargin>
<RaisedButton
primary
label={<Trans>Open GDevelop documentation</Trans>}
onClick={() => {
Window.openExternalURL(getHelpLink('/'));
}}
/>
</Line>
<Column noMargin>
<Column>
<Text size="title" noMargin>
<Trans>Video Tutorials</Trans>
</Text>
</Column>
<List>
{videoTutorials.map(tutorialHint => (
<TutorialListItem
key={tutorialHint.identifier}
tutorialHint={tutorialHint}
i18n={i18n}
/>
))}
</List>
</Column>
<Line justifyContent="center" noMargin>
<RaisedButton
primary
label={<Trans>Find more on GDevelop Youtube channel</Trans>}
onClick={() => {
Window.openExternalURL('https://www.youtube.com/c/GDevelopApp');
}}
/>
</Line>
<Column noMargin>
<Column>
<Text size="title" noMargin>
<Trans>Other Community Tutorials</Trans>
</Text>
</Column>
<List>
<List>
{nonVideoTutorials.map(tutorialHint => (
<TutorialListItem
key={tutorialHint.identifier}
tutorialHint={tutorialHint}
i18n={i18n}
/>
))}
</List>
</List>
</Column>
</ColumnStackLayout>
)}
</I18n>
);
}

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