mirror of
https://github.com/4ian/GDevelop.git
synced 2025-10-15 10:19:04 +00:00
Compare commits
30 Commits
v5.0.122
...
add-warnin
Author | SHA1 | Date | |
---|---|---|---|
![]() |
1f9d7860cf | ||
![]() |
83d6c7a2a2 | ||
![]() |
f7b0cfab1f | ||
![]() |
7e6a277335 | ||
![]() |
834a28ddbc | ||
![]() |
945555a8e9 | ||
![]() |
ad3d1dd8c3 | ||
![]() |
fd47282456 | ||
![]() |
dff1c88ef7 | ||
![]() |
4ea622ff99 | ||
![]() |
17ea918a91 | ||
![]() |
cc6af8979d | ||
![]() |
132e20fd24 | ||
![]() |
fb6a88785a | ||
![]() |
8a159d7ff5 | ||
![]() |
13c85bbe45 | ||
![]() |
ce8323e8e1 | ||
![]() |
dbc7a74e45 | ||
![]() |
cfb1d6888e | ||
![]() |
816dc8cc74 | ||
![]() |
106549e5fa | ||
![]() |
f8ca06d530 | ||
![]() |
34cbcdbc3a | ||
![]() |
3b208502ae | ||
![]() |
e3654fca99 | ||
![]() |
2a386cdcf1 | ||
![]() |
b134896687 | ||
![]() |
705dff43bc | ||
![]() |
d9eaf71ed1 | ||
![]() |
008b4291ab |
@@ -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")
|
||||
|
@@ -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"));
|
||||
}
|
||||
|
1
Extensions/P2P/A_peer.js
vendored
1
Extensions/P2P/A_peer.js
vendored
@@ -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
|
1
Extensions/P2P/A_peer.js.map
Normal file
1
Extensions/P2P/A_peer.js.map
Normal file
File diff suppressed because one or more lines are too long
@@ -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
|
||||
|
@@ -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',
|
||||
|
@@ -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,
|
||||
|
@@ -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 {
|
||||
|
@@ -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
3896
GDJS/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
117
GDJS/tests/tests/runtimeobject.separateFromObjects.js
Normal file
117
GDJS/tests/tests/runtimeobject.separateFromObjects.js
Normal 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
1
newIDE/app/.env
Normal file
@@ -0,0 +1 @@
|
||||
EXTEND_ESLINT = true
|
1
newIDE/app/.gitignore
vendored
1
newIDE/app/.gitignore
vendored
@@ -16,7 +16,6 @@ build
|
||||
|
||||
# misc
|
||||
.DS_Store
|
||||
.env
|
||||
.env.local
|
||||
npm-debug.log
|
||||
|
||||
|
@@ -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: {
|
||||
|
51175
newIDE/app/package-lock.json
generated
51175
newIDE/app/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -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": [
|
||||
|
BIN
newIDE/app/public/res/ribbon_default/networkicon32.png
Normal file
BIN
newIDE/app/public/res/ribbon_default/networkicon32.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 787 B |
@@ -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);
|
||||
|
@@ -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>
|
||||
)}
|
||||
|
@@ -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>
|
||||
|
@@ -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');
|
||||
|
||||
|
@@ -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,
|
||||
|
@@ -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>
|
||||
|
@@ -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: {
|
||||
|
@@ -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>
|
||||
|
@@ -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>
|
||||
|
@@ -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>
|
||||
|
@@ -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>
|
||||
|
@@ -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}
|
||||
|
@@ -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`,
|
||||
|
@@ -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() {
|
||||
|
@@ -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>
|
||||
);
|
||||
}
|
||||
|
@@ -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>
|
||||
}
|
||||
/>
|
||||
|
@@ -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>
|
||||
|
@@ -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
|
||||
|
@@ -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
|
||||
|
@@ -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>
|
||||
|
@@ -55,6 +55,7 @@ export const browserOnlineCordovaExportPipeline: ExportPipeline<
|
||||
> = {
|
||||
name: 'browser-online-cordova',
|
||||
onlineBuildType: 'cordova-build',
|
||||
limitedBuilds: true,
|
||||
packageNameWarningType: 'mobile',
|
||||
|
||||
getInitialExportState: () => ({
|
||||
|
@@ -55,6 +55,7 @@ export const browserOnlineElectronExportPipeline: ExportPipeline<
|
||||
> = {
|
||||
name: 'browser-online-electron',
|
||||
onlineBuildType: 'electron-build',
|
||||
limitedBuilds: true,
|
||||
packageNameWarningType: 'desktop',
|
||||
|
||||
getInitialExportState: () => ({
|
||||
|
@@ -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>
|
||||
|
@@ -52,6 +52,7 @@ const setupWindowClosedPolling = () => {
|
||||
);
|
||||
if (!Object.keys(previewWindowAndTargetIds).length) {
|
||||
clearInterval(windowClosedPollingIntervalId);
|
||||
windowClosedPollingIntervalId = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -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 (& 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 & 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,
|
||||
},
|
||||
];
|
||||
|
||||
|
@@ -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>
|
||||
);
|
||||
}
|
||||
}
|
128
newIDE/app/src/Export/ExportDialog/ExportHome.js
Normal file
128
newIDE/app/src/Export/ExportDialog/ExportHome.js
Normal 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;
|
24
newIDE/app/src/Export/ExportDialog/ExportHomeSeparator.js
Normal file
24
newIDE/app/src/Export/ExportDialog/ExportHomeSeparator.js
Normal 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>
|
||||
)
|
||||
);
|
||||
};
|
@@ -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>
|
||||
)}
|
196
newIDE/app/src/Export/ExportDialog/index.js
Normal file
196
newIDE/app/src/Export/ExportDialog/index.js
Normal 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;
|
@@ -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>,
|
||||
|
@@ -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 & Android (manual)</Trans>,
|
||||
helpPage: '/publishing/android_and_ios_with_cordova',
|
||||
};
|
||||
|
@@ -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',
|
||||
};
|
||||
|
@@ -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',
|
||||
};
|
||||
|
@@ -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',
|
||||
};
|
||||
|
@@ -157,3 +157,10 @@ export const SetupExportHeader = ({
|
||||
</Column>
|
||||
);
|
||||
};
|
||||
|
||||
export const onlineCordovaExporter = {
|
||||
key: 'onlinecordovaexport',
|
||||
tabName: <Trans>Mobile</Trans>,
|
||||
name: <Trans>Android (& iOS coming soon)</Trans>,
|
||||
helpPage: '/publishing/android_and_ios',
|
||||
};
|
||||
|
@@ -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',
|
||||
};
|
||||
|
@@ -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',
|
||||
};
|
||||
|
@@ -49,6 +49,7 @@ export const localOnlineCordovaExportPipeline: ExportPipeline<
|
||||
> = {
|
||||
name: 'local-online-cordova',
|
||||
onlineBuildType: 'cordova-build',
|
||||
limitedBuilds: true,
|
||||
packageNameWarningType: 'mobile',
|
||||
|
||||
getInitialExportState: () => ({
|
||||
|
@@ -49,6 +49,7 @@ export const localOnlineElectronExportPipeline: ExportPipeline<
|
||||
> = {
|
||||
name: 'local-online-electron',
|
||||
onlineBuildType: 'electron-build',
|
||||
limitedBuilds: true,
|
||||
packageNameWarningType: 'desktop',
|
||||
|
||||
getInitialExportState: () => ({
|
||||
|
@@ -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>
|
||||
|
@@ -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 (& 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 & 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,
|
||||
},
|
||||
];
|
||||
|
@@ -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={() => {
|
||||
|
@@ -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,
|
||||
|
146
newIDE/app/src/GamesShowcase/ShowcasedGameButtons.js
Normal file
146
newIDE/app/src/GamesShowcase/ShowcasedGameButtons.js
Normal 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;
|
62
newIDE/app/src/GamesShowcase/ShowcasedGameDialog.js
Normal file
62
newIDE/app/src/GamesShowcase/ShowcasedGameDialog.js
Normal 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;
|
@@ -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>
|
||||
|
31
newIDE/app/src/GamesShowcase/ShowcasedGameTitle.js
Normal file
31
newIDE/app/src/GamesShowcase/ShowcasedGameTitle.js
Normal 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;
|
@@ -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);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
@@ -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.`,
|
||||
});
|
||||
|
@@ -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',
|
||||
|
@@ -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}
|
||||
|
@@ -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> {
|
||||
|
@@ -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 = {|
|
||||
|
@@ -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)}
|
||||
/>
|
||||
|
@@ -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;
|
565
newIDE/app/src/MainFrame/EditorContainers/HomePage/index.js
Normal file
565
newIDE/app/src/MainFrame/EditorContainers/HomePage/index.js
Normal 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}
|
||||
/>
|
||||
);
|
@@ -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;
|
@@ -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}
|
||||
/>
|
||||
);
|
@@ -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);
|
||||
|
@@ -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, {
|
||||
|
@@ -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,
|
||||
|
@@ -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={
|
||||
|
@@ -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: () => {},
|
||||
|
@@ -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 & 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
|
||||
|
@@ -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);
|
||||
|
@@ -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>
|
||||
|
211
newIDE/app/src/MainFrame/Toolbar/PreviewAndPublishButtons.js
Normal file
211
newIDE/app/src/MainFrame/Toolbar/PreviewAndPublishButtons.js
Normal 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>
|
||||
);
|
||||
}
|
@@ -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>
|
||||
);
|
||||
}
|
@@ -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;
|
||||
);
|
||||
|
@@ -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();
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
@@ -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;
|
||||
|
@@ -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
|
||||
|
@@ -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>
|
||||
),
|
||||
},
|
||||
};
|
||||
|
@@ -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>
|
||||
|
@@ -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>
|
||||
|
@@ -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>
|
||||
);
|
||||
}
|
||||
}
|
@@ -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>
|
||||
|
@@ -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>
|
||||
);
|
||||
|
@@ -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;
|
@@ -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>
|
||||
);
|
||||
}
|
||||
}
|
@@ -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
Reference in New Issue
Block a user