Compare commits
55 Commits
v5.0.0-bet
...
v5.0.0-bet
Author | SHA1 | Date | |
---|---|---|---|
![]() |
5e3e0db9f3 | ||
![]() |
bfee61e9e8 | ||
![]() |
097dd779f6 | ||
![]() |
52453b7c58 | ||
![]() |
dd2ac68dab | ||
![]() |
6ea50f8114 | ||
![]() |
ec05ec6ea1 | ||
![]() |
3e7c5394ce | ||
![]() |
55aae059ff | ||
![]() |
61a20f1af8 | ||
![]() |
60170609e9 | ||
![]() |
0c55b3b8d4 | ||
![]() |
6b16934184 | ||
![]() |
c15acdde83 | ||
![]() |
209dafc269 | ||
![]() |
757c9502ef | ||
![]() |
be8ab96c4c | ||
![]() |
1600091249 | ||
![]() |
45b6caa06e | ||
![]() |
59754d1c27 | ||
![]() |
656881a11a | ||
![]() |
5e91bdf811 | ||
![]() |
e6d0f6ed4c | ||
![]() |
864acd3988 | ||
![]() |
630ece0f7e | ||
![]() |
3b102a74b7 | ||
![]() |
5096ecee24 | ||
![]() |
29f1d873a6 | ||
![]() |
088e3acae2 | ||
![]() |
0fbe7a62e8 | ||
![]() |
9e4849f218 | ||
![]() |
db536894a4 | ||
![]() |
a63c44b5ca | ||
![]() |
8756276793 | ||
![]() |
878f64d024 | ||
![]() |
bd09c58439 | ||
![]() |
c645d4a2d6 | ||
![]() |
45e03d61a0 | ||
![]() |
d924cd90bd | ||
![]() |
455fe8193a | ||
![]() |
1eb5113283 | ||
![]() |
3687c24e71 | ||
![]() |
8d75a8373d | ||
![]() |
3fed377c6c | ||
![]() |
957ab3f53b | ||
![]() |
e7e338805b | ||
![]() |
83d0c4cc65 | ||
![]() |
13461429f2 | ||
![]() |
b0fd36daff | ||
![]() |
2e84b2e648 | ||
![]() |
2b4d263410 | ||
![]() |
64343d7043 | ||
![]() |
3e9c4d594c | ||
![]() |
657ca85324 | ||
![]() |
6ac9c0a0ee |
@@ -48,7 +48,7 @@ before_install:
|
||||
#use SFML.
|
||||
- "export DISPLAY=:99.0"
|
||||
# This workaround is required to avoid libstdc++ errors (Emscripten requires a recent version of libstdc++)
|
||||
- wget -q -O libstdc++6 http://security.ubuntu.com/ubuntu/pool/main/g/gcc-5/libstdc++6_5.4.0-6ubuntu1~16.04.10_amd64.deb
|
||||
- wget -q -O libstdc++6 http://security.ubuntu.com/ubuntu/pool/main/g/gcc-5/libstdc++6_5.4.0-6ubuntu1~16.04.12_amd64.deb
|
||||
- sudo dpkg --force-all -i libstdc++6
|
||||
|
||||
install:
|
||||
|
@@ -115,7 +115,7 @@ void GD_CORE_API BuiltinExtensionsImplementer::ImplementsFileExtension(
|
||||
.AddAction(
|
||||
"LireFichierTxt",
|
||||
_("Read a text"),
|
||||
_("Read the text saved in the specified element and store it in a scene"
|
||||
_("Read the text saved in the specified element and store it in a scene "
|
||||
"variable.\nSpecify the structure leading to the element using / "
|
||||
"(example : Root/Level/Current)\nSpaces are forbidden in element "
|
||||
"names."),
|
||||
|
@@ -21,6 +21,16 @@ BuiltinExtensionsImplementer::ImplementsMathematicalToolsExtension(
|
||||
|
||||
#if defined(GD_IDE_ONLY)
|
||||
|
||||
extension
|
||||
.AddExpression("clamp",
|
||||
_("Clamp (restrict a value to a given range)"),
|
||||
_("Restrict a value to a given range"),
|
||||
_("Mathematical tools"),
|
||||
"res/mathfunction.png")
|
||||
.AddParameter("expression", _("Value"))
|
||||
.AddParameter("expression", _("Min"))
|
||||
.AddParameter("expression", _("Max"));
|
||||
|
||||
extension
|
||||
.AddExpression("AngleDifference",
|
||||
_("Difference between two angles"),
|
||||
|
@@ -31,7 +31,7 @@ void GD_CORE_API BuiltinExtensionsImplementer::ImplementsSpriteExtension(
|
||||
|
||||
#if defined(GD_IDE_ONLY)
|
||||
obj.AddAction("Opacity",
|
||||
_("Change Sprite opacity"),
|
||||
_("Change sprite opacity"),
|
||||
_("Change the opacity of a Sprite. 0 is fully transparent, 255 "
|
||||
"is opaque (default)."),
|
||||
_("Do _PARAM1__PARAM2_ to the opacity of _PARAM0_"),
|
||||
|
@@ -100,13 +100,6 @@ class GD_CORE_API AbstractFileSystem {
|
||||
virtual bool CopyFile(const gd::String& file,
|
||||
const gd::String& destination) = 0;
|
||||
|
||||
/**
|
||||
* \brief Copy a whole directory
|
||||
* \return true if the operation succeeded.
|
||||
*/
|
||||
virtual bool CopyDir(const gd::String& source,
|
||||
const gd::String& destination) = 0;
|
||||
|
||||
/**
|
||||
* \brief Write the content of a string to a file.
|
||||
* \return true if the operation succeeded.
|
||||
|
@@ -44,10 +44,6 @@ class MockFileSystem : public gd::AbstractFileSystem {
|
||||
virtual bool CopyFile(const gd::String& file, const gd::String& destination) {
|
||||
return true;
|
||||
}
|
||||
virtual bool CopyDir(const gd::String& source,
|
||||
const gd::String& destination) {
|
||||
return true;
|
||||
}
|
||||
virtual bool ClearDir(const gd::String& directory) { return true; }
|
||||
virtual bool WriteToFile(const gd::String& file, const gd::String& content) {
|
||||
return true;
|
||||
|
@@ -28,7 +28,7 @@ module.exports = {
|
||||
extension
|
||||
.addAction(
|
||||
'LoadDialogueFromSceneVariable',
|
||||
_('Load dialogue Tree from a Scene Variable'),
|
||||
_('Load dialogue Tree from a scene variable'),
|
||||
_(
|
||||
'Load a dialogue data object - Yarn json format, stored in a scene variable. Use this command to load all the Dialogue data at the beginning of the game.'
|
||||
),
|
||||
|
@@ -332,8 +332,8 @@ void DeclarePlatformBehaviorExtension(gd::PlatformExtension& extension) {
|
||||
"this can be useful to allow double jump for example."),
|
||||
_("Allow _PARAM0_ to jump again"),
|
||||
_("Options"),
|
||||
"res/conditions/keyboard24.png",
|
||||
"res/conditions/keyboard.png")
|
||||
"CppPlatform/Extensions/platformerobjecticon24.png",
|
||||
"CppPlatform/Extensions/platformerobjecticon16.png")
|
||||
.AddParameter("object", _("Object"))
|
||||
.AddParameter("behavior", _("Behavior"), "PlatformerObjectBehavior")
|
||||
.MarkAsSimple()
|
||||
|
@@ -260,7 +260,7 @@ void DeclareTextObjectExtension(gd::PlatformExtension& extension) {
|
||||
.AddParameter("yesorno", _("Show the shadow"));
|
||||
|
||||
obj.AddAction("Opacity",
|
||||
_("Change Text Opacity"),
|
||||
_("Change text opacity"),
|
||||
_("Change the opacity of a Text. 0 is fully transparent, 255 "
|
||||
"is opaque (default)."),
|
||||
_("Do _PARAM1__PARAM2_ to the opacity of _PARAM0_"),
|
||||
|
@@ -182,19 +182,13 @@ gdjs.VideoRuntimeObject.prototype._normalize = function(val, min, max) {
|
||||
return (val - min) / (max - min);
|
||||
};
|
||||
|
||||
/**
|
||||
* Restrict the value in the given interval
|
||||
*/
|
||||
gdjs.VideoRuntimeObject.prototype._clamp = function(val, min, max) {
|
||||
return val <= min ? min : val >= max ? max : val;
|
||||
};
|
||||
|
||||
/**
|
||||
* Set the volume of the video object.
|
||||
* @param {number} volume The new volume.
|
||||
*/
|
||||
gdjs.VideoRuntimeObject.prototype.setVolume = function(volume) {
|
||||
this._volume = this._clamp(this._normalize(volume, 0, 100), 0, 1) * 100;
|
||||
this._volume =
|
||||
gdjs.evtTools.common.clamp(this._normalize(volume, 0, 100), 0, 1) * 100;
|
||||
this._renderer.updateVolume();
|
||||
};
|
||||
|
||||
@@ -260,7 +254,7 @@ gdjs.VideoRuntimeObject.prototype.getCurrentTime = function() {
|
||||
* @param {number} playbackSpeed The new playback speed.
|
||||
*/
|
||||
gdjs.VideoRuntimeObject.prototype.setPlaybackSpeed = function(playbackSpeed) {
|
||||
this._playbackSpeed = this._clamp(playbackSpeed, 0.5, 2);
|
||||
this._playbackSpeed = gdjs.evtTools.common.clamp(playbackSpeed, 0.5, 2);
|
||||
this._renderer.setPlaybackSpeed(this._playbackSpeed);
|
||||
};
|
||||
|
||||
|
@@ -10,6 +10,10 @@
|
||||
namespace GDpriv {
|
||||
namespace MathematicalTools {
|
||||
|
||||
double GD_API clamp(double expression, double min, double max) {
|
||||
return std::min(std::max(expression, min), max);
|
||||
}
|
||||
|
||||
double GD_API Minimal(double expression1, double expression2) {
|
||||
return std::min(expression1, expression2);
|
||||
}
|
||||
|
@@ -13,6 +13,7 @@ namespace GDpriv {
|
||||
*/
|
||||
namespace MathematicalTools {
|
||||
|
||||
double GD_API clamp(double expression, double min, double max);
|
||||
double GD_API Minimal(double expression1, double expression2);
|
||||
double GD_API Maximal(double expression1, double expression2);
|
||||
double GD_API abs(double expression);
|
||||
|
@@ -15,6 +15,9 @@ MathematicalToolsExtension::MathematicalToolsExtension() {
|
||||
|
||||
#if defined(GD_IDE_ONLY)
|
||||
|
||||
GetAllExpressions()["clamp"]
|
||||
.SetFunctionName("GDpriv::MathematicalTools::clamp")
|
||||
.SetIncludeFile("GDCpp/Extensions/Builtin/MathematicalTools.h");
|
||||
GetAllExpressions()["AngleDifference"]
|
||||
.SetFunctionName("GDpriv::MathematicalTools::angleDifference")
|
||||
.SetIncludeFile("GDCpp/Extensions/Builtin/MathematicalTools.h");
|
||||
|
@@ -17,6 +17,7 @@ namespace gdjs {
|
||||
MathematicalToolsExtension::MathematicalToolsExtension() {
|
||||
gd::BuiltinExtensionsImplementer::ImplementsMathematicalToolsExtension(*this);
|
||||
|
||||
GetAllExpressions()["clamp"].SetFunctionName("gdjs.evtTools.common.clamp");
|
||||
GetAllExpressions()["cos"].SetFunctionName("Math.cos");
|
||||
GetAllExpressions()["sin"].SetFunctionName("Math.sin");
|
||||
GetAllExpressions()["tan"].SetFunctionName("Math.tan");
|
||||
|
@@ -501,7 +501,7 @@ bool ExporterHelper::CompleteIndexFile(
|
||||
fs.MakeRelative(scriptSrc, exportDir);
|
||||
}
|
||||
|
||||
codeFilesIncludes += "\t<script src=\"" + scriptSrc + "\"></script>\n";
|
||||
codeFilesIncludes += "\t<script src=\"" + scriptSrc + "\" crossorigin=\"anonymous\"></script>\n";
|
||||
}
|
||||
|
||||
str = str.FindAndReplace("/* GDJS_CUSTOM_STYLE */", customCss)
|
||||
|
@@ -12,14 +12,12 @@
|
||||
*/
|
||||
gdjs.evtTools.common = gdjs.evtTools.common || {};
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Get the value of a variable. Equivalent of variable.getAsNumber().
|
||||
* @private
|
||||
*/
|
||||
gdjs.evtTools.common.getVariableNumber = function(variable) {
|
||||
return variable.getAsNumber();
|
||||
return variable.getAsNumber();
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -27,50 +25,59 @@ gdjs.evtTools.common.getVariableNumber = function(variable) {
|
||||
* @private
|
||||
*/
|
||||
gdjs.evtTools.common.getVariableString = function(variable) {
|
||||
return variable.getAsString();
|
||||
return variable.getAsString();
|
||||
};
|
||||
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
gdjs.evtTools.common.sceneVariableExists = function(runtimeScene, variableName) {
|
||||
return runtimeScene.getVariables().has(variableName);
|
||||
gdjs.evtTools.common.sceneVariableExists = function(
|
||||
runtimeScene,
|
||||
variableName
|
||||
) {
|
||||
return runtimeScene.getVariables().has(variableName);
|
||||
};
|
||||
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
gdjs.evtTools.common.globalVariableExists = function(runtimeScene, variableName) {
|
||||
return runtimeScene.getGame().getVariables().has(variableName);
|
||||
gdjs.evtTools.common.globalVariableExists = function(
|
||||
runtimeScene,
|
||||
variableName
|
||||
) {
|
||||
return runtimeScene
|
||||
.getGame()
|
||||
.getVariables()
|
||||
.has(variableName);
|
||||
};
|
||||
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
gdjs.evtTools.common.variableChildExists = function(variable, childName) {
|
||||
return variable.hasChild(childName);
|
||||
return variable.hasChild(childName);
|
||||
};
|
||||
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
gdjs.evtTools.common.variableRemoveChild = function(variable, childName) {
|
||||
return variable.removeChild(childName);
|
||||
return variable.removeChild(childName);
|
||||
};
|
||||
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
gdjs.evtTools.common.variableClearChildren = function(variable) {
|
||||
variable.clearChildren();
|
||||
variable.clearChildren();
|
||||
};
|
||||
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
gdjs.evtTools.common.getVariableChildCount = function(variable) {
|
||||
if (variable.isStructure() == false) return 0;
|
||||
return Object.keys(variable.getAllChildren()).length;
|
||||
if (variable.isStructure() == false) return 0;
|
||||
return Object.keys(variable.getAllChildren()).length;
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -78,7 +85,7 @@ gdjs.evtTools.common.getVariableChildCount = function(variable) {
|
||||
* @private
|
||||
*/
|
||||
gdjs.evtTools.common.toNumber = function(str) {
|
||||
return parseFloat(str);
|
||||
return parseFloat(str);
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -86,9 +93,9 @@ gdjs.evtTools.common.toNumber = function(str) {
|
||||
* @private
|
||||
*/
|
||||
gdjs.evtTools.common.toString = function(num) {
|
||||
//Using String literal is fastest than using toString according to
|
||||
//http://jsperf.com/number-to-string/2 and http://jsben.ch/#/ghQYR
|
||||
return "" + num;
|
||||
//Using String literal is fastest than using toString according to
|
||||
//http://jsperf.com/number-to-string/2 and http://jsben.ch/#/ghQYR
|
||||
return '' + num;
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -96,85 +103,94 @@ gdjs.evtTools.common.toString = function(num) {
|
||||
* @private
|
||||
*/
|
||||
gdjs.evtTools.common.logicalNegation = function(bool) {
|
||||
return !bool;
|
||||
return !bool;
|
||||
};
|
||||
|
||||
gdjs.evtTools.common.clamp = function(x, min, max) {
|
||||
return Math.min(Math.max(x, min), max);
|
||||
};
|
||||
|
||||
gdjs.evtTools.common.acosh = function(arg) {
|
||||
// http://kevin.vanzonneveld.net
|
||||
// + original by: Onno Marsman
|
||||
return Math.log(arg + Math.sqrt(arg * arg - 1));
|
||||
// http://kevin.vanzonneveld.net
|
||||
// + original by: Onno Marsman
|
||||
return Math.log(arg + Math.sqrt(arg * arg - 1));
|
||||
};
|
||||
|
||||
gdjs.evtTools.common.asinh = function(arg) {
|
||||
// http://kevin.vanzonneveld.net
|
||||
// + original by: Onno Marsman
|
||||
return Math.log(arg + Math.sqrt(arg * arg + 1));
|
||||
// http://kevin.vanzonneveld.net
|
||||
// + original by: Onno Marsman
|
||||
return Math.log(arg + Math.sqrt(arg * arg + 1));
|
||||
};
|
||||
|
||||
gdjs.evtTools.common.atanh = function(arg) {
|
||||
// http://kevin.vanzonneveld.net
|
||||
// + original by: Onno Marsman
|
||||
return 0.5 * Math.log((1 + arg) / (1 - arg));
|
||||
// http://kevin.vanzonneveld.net
|
||||
// + original by: Onno Marsman
|
||||
return 0.5 * Math.log((1 + arg) / (1 - arg));
|
||||
};
|
||||
|
||||
gdjs.evtTools.common.cosh = function(arg) {
|
||||
return (Math.exp(arg) + Math.exp(-arg)) / 2;
|
||||
return (Math.exp(arg) + Math.exp(-arg)) / 2;
|
||||
};
|
||||
|
||||
gdjs.evtTools.common.sinh = function(arg) {
|
||||
return (Math.exp(arg) - Math.exp(-arg)) / 2;
|
||||
return (Math.exp(arg) - Math.exp(-arg)) / 2;
|
||||
};
|
||||
|
||||
gdjs.evtTools.common.tanh = function(arg) {
|
||||
return (Math.exp(arg) - Math.exp(-arg)) / (Math.exp(arg) + Math.exp(-arg));
|
||||
return (Math.exp(arg) - Math.exp(-arg)) / (Math.exp(arg) + Math.exp(-arg));
|
||||
};
|
||||
|
||||
gdjs.evtTools.common.cot = function(arg) {
|
||||
return 1/Math.tan(arg);
|
||||
return 1 / Math.tan(arg);
|
||||
};
|
||||
|
||||
gdjs.evtTools.common.csc = function(arg) {
|
||||
return 1/Math.sin(arg);
|
||||
return 1 / Math.sin(arg);
|
||||
};
|
||||
|
||||
gdjs.evtTools.common.sec = function(arg) {
|
||||
return 1/Math.cos(arg);
|
||||
return 1 / Math.cos(arg);
|
||||
};
|
||||
|
||||
gdjs.evtTools.common.log10 = function(arg) {
|
||||
return Math.log(arg) / Math.LN10;
|
||||
return Math.log(arg) / Math.LN10;
|
||||
};
|
||||
|
||||
gdjs.evtTools.common.log2 = function(arg) {
|
||||
return Math.log(arg) / Math.LN2;
|
||||
return Math.log(arg) / Math.LN2;
|
||||
};
|
||||
|
||||
gdjs.evtTools.common.sign = function(arg) {
|
||||
if ( arg === 0 ) return 0;
|
||||
if (arg === 0) return 0;
|
||||
|
||||
return (arg > 0 ? +1 : -1);
|
||||
return arg > 0 ? +1 : -1;
|
||||
};
|
||||
|
||||
gdjs.evtTools.common.cbrt = function(x) {
|
||||
return Math.pow(x, 1/3);
|
||||
return Math.pow(x, 1 / 3);
|
||||
};
|
||||
|
||||
gdjs.evtTools.common.nthroot = function(x, n) {
|
||||
return Math.pow(x, 1/n);
|
||||
return Math.pow(x, 1 / n);
|
||||
};
|
||||
|
||||
gdjs.evtTools.common.mod = function(x, y) {
|
||||
return x - y * Math.floor(x / y);
|
||||
return x - y * Math.floor(x / y);
|
||||
};
|
||||
|
||||
gdjs.evtTools.common.angleDifference = function(angle1, angle2) {
|
||||
return gdjs.evtTools.common.mod(gdjs.evtTools.common.mod(angle1 - angle2, 360.0) + 180.0, 360.0) - 180.0;
|
||||
return (
|
||||
gdjs.evtTools.common.mod(
|
||||
gdjs.evtTools.common.mod(angle1 - angle2, 360.0) + 180.0,
|
||||
360.0
|
||||
) - 180.0
|
||||
);
|
||||
};
|
||||
|
||||
gdjs.evtTools.common.lerp = function(a, b, x) {
|
||||
return a+(b-a)*x;
|
||||
return a + (b - a) * x;
|
||||
};
|
||||
|
||||
gdjs.evtTools.common.trunc = function(x) {
|
||||
return x|0;
|
||||
return x | 0;
|
||||
};
|
||||
|
@@ -265,20 +265,6 @@ class AbstractFileSystemJS : public AbstractFileSystem {
|
||||
directory.c_str());
|
||||
}
|
||||
|
||||
virtual bool CopyDir(const gd::String &source,
|
||||
const gd::String &destination) {
|
||||
return (bool)EM_ASM_INT(
|
||||
{
|
||||
var self = Module['getCache'](Module['AbstractFileSystemJS'])[$0];
|
||||
if (!self.hasOwnProperty('copyDir'))
|
||||
throw 'a JSImplementation must implement all functions, you forgot AbstractFileSystemJS::copyDir.';
|
||||
return self.copyDir(Pointer_stringify($1), Pointer_stringify($2));
|
||||
},
|
||||
(int)this,
|
||||
source.c_str(),
|
||||
destination.c_str());
|
||||
}
|
||||
|
||||
virtual bool WriteToFile(const gd::String &file, const gd::String &content) {
|
||||
return (bool)EM_ASM_INT(
|
||||
{
|
||||
|
@@ -3,3 +3,5 @@
|
||||
|
||||
REACT_APP_PREVIEW_S3_ACCESS_KEY_ID=
|
||||
REACT_APP_PREVIEW_S3_SECRET_ACCESS_KEY=
|
||||
REACT_APP_UPLOAD_S3_ACCESS_KEY_ID=
|
||||
REACT_APP_UPLOAD_S3_SECRET_ACCESS_KEY=
|
||||
|
6
newIDE/app/.gitignore
vendored
@@ -1,4 +1,5 @@
|
||||
# See http://help.github.com/ignore-files/ for more about ignoring files.
|
||||
# Generated files
|
||||
src/Version/VersionMetadata.js
|
||||
|
||||
# dependencies
|
||||
node_modules
|
||||
@@ -25,6 +26,9 @@ public/external/piskel/piskel-editor.zip
|
||||
public/external/monaco-editor-min/*
|
||||
public/external/jfxr/jfxr-editor/*
|
||||
public/external/yarn/yarn-editor/*
|
||||
public/external/zip.js/*
|
||||
public/external/zlib-asm/*
|
||||
public/external/__MACOSX/
|
||||
|
||||
# Resources
|
||||
resources/GDJS
|
||||
|
1
newIDE/app/flow-typed/libGD.js
vendored
@@ -68,6 +68,7 @@ declare type gdPropertyDescriptor = gdEmscriptenObject;
|
||||
declare type gdNamedPropertyDescriptor = gdEmscriptenObject;
|
||||
declare type gdNamedPropertyDescriptorsList = gdEmscriptenObject;
|
||||
|
||||
declare type gdjsExporter = gdEmscriptenObject;
|
||||
|
||||
declare type gdEventsContext = gdEmscriptenObject;
|
||||
|
||||
|
104
newIDE/app/flow-typed/zip.js
vendored
Normal file
@@ -0,0 +1,104 @@
|
||||
// @flow
|
||||
|
||||
// Flow type definitions for zip.js 2.x
|
||||
// Adapted from https://github.com/DefinitelyTyped/DefinitelyTyped/blob/master/types/zip.js/index.d.ts
|
||||
// Project: https://github.com/gildas-lormeau/zip.js
|
||||
// Definitions by: Louis Grignon <https://github.com/lgrignon>
|
||||
// Definitions: https://github.com/DefinitelyTyped/DefinitelyTyped
|
||||
|
||||
interface FileEntry {}
|
||||
|
||||
declare export class ZipJs$Reader {
|
||||
size: number;
|
||||
init(callback: () => void, onerror: (error: any) => void): void;
|
||||
readUint8Array(
|
||||
index: number,
|
||||
length: number,
|
||||
callback: (result: Uint8Array) => void,
|
||||
onerror?: (error: any) => void
|
||||
): void;
|
||||
}
|
||||
|
||||
declare export class ZipJs$ZipReader {
|
||||
getEntries(callback: (entries: ZipJs$Entry[]) => void): void;
|
||||
close(callback?: () => void): void;
|
||||
}
|
||||
|
||||
declare export class ZipJs$Writer {
|
||||
init(callback: () => void, onerror?: (error: any) => void): void;
|
||||
writeUint8Array(
|
||||
array: Uint8Array,
|
||||
callback: () => void,
|
||||
onerror?: (error: any) => void
|
||||
): void;
|
||||
getData(callback: (data: any) => void, onerror?: (error: any) => void): void;
|
||||
}
|
||||
|
||||
export interface ZipJs$Entry {
|
||||
filename: string;
|
||||
directory: boolean;
|
||||
compressedSize: number;
|
||||
uncompressedSize: number;
|
||||
lastModDate: Date;
|
||||
lastModDateRaw: number;
|
||||
comment: string;
|
||||
crc32: number;
|
||||
|
||||
getData(
|
||||
writer: ZipJs$Writer,
|
||||
onend: (result: any) => void,
|
||||
onprogress?: (progress: number, total: number) => void,
|
||||
checkCrc32?: boolean
|
||||
): void;
|
||||
}
|
||||
|
||||
export interface ZipJs$WriteOptions {
|
||||
directory?: boolean;
|
||||
level?: number;
|
||||
comment?: string;
|
||||
lastModDate?: Date;
|
||||
version?: number;
|
||||
}
|
||||
|
||||
declare export class ZipJs$ZipWriter {
|
||||
add(
|
||||
name: string,
|
||||
reader: ZipJs$Reader,
|
||||
onend: () => void,
|
||||
onprogress?: (progress: number, total: number) => void,
|
||||
options?: ZipJs$WriteOptions
|
||||
): void;
|
||||
close(callback: (result: any) => void): void;
|
||||
}
|
||||
|
||||
declare type ZipJs = {|
|
||||
useWebWorkers: boolean,
|
||||
workerScriptsPath: string,
|
||||
workerScripts: {
|
||||
deflater?: string[],
|
||||
inflater?: string[],
|
||||
},
|
||||
createReader: (
|
||||
reader: ZipJs$Reader,
|
||||
callback: (zipReader: ZipJs$ZipReader) => void,
|
||||
onerror?: (error: any) => void
|
||||
) => void,
|
||||
|
||||
createWriter: (
|
||||
writer: ZipJs$Writer,
|
||||
callback: (zipWriter: ZipJs$ZipWriter) => void,
|
||||
onerror?: (error: any) => void,
|
||||
dontDeflate?: boolean
|
||||
) => void,
|
||||
TextReader: (text: string) => ZipJs$Reader,
|
||||
BlobReader: (blob: Blob) => ZipJs$Reader,
|
||||
Data64URIReader: (dataURI: string) => ZipJs$Reader,
|
||||
HttpReader: (url: string) => ZipJs$Reader,
|
||||
ZipReader: () => ZipJs$ZipReader,
|
||||
|
||||
TextWriter: (encoding: string) => ZipJs$Writer,
|
||||
BlobWriter: (contentType: string) => ZipJs$Writer,
|
||||
FileWriter: (fileEntry: FileEntry) => ZipJs$Writer,
|
||||
Data64URIWriter: (mimeString?: string) => ZipJs$Writer,
|
||||
ZipWriter: () => ZipJs$ZipWriter,
|
||||
|};
|
429
newIDE/app/package-lock.json
generated
@@ -1097,6 +1097,12 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"@blueprintjs/core": {
|
||||
"version": "file:src/Utils/BlueprintJsPlaceholder"
|
||||
},
|
||||
"@blueprintjs/icons": {
|
||||
"version": "file:src/Utils/BlueprintJsPlaceholder"
|
||||
},
|
||||
"@csstools/convert-colors": {
|
||||
"version": "1.4.0",
|
||||
"resolved": "https://registry.npmjs.org/@csstools/convert-colors/-/convert-colors-1.4.0.tgz",
|
||||
@@ -1423,6 +1429,45 @@
|
||||
"protobufjs": "^6.8.6"
|
||||
}
|
||||
},
|
||||
"@hapi/address": {
|
||||
"version": "2.1.2",
|
||||
"resolved": "https://registry.npmjs.org/@hapi/address/-/address-2.1.2.tgz",
|
||||
"integrity": "sha512-O4QDrx+JoGKZc6aN64L04vqa7e41tIiLU+OvKdcYaEMP97UttL0f9GIi9/0A4WAMx0uBd6SidDIhktZhgOcN8Q==",
|
||||
"dev": true
|
||||
},
|
||||
"@hapi/bourne": {
|
||||
"version": "1.3.2",
|
||||
"resolved": "https://registry.npmjs.org/@hapi/bourne/-/bourne-1.3.2.tgz",
|
||||
"integrity": "sha512-1dVNHT76Uu5N3eJNTYcvxee+jzX4Z9lfciqRRHCU27ihbUcYi+iSc2iml5Ke1LXe1SyJCLA0+14Jh4tXJgOppA==",
|
||||
"dev": true
|
||||
},
|
||||
"@hapi/hoek": {
|
||||
"version": "8.5.0",
|
||||
"resolved": "https://registry.npmjs.org/@hapi/hoek/-/hoek-8.5.0.tgz",
|
||||
"integrity": "sha512-7XYT10CZfPsH7j9F1Jmg1+d0ezOux2oM2GfArAzLwWe4mE2Dr3hVjsAL6+TFY49RRJlCdJDMw3nJsLFroTc8Kw==",
|
||||
"dev": true
|
||||
},
|
||||
"@hapi/joi": {
|
||||
"version": "15.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@hapi/joi/-/joi-15.1.1.tgz",
|
||||
"integrity": "sha512-entf8ZMOK8sc+8YfeOlM8pCfg3b5+WZIKBfUaaJT8UsjAAPjartzxIYm3TIbjvA4u+u++KbcXD38k682nVHDAQ==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@hapi/address": "2.x.x",
|
||||
"@hapi/bourne": "1.x.x",
|
||||
"@hapi/hoek": "8.x.x",
|
||||
"@hapi/topo": "3.x.x"
|
||||
}
|
||||
},
|
||||
"@hapi/topo": {
|
||||
"version": "3.1.6",
|
||||
"resolved": "https://registry.npmjs.org/@hapi/topo/-/topo-3.1.6.tgz",
|
||||
"integrity": "sha512-tAag0jEcjwH+P2quUfipd7liWCNX2F8NvYjQp2wtInsZxnMlypdw0FtAOLxtvvkO+GSRRbmNi8m/5y42PQJYCQ==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@hapi/hoek": "^8.3.0"
|
||||
}
|
||||
},
|
||||
"@jest/types": {
|
||||
"version": "24.9.0",
|
||||
"resolved": "https://registry.npmjs.org/@jest/types/-/types-24.9.0.tgz",
|
||||
@@ -3425,12 +3470,35 @@
|
||||
"dev": true
|
||||
},
|
||||
"axios": {
|
||||
"version": "0.16.2",
|
||||
"resolved": "https://registry.npmjs.org/axios/-/axios-0.16.2.tgz",
|
||||
"integrity": "sha1-uk+S8XFn37q0CYN4VFS5rBScPG0=",
|
||||
"version": "0.18.1",
|
||||
"resolved": "https://registry.npmjs.org/axios/-/axios-0.18.1.tgz",
|
||||
"integrity": "sha512-0BfJq4NSfQXd+SkFdrvFbG7addhYSBA2mQwISr46pD6E5iqkWg02RAs8vyTT/j0RTnoYmeXauBuSv1qKwR179g==",
|
||||
"requires": {
|
||||
"follow-redirects": "^1.2.3",
|
||||
"is-buffer": "^1.1.5"
|
||||
"follow-redirects": "1.5.10",
|
||||
"is-buffer": "^2.0.2"
|
||||
},
|
||||
"dependencies": {
|
||||
"debug": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz",
|
||||
"integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==",
|
||||
"requires": {
|
||||
"ms": "2.0.0"
|
||||
}
|
||||
},
|
||||
"follow-redirects": {
|
||||
"version": "1.5.10",
|
||||
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.5.10.tgz",
|
||||
"integrity": "sha512-0V5l4Cizzvqt5D44aTXbFZz+FtyXV1vrDN6qrelxtfYQKW0KO0W2T/hkE8xvGa/540LkZlkaUjO4ailYTFtHVQ==",
|
||||
"requires": {
|
||||
"debug": "=3.1.0"
|
||||
}
|
||||
},
|
||||
"is-buffer": {
|
||||
"version": "2.0.4",
|
||||
"resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-2.0.4.tgz",
|
||||
"integrity": "sha512-Kq1rokWXOPXWuaMAqZiJW4XxsmD9zGx9q4aePabbn3qCRGedtH7Cm+zV8WETitMfu1wdh+Rvd6w5egwSngUX2A=="
|
||||
}
|
||||
}
|
||||
},
|
||||
"axobject-query": {
|
||||
@@ -8438,6 +8506,7 @@
|
||||
"version": "1.9.0",
|
||||
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.9.0.tgz",
|
||||
"integrity": "sha512-CRcPzsSIbXyVDl0QI01muNDu69S8trU4jArW9LpOt2WtC6LyUJetcIrmfHsRBx7/Jb6GHJUiuqyYxPooFfNt6A==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"debug": "^3.0.0"
|
||||
},
|
||||
@@ -8446,6 +8515,7 @@
|
||||
"version": "3.2.6",
|
||||
"resolved": "https://registry.npmjs.org/debug/-/debug-3.2.6.tgz",
|
||||
"integrity": "sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"ms": "^2.1.1"
|
||||
}
|
||||
@@ -8453,7 +8523,8 @@
|
||||
"ms": {
|
||||
"version": "2.1.2",
|
||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
|
||||
"integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="
|
||||
"integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==",
|
||||
"dev": true
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -9297,9 +9368,9 @@
|
||||
"integrity": "sha512-2MSPMu7S1iOTL+BOa6K1S62hB2zUAYNF/lV0gSVlOaacd087lc6nR1H1r0e3B1CerTo+RceOmi1iJW+vp21xcQ=="
|
||||
},
|
||||
"get-own-enumerable-property-symbols": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/get-own-enumerable-property-symbols/-/get-own-enumerable-property-symbols-3.0.0.tgz",
|
||||
"integrity": "sha512-CIJYJC4GGF06TakLg8z4GQKvDsx9EMspVxOYih7LerEL/WosUnFIww45CGfxfeKHqlg3twgUrYRT1O3WQqjGCg==",
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmjs.org/get-own-enumerable-property-symbols/-/get-own-enumerable-property-symbols-3.0.1.tgz",
|
||||
"integrity": "sha512-09/VS4iek66Dh2bctjRkowueRJbY1JDGR1L/zRxO1Qk8Uxs6PnqaNSqalpizPT+CDjre3hnEsuzvhgomz9qYrA==",
|
||||
"dev": true
|
||||
},
|
||||
"get-stream": {
|
||||
@@ -16270,9 +16341,9 @@
|
||||
"dev": true
|
||||
},
|
||||
"pretty-bytes": {
|
||||
"version": "4.0.2",
|
||||
"resolved": "https://registry.npmjs.org/pretty-bytes/-/pretty-bytes-4.0.2.tgz",
|
||||
"integrity": "sha1-sr+C5zUNZcbDOqlaqlpPYyf2HNk=",
|
||||
"version": "5.3.0",
|
||||
"resolved": "https://registry.npmjs.org/pretty-bytes/-/pretty-bytes-5.3.0.tgz",
|
||||
"integrity": "sha512-hjGrh+P926p4R4WbaB6OckyRtO0F0/lQBiT+0gnxjV+5kjPBrfVBFCsCLbMqVQeydvIoouYTCmmEURiH3R1Bdg==",
|
||||
"dev": true
|
||||
},
|
||||
"pretty-error": {
|
||||
@@ -22888,12 +22959,12 @@
|
||||
"dev": true
|
||||
},
|
||||
"workbox-background-sync": {
|
||||
"version": "3.6.3",
|
||||
"resolved": "https://registry.npmjs.org/workbox-background-sync/-/workbox-background-sync-3.6.3.tgz",
|
||||
"integrity": "sha512-ypLo0B6dces4gSpaslmDg5wuoUWrHHVJfFWwl1udvSylLdXvnrfhFfriCS42SNEe5lsZtcNZF27W/SMzBlva7Q==",
|
||||
"version": "4.3.1",
|
||||
"resolved": "https://registry.npmjs.org/workbox-background-sync/-/workbox-background-sync-4.3.1.tgz",
|
||||
"integrity": "sha512-1uFkvU8JXi7L7fCHVBEEnc3asPpiAL33kO495UMcD5+arew9IbKW2rV5lpzhoWcm/qhGB89YfO4PmB/0hQwPRg==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"workbox-core": "^3.6.3"
|
||||
"workbox-core": "^4.3.1"
|
||||
}
|
||||
},
|
||||
"workbox-broadcast-cache-update": {
|
||||
@@ -22903,36 +22974,54 @@
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"workbox-core": "^3.6.3"
|
||||
},
|
||||
"dependencies": {
|
||||
"workbox-core": {
|
||||
"version": "3.6.3",
|
||||
"resolved": "https://registry.npmjs.org/workbox-core/-/workbox-core-3.6.3.tgz",
|
||||
"integrity": "sha512-cx9cx0nscPkIWs8Pt98HGrS9/aORuUcSkWjG25GqNWdvD/pSe7/5Oh3BKs0fC+rUshCiyLbxW54q0hA+GqZeSQ==",
|
||||
"dev": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"workbox-broadcast-update": {
|
||||
"version": "4.3.1",
|
||||
"resolved": "https://registry.npmjs.org/workbox-broadcast-update/-/workbox-broadcast-update-4.3.1.tgz",
|
||||
"integrity": "sha512-MTSfgzIljpKLTBPROo4IpKjESD86pPFlZwlvVG32Kb70hW+aob4Jxpblud8EhNb1/L5m43DUM4q7C+W6eQMMbA==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"workbox-core": "^4.3.1"
|
||||
}
|
||||
},
|
||||
"workbox-build": {
|
||||
"version": "3.6.3",
|
||||
"resolved": "https://registry.npmjs.org/workbox-build/-/workbox-build-3.6.3.tgz",
|
||||
"integrity": "sha512-w0clZ/pVjL8VXy6GfthefxpEXs0T8uiRuopZSFVQ8ovfbH6c6kUpEh6DcYwm/Y6dyWPiCucdyAZotgjz+nRz8g==",
|
||||
"version": "4.3.1",
|
||||
"resolved": "https://registry.npmjs.org/workbox-build/-/workbox-build-4.3.1.tgz",
|
||||
"integrity": "sha512-UHdwrN3FrDvicM3AqJS/J07X0KXj67R8Cg0waq1MKEOqzo89ap6zh6LmaLnRAjpB+bDIz+7OlPye9iii9KBnxw==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"babel-runtime": "^6.26.0",
|
||||
"common-tags": "^1.4.0",
|
||||
"@babel/runtime": "^7.3.4",
|
||||
"@hapi/joi": "^15.0.0",
|
||||
"common-tags": "^1.8.0",
|
||||
"fs-extra": "^4.0.2",
|
||||
"glob": "^7.1.2",
|
||||
"joi": "^11.1.1",
|
||||
"glob": "^7.1.3",
|
||||
"lodash.template": "^4.4.0",
|
||||
"pretty-bytes": "^4.0.2",
|
||||
"stringify-object": "^3.2.2",
|
||||
"pretty-bytes": "^5.1.0",
|
||||
"stringify-object": "^3.3.0",
|
||||
"strip-comments": "^1.0.2",
|
||||
"workbox-background-sync": "^3.6.3",
|
||||
"workbox-broadcast-cache-update": "^3.6.3",
|
||||
"workbox-cache-expiration": "^3.6.3",
|
||||
"workbox-cacheable-response": "^3.6.3",
|
||||
"workbox-core": "^3.6.3",
|
||||
"workbox-google-analytics": "^3.6.3",
|
||||
"workbox-navigation-preload": "^3.6.3",
|
||||
"workbox-precaching": "^3.6.3",
|
||||
"workbox-range-requests": "^3.6.3",
|
||||
"workbox-routing": "^3.6.3",
|
||||
"workbox-strategies": "^3.6.3",
|
||||
"workbox-streams": "^3.6.3",
|
||||
"workbox-sw": "^3.6.3"
|
||||
"workbox-background-sync": "^4.3.1",
|
||||
"workbox-broadcast-update": "^4.3.1",
|
||||
"workbox-cacheable-response": "^4.3.1",
|
||||
"workbox-core": "^4.3.1",
|
||||
"workbox-expiration": "^4.3.1",
|
||||
"workbox-google-analytics": "^4.3.1",
|
||||
"workbox-navigation-preload": "^4.3.1",
|
||||
"workbox-precaching": "^4.3.1",
|
||||
"workbox-range-requests": "^4.3.1",
|
||||
"workbox-routing": "^4.3.1",
|
||||
"workbox-strategies": "^4.3.1",
|
||||
"workbox-streams": "^4.3.1",
|
||||
"workbox-sw": "^4.3.1",
|
||||
"workbox-window": "^4.3.1"
|
||||
},
|
||||
"dependencies": {
|
||||
"fs-extra": {
|
||||
@@ -22964,93 +23053,110 @@
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"workbox-core": "^3.6.3"
|
||||
},
|
||||
"dependencies": {
|
||||
"workbox-core": {
|
||||
"version": "3.6.3",
|
||||
"resolved": "https://registry.npmjs.org/workbox-core/-/workbox-core-3.6.3.tgz",
|
||||
"integrity": "sha512-cx9cx0nscPkIWs8Pt98HGrS9/aORuUcSkWjG25GqNWdvD/pSe7/5Oh3BKs0fC+rUshCiyLbxW54q0hA+GqZeSQ==",
|
||||
"dev": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"workbox-cacheable-response": {
|
||||
"version": "3.6.3",
|
||||
"resolved": "https://registry.npmjs.org/workbox-cacheable-response/-/workbox-cacheable-response-3.6.3.tgz",
|
||||
"integrity": "sha512-QpmbGA9SLcA7fklBLm06C4zFg577Dt8u3QgLM0eMnnbaVv3rhm4vbmDpBkyTqvgK/Ly8MBDQzlXDtUCswQwqqg==",
|
||||
"version": "4.3.1",
|
||||
"resolved": "https://registry.npmjs.org/workbox-cacheable-response/-/workbox-cacheable-response-4.3.1.tgz",
|
||||
"integrity": "sha512-Rp5qlzm6z8IOvnQNkCdO9qrDgDpoPNguovs0H8C+wswLuPgSzSp9p2afb5maUt9R1uTIwOXrVQMmPfPypv+npw==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"workbox-core": "^3.6.3"
|
||||
"workbox-core": "^4.3.1"
|
||||
}
|
||||
},
|
||||
"workbox-core": {
|
||||
"version": "3.6.3",
|
||||
"resolved": "https://registry.npmjs.org/workbox-core/-/workbox-core-3.6.3.tgz",
|
||||
"integrity": "sha512-cx9cx0nscPkIWs8Pt98HGrS9/aORuUcSkWjG25GqNWdvD/pSe7/5Oh3BKs0fC+rUshCiyLbxW54q0hA+GqZeSQ==",
|
||||
"version": "4.3.1",
|
||||
"resolved": "https://registry.npmjs.org/workbox-core/-/workbox-core-4.3.1.tgz",
|
||||
"integrity": "sha512-I3C9jlLmMKPxAC1t0ExCq+QoAMd0vAAHULEgRZ7kieCdUd919n53WC0AfvokHNwqRhGn+tIIj7vcb5duCjs2Kg==",
|
||||
"dev": true
|
||||
},
|
||||
"workbox-google-analytics": {
|
||||
"version": "3.6.3",
|
||||
"resolved": "https://registry.npmjs.org/workbox-google-analytics/-/workbox-google-analytics-3.6.3.tgz",
|
||||
"integrity": "sha512-RQBUo/6SXtIaQTRFj4RQZ9e1gAl7D8oS5S+Hi173Kk70/BgJjzPwXpC5A249Jv5YfkCOLMQCeF9A27BiD0b0ig==",
|
||||
"workbox-expiration": {
|
||||
"version": "4.3.1",
|
||||
"resolved": "https://registry.npmjs.org/workbox-expiration/-/workbox-expiration-4.3.1.tgz",
|
||||
"integrity": "sha512-vsJLhgQsQouv9m0rpbXubT5jw0jMQdjpkum0uT+d9tTwhXcEZks7qLfQ9dGSaufTD2eimxbUOJfWLbNQpIDMPw==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"workbox-background-sync": "^3.6.3",
|
||||
"workbox-core": "^3.6.3",
|
||||
"workbox-routing": "^3.6.3",
|
||||
"workbox-strategies": "^3.6.3"
|
||||
"workbox-core": "^4.3.1"
|
||||
}
|
||||
},
|
||||
"workbox-google-analytics": {
|
||||
"version": "4.3.1",
|
||||
"resolved": "https://registry.npmjs.org/workbox-google-analytics/-/workbox-google-analytics-4.3.1.tgz",
|
||||
"integrity": "sha512-xzCjAoKuOb55CBSwQrbyWBKqp35yg1vw9ohIlU2wTy06ZrYfJ8rKochb1MSGlnoBfXGWss3UPzxR5QL5guIFdg==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"workbox-background-sync": "^4.3.1",
|
||||
"workbox-core": "^4.3.1",
|
||||
"workbox-routing": "^4.3.1",
|
||||
"workbox-strategies": "^4.3.1"
|
||||
}
|
||||
},
|
||||
"workbox-navigation-preload": {
|
||||
"version": "3.6.3",
|
||||
"resolved": "https://registry.npmjs.org/workbox-navigation-preload/-/workbox-navigation-preload-3.6.3.tgz",
|
||||
"integrity": "sha512-dd26xTX16DUu0i+MhqZK/jQXgfIitu0yATM4jhRXEmpMqQ4MxEeNvl2CgjDMOHBnCVMax+CFZQWwxMx/X/PqCw==",
|
||||
"version": "4.3.1",
|
||||
"resolved": "https://registry.npmjs.org/workbox-navigation-preload/-/workbox-navigation-preload-4.3.1.tgz",
|
||||
"integrity": "sha512-K076n3oFHYp16/C+F8CwrRqD25GitA6Rkd6+qAmLmMv1QHPI2jfDwYqrytOfKfYq42bYtW8Pr21ejZX7GvALOw==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"workbox-core": "^3.6.3"
|
||||
"workbox-core": "^4.3.1"
|
||||
}
|
||||
},
|
||||
"workbox-precaching": {
|
||||
"version": "3.6.3",
|
||||
"resolved": "https://registry.npmjs.org/workbox-precaching/-/workbox-precaching-3.6.3.tgz",
|
||||
"integrity": "sha512-aBqT66BuMFviPTW6IpccZZHzpA8xzvZU2OM1AdhmSlYDXOJyb1+Z6blVD7z2Q8VNtV1UVwQIdImIX+hH3C3PIw==",
|
||||
"version": "4.3.1",
|
||||
"resolved": "https://registry.npmjs.org/workbox-precaching/-/workbox-precaching-4.3.1.tgz",
|
||||
"integrity": "sha512-piSg/2csPoIi/vPpp48t1q5JLYjMkmg5gsXBQkh/QYapCdVwwmKlU9mHdmy52KsDGIjVaqEUMFvEzn2LRaigqQ==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"workbox-core": "^3.6.3"
|
||||
"workbox-core": "^4.3.1"
|
||||
}
|
||||
},
|
||||
"workbox-range-requests": {
|
||||
"version": "3.6.3",
|
||||
"resolved": "https://registry.npmjs.org/workbox-range-requests/-/workbox-range-requests-3.6.3.tgz",
|
||||
"integrity": "sha512-R+yLWQy7D9aRF9yJ3QzwYnGFnGDhMUij4jVBUVtkl67oaVoP1ymZ81AfCmfZro2kpPRI+vmNMfxxW531cqdx8A==",
|
||||
"version": "4.3.1",
|
||||
"resolved": "https://registry.npmjs.org/workbox-range-requests/-/workbox-range-requests-4.3.1.tgz",
|
||||
"integrity": "sha512-S+HhL9+iTFypJZ/yQSl/x2Bf5pWnbXdd3j57xnb0V60FW1LVn9LRZkPtneODklzYuFZv7qK6riZ5BNyc0R0jZA==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"workbox-core": "^3.6.3"
|
||||
"workbox-core": "^4.3.1"
|
||||
}
|
||||
},
|
||||
"workbox-routing": {
|
||||
"version": "3.6.3",
|
||||
"resolved": "https://registry.npmjs.org/workbox-routing/-/workbox-routing-3.6.3.tgz",
|
||||
"integrity": "sha512-bX20i95OKXXQovXhFOViOK63HYmXvsIwZXKWbSpVeKToxMrp0G/6LZXnhg82ijj/S5yhKNRf9LeGDzaqxzAwMQ==",
|
||||
"version": "4.3.1",
|
||||
"resolved": "https://registry.npmjs.org/workbox-routing/-/workbox-routing-4.3.1.tgz",
|
||||
"integrity": "sha512-FkbtrODA4Imsi0p7TW9u9MXuQ5P4pVs1sWHK4dJMMChVROsbEltuE79fBoIk/BCztvOJ7yUpErMKa4z3uQLX+g==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"workbox-core": "^3.6.3"
|
||||
"workbox-core": "^4.3.1"
|
||||
}
|
||||
},
|
||||
"workbox-strategies": {
|
||||
"version": "3.6.3",
|
||||
"resolved": "https://registry.npmjs.org/workbox-strategies/-/workbox-strategies-3.6.3.tgz",
|
||||
"integrity": "sha512-Pg5eulqeKet2y8j73Yw6xTgLdElktcWExGkzDVCGqfV9JCvnGuEpz5eVsCIK70+k4oJcBCin9qEg3g3CwEIH3g==",
|
||||
"version": "4.3.1",
|
||||
"resolved": "https://registry.npmjs.org/workbox-strategies/-/workbox-strategies-4.3.1.tgz",
|
||||
"integrity": "sha512-F/+E57BmVG8dX6dCCopBlkDvvhg/zj6VDs0PigYwSN23L8hseSRwljrceU2WzTvk/+BSYICsWmRq5qHS2UYzhw==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"workbox-core": "^3.6.3"
|
||||
"workbox-core": "^4.3.1"
|
||||
}
|
||||
},
|
||||
"workbox-streams": {
|
||||
"version": "3.6.3",
|
||||
"resolved": "https://registry.npmjs.org/workbox-streams/-/workbox-streams-3.6.3.tgz",
|
||||
"integrity": "sha512-rqDuS4duj+3aZUYI1LsrD2t9hHOjwPqnUIfrXSOxSVjVn83W2MisDF2Bj+dFUZv4GalL9xqErcFW++9gH+Z27w==",
|
||||
"version": "4.3.1",
|
||||
"resolved": "https://registry.npmjs.org/workbox-streams/-/workbox-streams-4.3.1.tgz",
|
||||
"integrity": "sha512-4Kisis1f/y0ihf4l3u/+ndMkJkIT4/6UOacU3A4BwZSAC9pQ9vSvJpIi/WFGQRH/uPXvuVjF5c2RfIPQFSS2uA==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"workbox-core": "^3.6.3"
|
||||
"workbox-core": "^4.3.1"
|
||||
}
|
||||
},
|
||||
"workbox-sw": {
|
||||
"version": "3.6.3",
|
||||
"resolved": "https://registry.npmjs.org/workbox-sw/-/workbox-sw-3.6.3.tgz",
|
||||
"integrity": "sha512-IQOUi+RLhvYCiv80RP23KBW/NTtIvzvjex28B8NW1jOm+iV4VIu3VXKXTA6er5/wjjuhmtB28qEAUqADLAyOSg==",
|
||||
"version": "4.3.1",
|
||||
"resolved": "https://registry.npmjs.org/workbox-sw/-/workbox-sw-4.3.1.tgz",
|
||||
"integrity": "sha512-0jXdusCL2uC5gM3yYFT6QMBzKfBr2XTk0g5TPAV4y8IZDyVNDyj1a8uSXy3/XrvkVTmQvLN4O5k3JawGReXr9w==",
|
||||
"dev": true
|
||||
},
|
||||
"workbox-webpack-plugin": {
|
||||
@@ -23062,6 +23168,169 @@
|
||||
"babel-runtime": "^6.26.0",
|
||||
"json-stable-stringify": "^1.0.1",
|
||||
"workbox-build": "^3.6.3"
|
||||
},
|
||||
"dependencies": {
|
||||
"fs-extra": {
|
||||
"version": "4.0.3",
|
||||
"resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-4.0.3.tgz",
|
||||
"integrity": "sha512-q6rbdDd1o2mAnQreO7YADIxf/Whx4AHBiRf6d+/cVT8h44ss+lHgxf1FemcqDnQt9X3ct4McHr+JMGlYSsK7Cg==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"graceful-fs": "^4.1.2",
|
||||
"jsonfile": "^4.0.0",
|
||||
"universalify": "^0.1.0"
|
||||
}
|
||||
},
|
||||
"jsonfile": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz",
|
||||
"integrity": "sha1-h3Gq4HmbZAdrdmQPygWPnBDjPss=",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"graceful-fs": "^4.1.6"
|
||||
}
|
||||
},
|
||||
"pretty-bytes": {
|
||||
"version": "4.0.2",
|
||||
"resolved": "https://registry.npmjs.org/pretty-bytes/-/pretty-bytes-4.0.2.tgz",
|
||||
"integrity": "sha1-sr+C5zUNZcbDOqlaqlpPYyf2HNk=",
|
||||
"dev": true
|
||||
},
|
||||
"workbox-background-sync": {
|
||||
"version": "3.6.3",
|
||||
"resolved": "https://registry.npmjs.org/workbox-background-sync/-/workbox-background-sync-3.6.3.tgz",
|
||||
"integrity": "sha512-ypLo0B6dces4gSpaslmDg5wuoUWrHHVJfFWwl1udvSylLdXvnrfhFfriCS42SNEe5lsZtcNZF27W/SMzBlva7Q==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"workbox-core": "^3.6.3"
|
||||
}
|
||||
},
|
||||
"workbox-build": {
|
||||
"version": "3.6.3",
|
||||
"resolved": "https://registry.npmjs.org/workbox-build/-/workbox-build-3.6.3.tgz",
|
||||
"integrity": "sha512-w0clZ/pVjL8VXy6GfthefxpEXs0T8uiRuopZSFVQ8ovfbH6c6kUpEh6DcYwm/Y6dyWPiCucdyAZotgjz+nRz8g==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"babel-runtime": "^6.26.0",
|
||||
"common-tags": "^1.4.0",
|
||||
"fs-extra": "^4.0.2",
|
||||
"glob": "^7.1.2",
|
||||
"joi": "^11.1.1",
|
||||
"lodash.template": "^4.4.0",
|
||||
"pretty-bytes": "^4.0.2",
|
||||
"stringify-object": "^3.2.2",
|
||||
"strip-comments": "^1.0.2",
|
||||
"workbox-background-sync": "^3.6.3",
|
||||
"workbox-broadcast-cache-update": "^3.6.3",
|
||||
"workbox-cache-expiration": "^3.6.3",
|
||||
"workbox-cacheable-response": "^3.6.3",
|
||||
"workbox-core": "^3.6.3",
|
||||
"workbox-google-analytics": "^3.6.3",
|
||||
"workbox-navigation-preload": "^3.6.3",
|
||||
"workbox-precaching": "^3.6.3",
|
||||
"workbox-range-requests": "^3.6.3",
|
||||
"workbox-routing": "^3.6.3",
|
||||
"workbox-strategies": "^3.6.3",
|
||||
"workbox-streams": "^3.6.3",
|
||||
"workbox-sw": "^3.6.3"
|
||||
}
|
||||
},
|
||||
"workbox-cacheable-response": {
|
||||
"version": "3.6.3",
|
||||
"resolved": "https://registry.npmjs.org/workbox-cacheable-response/-/workbox-cacheable-response-3.6.3.tgz",
|
||||
"integrity": "sha512-QpmbGA9SLcA7fklBLm06C4zFg577Dt8u3QgLM0eMnnbaVv3rhm4vbmDpBkyTqvgK/Ly8MBDQzlXDtUCswQwqqg==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"workbox-core": "^3.6.3"
|
||||
}
|
||||
},
|
||||
"workbox-core": {
|
||||
"version": "3.6.3",
|
||||
"resolved": "https://registry.npmjs.org/workbox-core/-/workbox-core-3.6.3.tgz",
|
||||
"integrity": "sha512-cx9cx0nscPkIWs8Pt98HGrS9/aORuUcSkWjG25GqNWdvD/pSe7/5Oh3BKs0fC+rUshCiyLbxW54q0hA+GqZeSQ==",
|
||||
"dev": true
|
||||
},
|
||||
"workbox-google-analytics": {
|
||||
"version": "3.6.3",
|
||||
"resolved": "https://registry.npmjs.org/workbox-google-analytics/-/workbox-google-analytics-3.6.3.tgz",
|
||||
"integrity": "sha512-RQBUo/6SXtIaQTRFj4RQZ9e1gAl7D8oS5S+Hi173Kk70/BgJjzPwXpC5A249Jv5YfkCOLMQCeF9A27BiD0b0ig==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"workbox-background-sync": "^3.6.3",
|
||||
"workbox-core": "^3.6.3",
|
||||
"workbox-routing": "^3.6.3",
|
||||
"workbox-strategies": "^3.6.3"
|
||||
}
|
||||
},
|
||||
"workbox-navigation-preload": {
|
||||
"version": "3.6.3",
|
||||
"resolved": "https://registry.npmjs.org/workbox-navigation-preload/-/workbox-navigation-preload-3.6.3.tgz",
|
||||
"integrity": "sha512-dd26xTX16DUu0i+MhqZK/jQXgfIitu0yATM4jhRXEmpMqQ4MxEeNvl2CgjDMOHBnCVMax+CFZQWwxMx/X/PqCw==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"workbox-core": "^3.6.3"
|
||||
}
|
||||
},
|
||||
"workbox-precaching": {
|
||||
"version": "3.6.3",
|
||||
"resolved": "https://registry.npmjs.org/workbox-precaching/-/workbox-precaching-3.6.3.tgz",
|
||||
"integrity": "sha512-aBqT66BuMFviPTW6IpccZZHzpA8xzvZU2OM1AdhmSlYDXOJyb1+Z6blVD7z2Q8VNtV1UVwQIdImIX+hH3C3PIw==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"workbox-core": "^3.6.3"
|
||||
}
|
||||
},
|
||||
"workbox-range-requests": {
|
||||
"version": "3.6.3",
|
||||
"resolved": "https://registry.npmjs.org/workbox-range-requests/-/workbox-range-requests-3.6.3.tgz",
|
||||
"integrity": "sha512-R+yLWQy7D9aRF9yJ3QzwYnGFnGDhMUij4jVBUVtkl67oaVoP1ymZ81AfCmfZro2kpPRI+vmNMfxxW531cqdx8A==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"workbox-core": "^3.6.3"
|
||||
}
|
||||
},
|
||||
"workbox-routing": {
|
||||
"version": "3.6.3",
|
||||
"resolved": "https://registry.npmjs.org/workbox-routing/-/workbox-routing-3.6.3.tgz",
|
||||
"integrity": "sha512-bX20i95OKXXQovXhFOViOK63HYmXvsIwZXKWbSpVeKToxMrp0G/6LZXnhg82ijj/S5yhKNRf9LeGDzaqxzAwMQ==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"workbox-core": "^3.6.3"
|
||||
}
|
||||
},
|
||||
"workbox-strategies": {
|
||||
"version": "3.6.3",
|
||||
"resolved": "https://registry.npmjs.org/workbox-strategies/-/workbox-strategies-3.6.3.tgz",
|
||||
"integrity": "sha512-Pg5eulqeKet2y8j73Yw6xTgLdElktcWExGkzDVCGqfV9JCvnGuEpz5eVsCIK70+k4oJcBCin9qEg3g3CwEIH3g==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"workbox-core": "^3.6.3"
|
||||
}
|
||||
},
|
||||
"workbox-streams": {
|
||||
"version": "3.6.3",
|
||||
"resolved": "https://registry.npmjs.org/workbox-streams/-/workbox-streams-3.6.3.tgz",
|
||||
"integrity": "sha512-rqDuS4duj+3aZUYI1LsrD2t9hHOjwPqnUIfrXSOxSVjVn83W2MisDF2Bj+dFUZv4GalL9xqErcFW++9gH+Z27w==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"workbox-core": "^3.6.3"
|
||||
}
|
||||
},
|
||||
"workbox-sw": {
|
||||
"version": "3.6.3",
|
||||
"resolved": "https://registry.npmjs.org/workbox-sw/-/workbox-sw-3.6.3.tgz",
|
||||
"integrity": "sha512-IQOUi+RLhvYCiv80RP23KBW/NTtIvzvjex28B8NW1jOm+iV4VIu3VXKXTA6er5/wjjuhmtB28qEAUqADLAyOSg==",
|
||||
"dev": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"workbox-window": {
|
||||
"version": "4.3.1",
|
||||
"resolved": "https://registry.npmjs.org/workbox-window/-/workbox-window-4.3.1.tgz",
|
||||
"integrity": "sha512-C5gWKh6I58w3GeSc0wp2Ne+rqVw8qwcmZnQGpjiek8A2wpbxSJb1FdCoQVO+jDJs35bFgo/WETgl1fqgsxN0Hg==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"workbox-core": "^4.3.1"
|
||||
}
|
||||
},
|
||||
"worker-farm": {
|
||||
|
@@ -23,7 +23,8 @@
|
||||
"recursive-readdir": "^2.2.2",
|
||||
"shelljs": "^0.7.7",
|
||||
"unzipper": "^0.9.11",
|
||||
"webpack": "4.28.3"
|
||||
"webpack": "4.28.3",
|
||||
"workbox-build": "^4.3.1"
|
||||
},
|
||||
"dependencies": {
|
||||
"@blueprintjs/core": "file:src/Utils/BlueprintJsPlaceholder",
|
||||
@@ -33,7 +34,7 @@
|
||||
"@material-ui/icons": "4.2.1",
|
||||
"algoliasearch": "3.33.0",
|
||||
"aws-sdk": "^2.100.0",
|
||||
"axios": "^0.16.1",
|
||||
"axios": "^0.18.1",
|
||||
"blueimp-md5": "^2.10.0",
|
||||
"classnames": "2.2.5",
|
||||
"date-fns": "^1.29.0",
|
||||
@@ -72,13 +73,15 @@
|
||||
"wait-promise": "0.4.1"
|
||||
},
|
||||
"scripts": {
|
||||
"postinstall": "npm run import-resources",
|
||||
"import-resources": "cd scripts && node import-libGD.js && node import-GDJS-Runtime.js && node import-monaco-editor.js && node import-zipped-editor.js piskel 5.0.0-beta56 a2c36775109a1c1181d0b9ab9b7c903365c4ecb340118f1237b66236f23e20dd && node import-zipped-editor.js jfxr 5.0.0-beta55 8ac12b557c2ddba958c6f0d3e0c5df8cf3369a65262dcb90cf5c8a7a7d20bdf6 && node import-zipped-editor.js yarn 5.0.0-beta80 5930c686ccf2b6fd3433b1c9539483473a6aedb8b6a73dd13c07030c95efecb9",
|
||||
"start": "npm run import-resources && react-scripts start",
|
||||
"postinstall": "npm run import-resources && npm run make-version-metadata",
|
||||
"import-resources": "cd scripts && node import-libGD.js && node import-GDJS-Runtime.js && node import-monaco-editor.js && node import-zipped-external-libs.js && node import-zipped-editor.js piskel 5.0.0-beta56 a2c36775109a1c1181d0b9ab9b7c903365c4ecb340118f1237b66236f23e20dd && node import-zipped-editor.js jfxr 5.0.0-beta55 8ac12b557c2ddba958c6f0d3e0c5df8cf3369a65262dcb90cf5c8a7a7d20bdf6 && node import-zipped-editor.js yarn 5.0.0-beta80 5930c686ccf2b6fd3433b1c9539483473a6aedb8b6a73dd13c07030c95efecb9",
|
||||
"make-version-metadata": "cd scripts && node make-version-metadata.js",
|
||||
"make-service-worker": "cd scripts && node make-service-worker.js",
|
||||
"start": "npm run import-resources && npm run make-version-metadata && react-scripts start",
|
||||
"electron-win": "cd ../electron-app && node node_modules/electron/cli.js app",
|
||||
"electron-linux": "cd ../electron-app && ./node_modules/electron/dist/electron app",
|
||||
"electron-mac": "cd ../electron-app && ./node_modules/electron/dist/Electron.app/Contents/MacOS/Electron app",
|
||||
"build": "npm run import-resources && react-scripts build",
|
||||
"build": "npm run import-resources && npm run make-version-metadata && react-scripts build && npm run make-service-worker",
|
||||
"format": "prettier --write \"src/!(locales)/**/*.js\"",
|
||||
"check-format": "prettier --list-different \"src/!(locales)/**/*.js\"",
|
||||
"test": "react-scripts test",
|
||||
|
BIN
newIDE/app/public/apple-touch-icon.png
Normal file
After Width: | Height: | Size: 4.8 KiB |
BIN
newIDE/app/public/favicon-256.png
Executable file
After Width: | Height: | Size: 5.9 KiB |
BIN
newIDE/app/public/favicon-512.png
Executable file
After Width: | Height: | Size: 11 KiB |
Before Width: | Height: | Size: 361 KiB After Width: | Height: | Size: 3.4 KiB |
@@ -1,15 +1,30 @@
|
||||
{
|
||||
"short_name": "GDevelop",
|
||||
"name": "GDevelop game creator",
|
||||
"name": "GDevelop",
|
||||
"icons": [
|
||||
{
|
||||
"src": "favicon.ico",
|
||||
"src": "favicon-512.png",
|
||||
"sizes": "512x512",
|
||||
"type": "image/png"
|
||||
},
|
||||
{
|
||||
"src": "favicon-256.png",
|
||||
"sizes": "256x256",
|
||||
"type": "image/png"
|
||||
},
|
||||
{
|
||||
"src": "apple-touch-icon.png",
|
||||
"type": "image/png",
|
||||
"sizes": "192x192"
|
||||
},
|
||||
{
|
||||
"src": "favicon.ico",
|
||||
"sizes": "128x128",
|
||||
"type": "image/png"
|
||||
}
|
||||
],
|
||||
"start_url": "./index.html",
|
||||
"display": "standalone",
|
||||
"theme_color": "#4ab0e4",
|
||||
"theme_color": "#252525",
|
||||
"background_color": "#f0f0f0"
|
||||
}
|
||||
}
|
||||
|
Before Width: | Height: | Size: 5.5 KiB After Width: | Height: | Size: 5.5 KiB |
BIN
newIDE/app/scripts/external-libs/zip.js.zip
Normal file
BIN
newIDE/app/scripts/external-libs/zlib-asm.zip
Normal file
51
newIDE/app/scripts/import-zipped-external-libs.js
Normal file
@@ -0,0 +1,51 @@
|
||||
/**
|
||||
* This script extracts the external libraries in external-libs inside
|
||||
* public/external. It's up to the IDE to load them appropriately.
|
||||
*/
|
||||
var shell = require('shelljs');
|
||||
var fs = require('fs');
|
||||
var unzipper = require('unzipper');
|
||||
var path = require('path');
|
||||
|
||||
const externalLibsFolder = path.join(__dirname, 'external-libs');
|
||||
|
||||
const getAllExternalLibFiles = () =>
|
||||
new Promise((resolve, reject) => {
|
||||
fs.readdir(externalLibsFolder, (error, externalLibs) => {
|
||||
if (error) {
|
||||
return reject(error);
|
||||
}
|
||||
|
||||
return resolve(
|
||||
externalLibs
|
||||
.filter(name => name !== '.DS_Store')
|
||||
.filter(name => name !== '.')
|
||||
.filter(name => name !== '..')
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
getAllExternalLibFiles().then(externalLibFiles => {
|
||||
externalLibFiles.forEach(externalLibFile => {
|
||||
try {
|
||||
fs.createReadStream(path.join(externalLibsFolder, externalLibFile))
|
||||
.pipe(
|
||||
unzipper.Extract({
|
||||
path: path.join('../public/external/'),
|
||||
})
|
||||
)
|
||||
.on('close', function() {
|
||||
shell.echo(
|
||||
'✅ Extracted ' + externalLibFile + ' to public/external/ folder'
|
||||
);
|
||||
});
|
||||
} catch (e) {
|
||||
shell.echo(
|
||||
'❌ Error while extracting ' +
|
||||
externalLibFile +
|
||||
' to public/external/ folder',
|
||||
e.message
|
||||
);
|
||||
}
|
||||
});
|
||||
});
|
83
newIDE/app/scripts/make-service-worker.js
Normal file
@@ -0,0 +1,83 @@
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const workboxBuild = require('workbox-build');
|
||||
const buildPath = '../build';
|
||||
|
||||
/**
|
||||
* Remove files created by create-react-app default service worker.
|
||||
*/
|
||||
const cleanBuildFiles = () => {
|
||||
const listBuildFiles = () =>
|
||||
new Promise((resolve, reject) => {
|
||||
fs.readdir(buildPath, (err, files) => {
|
||||
if (err) {
|
||||
reject(err);
|
||||
return;
|
||||
}
|
||||
|
||||
resolve(files);
|
||||
});
|
||||
});
|
||||
|
||||
return listBuildFiles().then(files => {
|
||||
files
|
||||
.filter(filename => filename.indexOf('precache-manifest.') === 0)
|
||||
.forEach(filename => {
|
||||
fs.unlinkSync(path.join(buildPath, filename));
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Create the service worker with workbox.
|
||||
*/
|
||||
const buildSW = () => {
|
||||
return workboxBuild
|
||||
.injectManifest({
|
||||
swSrc: 'service-worker-template/service-worker-template.js',
|
||||
swDest: '../build/service-worker.js',
|
||||
globDirectory: buildPath,
|
||||
globPatterns: [
|
||||
// Application:
|
||||
'*.{js,css,html,png}', // Root files
|
||||
'static/css/*.css',
|
||||
'static/media/*',
|
||||
'static/js/!(locales-|local-app)*.js',
|
||||
|
||||
// Resources:
|
||||
'{JsPlatform,CppPlatform,res}/**/*.png',
|
||||
|
||||
// External libs:
|
||||
|
||||
// Zip.js
|
||||
'external/zip.js/WebContent/{deflate,inflate,z-worker,zip}.js', // Zip.js
|
||||
'external/zip.js/WebContent/zlib-asm/codecs.js', // zlib-asm codec for Zip.js
|
||||
'external/zlib-asm/zlib.js', // zlib-asm
|
||||
|
||||
// Monaco Editor (for JavaScript)
|
||||
'external/monaco-editor-min/vs/loader.js',
|
||||
'external/monaco-editor-min/vs/base/worker/workerMain.js',
|
||||
'external/monaco-editor-min/vs/basic-languages/javascript/javascript.js',
|
||||
'external/monaco-editor-min/vs/language/typescript/tsMode.js',
|
||||
'external/monaco-editor-min/vs/language/typescript/tsWorker.js',
|
||||
// 'external/monaco-editor-min/vs/editor/editor.main.js', // Seems useless?
|
||||
// 'external/monaco-editor-min/vs/editor/editor.main.css',
|
||||
],
|
||||
|
||||
// Increase the limit to 6mb:
|
||||
maximumFileSizeToCacheInBytes: 6 * 1024 * 1024,
|
||||
})
|
||||
.then(({ count, size, warnings }) => {
|
||||
// Optionally, log any warnings and details.
|
||||
warnings.forEach(warning => {
|
||||
console.log(`⚠️ workbox warning: ${warning}`);
|
||||
});
|
||||
console.log(
|
||||
`ℹ️ ${count} files will be precached, totaling ${size /
|
||||
1000 /
|
||||
1000} MB.`
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
cleanBuildFiles().then(() => buildSW());
|
46
newIDE/app/scripts/make-version-metadata.js
Normal file
@@ -0,0 +1,46 @@
|
||||
/**
|
||||
* Launch this script to re-generate the files containing the list of extensions
|
||||
* being used by each example.
|
||||
*/
|
||||
const fs = require('fs');
|
||||
var shell = require('shelljs');
|
||||
|
||||
const electronAppPackageJson = require('../../electron-app/app/package.json');
|
||||
const version = electronAppPackageJson.version;
|
||||
const outputFile = '../src/Version/VersionMetadata.js';
|
||||
const gitHashShellString = shell.exec(`git rev-parse "HEAD"`, {
|
||||
silent: true,
|
||||
});
|
||||
|
||||
let gitHash = gitHashShellString.stdout.trim();
|
||||
if (gitHashShellString.stderr || gitHashShellString.code) {
|
||||
shell.echo(`⚠️ Can't find the hash or branch of the associated commit.`);
|
||||
gitHash = 'unknown-hash';
|
||||
}
|
||||
|
||||
const writeFile = object => {
|
||||
return new Promise((resolve, reject) => {
|
||||
const content = [
|
||||
`// @flow`,
|
||||
`// This file is generated by make-version-metadata.js script`,
|
||||
`// Don't import this file directly, prefer to use newIDE/app/src/Version/index.js instead.`,
|
||||
`// prettier-ignore`,
|
||||
`module.exports = ${JSON.stringify(object, null, 2)};`,
|
||||
``,
|
||||
].join('\n');
|
||||
fs.writeFile(outputFile, content, err => {
|
||||
if (err) return reject(err);
|
||||
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
writeFile({
|
||||
version,
|
||||
gitHash,
|
||||
versionWithHash: [version, gitHash].join('-'),
|
||||
}).then(
|
||||
() => console.info('✅ src/Version/VersionMetadata.js properly generated.'),
|
||||
err => console.error('❌ Error while src/Version/VersionMetadata.js', err)
|
||||
);
|
@@ -0,0 +1,47 @@
|
||||
if (typeof importScripts === 'function') {
|
||||
importScripts(
|
||||
'https://storage.googleapis.com/workbox-cdn/releases/3.5.0/workbox-sw.js'
|
||||
);
|
||||
/* global workbox */
|
||||
if (workbox) {
|
||||
/* injection point for manifest files. */
|
||||
workbox.precaching.precacheAndRoute([]);
|
||||
|
||||
/* custom cache rules*/
|
||||
workbox.routing.registerNavigationRoute('/index.html', {
|
||||
blacklist: [/^\/_/, /\/[^\/]+\.[^\/]+$/],
|
||||
});
|
||||
|
||||
// Cache resources from GDevelop cloudfront server (CORS enabled).
|
||||
workbox.routing.registerRoute(
|
||||
/https:\/\/df5lqcdudryde\.cloudfront\.net\/.*$/,
|
||||
workbox.strategies.networkFirst({
|
||||
cacheName: 'gdevelop-cloudfront-df5lqcdudryde',
|
||||
plugins: [
|
||||
new workbox.expiration.Plugin({
|
||||
maxEntries: 500,
|
||||
}),
|
||||
],
|
||||
})
|
||||
);
|
||||
|
||||
// TODO: this should be useless?
|
||||
workbox.routing.registerRoute(
|
||||
/\.(?:png|gif|jpg|jpeg)$/,
|
||||
workbox.strategies.networkFirst({
|
||||
cacheName: 'images',
|
||||
plugins: [
|
||||
new workbox.expiration.Plugin({
|
||||
maxEntries: 150,
|
||||
}),
|
||||
],
|
||||
})
|
||||
);
|
||||
} else {
|
||||
console.log('Workbox could not be loaded - no offline support');
|
||||
}
|
||||
} else {
|
||||
console.log(
|
||||
'importScripts does not exist on this browser - no offline support'
|
||||
);
|
||||
}
|
@@ -44,21 +44,13 @@ export const create = (authentification: Authentification) => {
|
||||
{({ i18n, eventsFunctionsExtensionsState }) => (
|
||||
<ProjectStorageProviders
|
||||
appArguments={appArguments}
|
||||
storageProviders={
|
||||
Window.isDev()
|
||||
? [
|
||||
InternalFileStorageProvider,
|
||||
GoogleDriveStorageProvider,
|
||||
DropboxStorageProvider,
|
||||
OneDriveStorageProvider,
|
||||
DownloadFileStorageProvider,
|
||||
]
|
||||
: [
|
||||
// TODO: Enable Google Drive once app is validated.
|
||||
InternalFileStorageProvider,
|
||||
DownloadFileStorageProvider,
|
||||
]
|
||||
}
|
||||
storageProviders={[
|
||||
InternalFileStorageProvider,
|
||||
GoogleDriveStorageProvider,
|
||||
DropboxStorageProvider,
|
||||
OneDriveStorageProvider,
|
||||
DownloadFileStorageProvider,
|
||||
]}
|
||||
defaultStorageProvider={InternalFileStorageProvider}
|
||||
>
|
||||
{({
|
||||
@@ -70,9 +62,15 @@ export const create = (authentification: Authentification) => {
|
||||
<MainFrame
|
||||
i18n={i18n}
|
||||
eventsFunctionsExtensionsState={eventsFunctionsExtensionsState}
|
||||
previewLauncher={<BrowserS3PreviewLauncher />}
|
||||
renderPreviewLauncher={(props, ref) => (
|
||||
<BrowserS3PreviewLauncher {...props} ref={ref} />
|
||||
)}
|
||||
renderExportDialog={props => (
|
||||
<ExportDialog {...props} exporters={getBrowserExporters()} />
|
||||
<ExportDialog
|
||||
{...props}
|
||||
exporters={getBrowserExporters()}
|
||||
allExportersRequireOnline
|
||||
/>
|
||||
)}
|
||||
renderCreateDialog={props => (
|
||||
<CreateProjectDialog
|
||||
@@ -87,7 +85,6 @@ export const create = (authentification: Authentification) => {
|
||||
storageProviderOperations={currentStorageProviderOperations}
|
||||
resourceSources={browserResourceSources}
|
||||
resourceExternalEditors={browserResourceExternalEditors}
|
||||
authentification={authentification}
|
||||
extensionsLoader={makeExtensionsLoader({
|
||||
objectsEditorService: ObjectsEditorService,
|
||||
objectsRenderingService: ObjectsRenderingService,
|
||||
|
@@ -36,7 +36,7 @@ export const setupAutocompletions = (monaco: any) => {
|
||||
});
|
||||
});
|
||||
|
||||
findGDJS(gdjsRoot => {
|
||||
findGDJS().then(({ gdjsRoot }) => {
|
||||
const runtimePath = path.join(gdjsRoot, 'Runtime');
|
||||
const extensionsPath = path.join(runtimePath, 'Extensions');
|
||||
const eventToolsPath = path.join(runtimePath, 'events-tools');
|
||||
|
@@ -220,11 +220,12 @@ function generateBehavior(
|
||||
options.codeNamespacePrefix +
|
||||
'__' +
|
||||
mangleName(eventsBasedBehavior.getName());
|
||||
|
||||
behaviorMetadata.setIncludeFile(
|
||||
options.eventsFunctionCodeWriter.getIncludeFileFor(codeNamespace)
|
||||
const includeFile = options.eventsFunctionCodeWriter.getIncludeFileFor(
|
||||
codeNamespace
|
||||
);
|
||||
|
||||
behaviorMetadata.setIncludeFile(includeFile);
|
||||
|
||||
return Promise.resolve().then(() => {
|
||||
const behaviorMethodMangledNames = new gd.MapStringString();
|
||||
|
||||
@@ -260,11 +261,7 @@ function generateBehavior(
|
||||
|
||||
const codeExtraInformation = instructionOrExpression.getCodeExtraInformation();
|
||||
codeExtraInformation
|
||||
.setIncludeFile(
|
||||
options.eventsFunctionCodeWriter.getIncludeFileFor(
|
||||
eventsFunctionMangledName
|
||||
)
|
||||
)
|
||||
.setIncludeFile(includeFile)
|
||||
.setFunctionName(eventsFunctionMangledName);
|
||||
});
|
||||
|
||||
|
@@ -205,6 +205,7 @@ type EventsTreeProps = {|
|
||||
searchFocusOffset: ?number,
|
||||
|
||||
onEventMoved: () => void,
|
||||
onScroll?: () => void,
|
||||
|
||||
screenType: ScreenType,
|
||||
windowWidth: WidthType,
|
||||
@@ -396,6 +397,7 @@ export default class ThemableEventsTree extends Component<EventsTreeProps, *> {
|
||||
})}
|
||||
alt=""
|
||||
src={getThumbnail(project, object)}
|
||||
crossOrigin="anonymous"
|
||||
/>
|
||||
);
|
||||
};
|
||||
@@ -496,6 +498,7 @@ export default class ThemableEventsTree extends Component<EventsTreeProps, *> {
|
||||
}
|
||||
reactVirtualizedListProps={{
|
||||
ref: list => (this._list = list),
|
||||
onScroll: this.props.onScroll,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
@@ -1,5 +1,11 @@
|
||||
/* This overwrite the default react-sortable-tree css to display events list */
|
||||
|
||||
/**
|
||||
* Remove the outline visible on the events sheet (contrary to most
|
||||
* controls on screen, we don't want a visible focus there).
|
||||
*/
|
||||
.gd-events-sheet:focus { outline: none; }
|
||||
|
||||
/**
|
||||
* Draggable handle on the left of an event
|
||||
*/
|
||||
|
@@ -7,12 +7,18 @@ import { Column } from '../UI/Grid';
|
||||
const styles = {
|
||||
popover: {
|
||||
paddingBottom: 10,
|
||||
paddingLeft: 5,
|
||||
paddingRight: 5,
|
||||
overflowY: 'auto',
|
||||
|
||||
// Restrict size in case of extra small or large popover (though this should not happen)
|
||||
minHeight: 30,
|
||||
maxHeight: 400,
|
||||
maxWidth: 600,
|
||||
height: 80,
|
||||
overflowY: 'hidden',
|
||||
minWidth: 300, // Avoid extra small popover for some parameters like relational operator
|
||||
|
||||
// When displayed in an events sheet that has Mosaic windows (see `EditorMosaic`) next to it,
|
||||
// it could be displayed behind them, because they have a z-index of 1 :/ Use a z-index of 2
|
||||
// then. Only one InlinePopover should be shown at a time anyway.
|
||||
zIndex: 2,
|
||||
},
|
||||
contentContainer: {
|
||||
overflow: 'hidden',
|
||||
|
@@ -14,7 +14,7 @@ import EmptyMessage from '../../../UI/EmptyMessage';
|
||||
import ScrollView from '../../../UI/ScrollView';
|
||||
import { Line } from '../../../UI/Grid';
|
||||
import { ListItem } from '../../../UI/List';
|
||||
import { getInstructionListItemKey } from '../SelectorListItems/Keys';
|
||||
import { getInstructionListItemValue } from '../SelectorListItems/Keys';
|
||||
|
||||
const styles = {
|
||||
searchBar: {
|
||||
@@ -141,7 +141,7 @@ export default class InstructionOrExpressionSelector extends React.PureComponent
|
||||
enumeratedInstructionOrExpressionMetadata.type,
|
||||
enumeratedInstructionOrExpressionMetadata
|
||||
),
|
||||
selectedValue: getInstructionListItemKey(
|
||||
selectedValue: getInstructionListItemValue(
|
||||
selectedType
|
||||
),
|
||||
})
|
||||
@@ -151,7 +151,9 @@ export default class InstructionOrExpressionSelector extends React.PureComponent
|
||||
iconSize,
|
||||
onChoose,
|
||||
useSubheaders,
|
||||
selectedValue: getInstructionListItemKey(selectedType),
|
||||
selectedValue: getInstructionListItemValue(
|
||||
selectedType
|
||||
),
|
||||
initiallyOpenedPath: this.initialInstructionTypePath,
|
||||
selectedItemRef: this._selectedItem,
|
||||
})}
|
||||
|
@@ -38,8 +38,8 @@ import {
|
||||
getTagsFromString,
|
||||
} from '../../Utils/TagsHelper';
|
||||
import {
|
||||
getObjectOrObjectGroupListItemKey,
|
||||
getInstructionListItemKey,
|
||||
getObjectOrObjectGroupListItemValue,
|
||||
getInstructionListItemValue,
|
||||
} from './SelectorListItems/Keys';
|
||||
|
||||
const styles = {
|
||||
@@ -272,7 +272,7 @@ export default class InstructionOrObjectSelector extends React.PureComponent<
|
||||
objectWithContext.object.getName()
|
||||
),
|
||||
selectedValue: chosenObjectName
|
||||
? getObjectOrObjectGroupListItemKey(
|
||||
? getObjectOrObjectGroupListItemValue(
|
||||
chosenObjectName
|
||||
)
|
||||
: undefined,
|
||||
@@ -292,7 +292,7 @@ export default class InstructionOrObjectSelector extends React.PureComponent<
|
||||
onClick: () =>
|
||||
onChooseObject(groupWithContext.group.getName()),
|
||||
selectedValue: chosenObjectName
|
||||
? getObjectOrObjectGroupListItemKey(
|
||||
? getObjectOrObjectGroupListItemValue(
|
||||
chosenObjectName
|
||||
)
|
||||
: undefined,
|
||||
@@ -318,7 +318,9 @@ export default class InstructionOrObjectSelector extends React.PureComponent<
|
||||
instructionMetadata
|
||||
),
|
||||
selectedValue: chosenInstructionType
|
||||
? getInstructionListItemKey(chosenInstructionType)
|
||||
? getInstructionListItemValue(
|
||||
chosenInstructionType
|
||||
)
|
||||
: undefined,
|
||||
})
|
||||
)}
|
||||
@@ -329,7 +331,7 @@ export default class InstructionOrObjectSelector extends React.PureComponent<
|
||||
onChoose: onChooseInstruction,
|
||||
iconSize,
|
||||
selectedValue: chosenInstructionType
|
||||
? getInstructionListItemKey(chosenInstructionType)
|
||||
? getInstructionListItemValue(chosenInstructionType)
|
||||
: undefined,
|
||||
initiallyOpenedPath: this.initialInstructionTypePath,
|
||||
selectedItemRef: this._selectedItem,
|
||||
|
@@ -1,13 +1,31 @@
|
||||
// @flow
|
||||
import type {
|
||||
GroupWithContext,
|
||||
ObjectWithContext,
|
||||
} from '../../../ObjectsList/EnumerateObjects';
|
||||
import { type EnumeratedInstructionOrExpressionMetadata } from '../InstructionOrExpressionSelector/EnumeratedInstructionOrExpressionMetadata.js';
|
||||
|
||||
// ListItem created must have consistent keys that include their type
|
||||
// (object, group, instruction) to allow them to be living
|
||||
// in the same list (in search results) and be selected.
|
||||
|
||||
export const getObjectOrObjectGroupListItemKey = (
|
||||
export const getObjectGroupListItemKey = (groupWithContext: GroupWithContext) =>
|
||||
`object-group-key-${groupWithContext.group.getName()}-${
|
||||
groupWithContext.global ? '-global' : ''
|
||||
}`;
|
||||
export const getObjectListItemKey = (objectWithContext: ObjectWithContext) =>
|
||||
`object-key-${objectWithContext.object.getName()}-${
|
||||
objectWithContext.global ? '-global' : ''
|
||||
}`;
|
||||
export const getObjectOrObjectGroupListItemValue = (
|
||||
objectOrObjectGroupName: string
|
||||
) => `object-or-object-group-${objectOrObjectGroupName}`;
|
||||
export const getInstructionListItemKey = (instructionType: string) =>
|
||||
`instruction-${instructionType}`;
|
||||
) => `object-or-object-group-value-${objectOrObjectGroupName}`;
|
||||
|
||||
export const getInstructionListItemKey = (
|
||||
instruction: EnumeratedInstructionOrExpressionMetadata
|
||||
) => `instruction-key-${instruction.fullGroupName}-${instruction.type}`;
|
||||
export const getInstructionListItemValue = (instructionType: string) =>
|
||||
`instruction-value-${instructionType}`;
|
||||
|
||||
export const getSubheaderListItemKey = (subheader: string) =>
|
||||
`subheader-${subheader}`;
|
||||
`subheader-key-${subheader}`;
|
||||
|
@@ -3,7 +3,10 @@ import * as React from 'react';
|
||||
import { ListItem } from '../../../UI/List';
|
||||
import ListIcon from '../../../UI/ListIcon';
|
||||
import type { GroupWithContext } from '../../../ObjectsList/EnumerateObjects';
|
||||
import { getObjectOrObjectGroupListItemKey } from './Keys';
|
||||
import {
|
||||
getObjectGroupListItemKey,
|
||||
getObjectOrObjectGroupListItemValue,
|
||||
} from './Keys';
|
||||
|
||||
type Props = {|
|
||||
groupWithContext: GroupWithContext,
|
||||
@@ -21,11 +24,10 @@ export const renderGroupObjectsListItem = ({
|
||||
const groupName: string = groupWithContext.group.getName();
|
||||
return (
|
||||
<ListItem
|
||||
key={
|
||||
getObjectOrObjectGroupListItemKey(groupName) +
|
||||
(groupWithContext.global ? '-global' : '')
|
||||
key={getObjectGroupListItemKey(groupWithContext)}
|
||||
selected={
|
||||
selectedValue === getObjectOrObjectGroupListItemValue(groupName)
|
||||
}
|
||||
selected={selectedValue === getObjectOrObjectGroupListItemKey(groupName)}
|
||||
primaryText={groupName}
|
||||
leftIcon={
|
||||
<ListIcon
|
||||
|
@@ -3,7 +3,7 @@ import * as React from 'react';
|
||||
import { ListItem } from '../../../UI/List';
|
||||
import ListIcon from '../../../UI/ListIcon';
|
||||
import { type EnumeratedInstructionOrExpressionMetadata } from '../InstructionOrExpressionSelector/EnumeratedInstructionOrExpressionMetadata.js';
|
||||
import { getInstructionListItemKey } from './Keys';
|
||||
import { getInstructionListItemValue, getInstructionListItemKey } from './Keys';
|
||||
|
||||
type Props = {|
|
||||
instructionOrExpressionMetadata: EnumeratedInstructionOrExpressionMetadata,
|
||||
@@ -18,11 +18,13 @@ export const renderInstructionOrExpressionListItem = ({
|
||||
onClick,
|
||||
selectedValue,
|
||||
}: Props) => {
|
||||
const key = getInstructionListItemKey(instructionOrExpressionMetadata.type);
|
||||
return (
|
||||
<ListItem
|
||||
key={key}
|
||||
selected={selectedValue === key}
|
||||
key={getInstructionListItemKey(instructionOrExpressionMetadata)}
|
||||
selected={
|
||||
selectedValue ===
|
||||
getInstructionListItemValue(instructionOrExpressionMetadata.type)
|
||||
}
|
||||
primaryText={instructionOrExpressionMetadata.displayedName}
|
||||
secondaryText={instructionOrExpressionMetadata.fullGroupName}
|
||||
leftIcon={
|
||||
|
@@ -6,7 +6,7 @@ import { type InstructionOrExpressionTreeNode } from '../InstructionOrExpression
|
||||
import { type EnumeratedInstructionOrExpressionMetadata } from '../InstructionOrExpressionSelector/EnumeratedInstructionOrExpressionMetadata.js';
|
||||
import Subheader from '../../../UI/Subheader';
|
||||
import flatten from 'lodash/flatten';
|
||||
import { getSubheaderListItemKey, getInstructionListItemKey } from './Keys';
|
||||
import { getSubheaderListItemKey, getInstructionListItemValue } from './Keys';
|
||||
|
||||
type Props = {|
|
||||
instructionTreeNode: InstructionOrExpressionTreeNode,
|
||||
@@ -44,7 +44,7 @@ export const renderInstructionTree = ({
|
||||
if (typeof instructionOrGroup.type === 'string') {
|
||||
// $FlowFixMe - see above
|
||||
const instructionInformation: EnumeratedInstructionOrExpressionMetadata = instructionOrGroup;
|
||||
const value = getInstructionListItemKey(instructionOrGroup.type);
|
||||
const value = getInstructionListItemValue(instructionOrGroup.type);
|
||||
const selected = selectedValue === value;
|
||||
return (
|
||||
<ListItem
|
||||
|
@@ -4,7 +4,10 @@ import { ListItem } from '../../../UI/List';
|
||||
import ListIcon from '../../../UI/ListIcon';
|
||||
import ObjectsRenderingService from '../../../ObjectsRendering/ObjectsRenderingService';
|
||||
import type { ObjectWithContext } from '../../../ObjectsList/EnumerateObjects';
|
||||
import { getObjectOrObjectGroupListItemKey } from './Keys';
|
||||
import {
|
||||
getObjectOrObjectGroupListItemValue,
|
||||
getObjectListItemKey,
|
||||
} from './Keys';
|
||||
|
||||
type Props = {|
|
||||
project: gdProject,
|
||||
@@ -24,11 +27,10 @@ export const renderObjectListItem = ({
|
||||
const objectName: string = objectWithContext.object.getName();
|
||||
return (
|
||||
<ListItem
|
||||
key={
|
||||
getObjectOrObjectGroupListItemKey(objectName) +
|
||||
(objectWithContext.global ? '-global' : '')
|
||||
key={getObjectListItemKey(objectWithContext)}
|
||||
selected={
|
||||
selectedValue === getObjectOrObjectGroupListItemValue(objectName)
|
||||
}
|
||||
selected={selectedValue === getObjectOrObjectGroupListItemKey(objectName)}
|
||||
primaryText={objectName}
|
||||
leftIcon={
|
||||
<ListIcon
|
||||
|
@@ -22,6 +22,16 @@ import ClickAwayListener from '@material-ui/core/ClickAwayListener';
|
||||
import Paper from '@material-ui/core/Paper';
|
||||
const gd = global.gd;
|
||||
|
||||
export const textFieldRightButtonMargins = {
|
||||
marginTop: 17, //Properly align with the text field
|
||||
marginLeft: 10,
|
||||
};
|
||||
|
||||
export const textFieldWithLabelRightButtonMargins = {
|
||||
marginTop: 33, //Properly align with the text field
|
||||
marginLeft: 10,
|
||||
};
|
||||
|
||||
const styles = {
|
||||
container: {
|
||||
display: 'flex',
|
||||
@@ -36,7 +46,7 @@ const styles = {
|
||||
},
|
||||
expressionSelectorPopoverContent: {
|
||||
display: 'flex',
|
||||
maxHeight: 350,
|
||||
maxHeight: 250,
|
||||
},
|
||||
input: {
|
||||
fontFamily: '"Lucida Console", Monaco, monospace',
|
||||
@@ -48,14 +58,6 @@ const styles = {
|
||||
backgroundHighlightingWithDescription: {
|
||||
marginTop: 38, //Properly align with the text field
|
||||
},
|
||||
functionsButton: {
|
||||
marginTop: 17, //Properly align with the text field
|
||||
marginLeft: 10,
|
||||
},
|
||||
functionsButtonWithDescription: {
|
||||
marginTop: 33, //Properly align with the text field
|
||||
marginLeft: 10,
|
||||
},
|
||||
};
|
||||
|
||||
type State = {|
|
||||
@@ -165,22 +167,33 @@ export default class ExpressionField extends React.Component<Props, State> {
|
||||
|
||||
const functionCall = formatExpressionCall(expressionInfo, parameterValues);
|
||||
|
||||
// Generate the expression with the function call
|
||||
const { value } = this.props;
|
||||
const newValue =
|
||||
value.substr(0, cursorPosition) +
|
||||
functionCall +
|
||||
value.substr(cursorPosition);
|
||||
|
||||
// Apply changes
|
||||
if (this.props.onChange) this.props.onChange(newValue);
|
||||
this.setState(
|
||||
{
|
||||
validatedValue: newValue,
|
||||
},
|
||||
() => this._enqueueValidation()
|
||||
);
|
||||
|
||||
// Focus again and select what was just added.
|
||||
setTimeout(() => {
|
||||
if (this._field) this._field.focus();
|
||||
|
||||
setTimeout(() => {
|
||||
if (this._inputElement)
|
||||
if (this._inputElement) {
|
||||
this._inputElement.setSelectionRange(
|
||||
cursorPosition,
|
||||
cursorPosition + functionCall.length
|
||||
);
|
||||
}
|
||||
}, 5);
|
||||
}, 5);
|
||||
};
|
||||
@@ -252,6 +265,7 @@ export default class ExpressionField extends React.Component<Props, State> {
|
||||
|
||||
const popoverStyle = {
|
||||
width: this._fieldElement ? this._fieldElement.clientWidth : 'auto',
|
||||
marginLeft: -5, // Remove the offset that the Popper has for some reason with disablePortal={true}
|
||||
// Ensure the popper is above everything (modal, dialog, snackbar, tooltips, etc).
|
||||
// There will be only one ExpressionSelector opened at a time, so it's fair to put the
|
||||
// highest z index. If this is breaking, check the z-index of material-ui.
|
||||
@@ -292,6 +306,9 @@ export default class ExpressionField extends React.Component<Props, State> {
|
||||
open={this.state.popoverOpen}
|
||||
anchorEl={this._fieldElement}
|
||||
placement="bottom"
|
||||
disablePortal={
|
||||
true /* Can't use portals as this would put the Popper outside of the Modal, which is keeping the focus in the modal (so the search bar and keyboard browsing won't not work) */
|
||||
}
|
||||
>
|
||||
<Paper style={styles.expressionSelectorPopoverContent}>
|
||||
<ExpressionSelector
|
||||
@@ -312,8 +329,8 @@ export default class ExpressionField extends React.Component<Props, State> {
|
||||
this.props.renderExtraButton &&
|
||||
this.props.renderExtraButton({
|
||||
style: description
|
||||
? styles.functionsButtonWithDescription
|
||||
: styles.functionsButton,
|
||||
? textFieldWithLabelRightButtonMargins
|
||||
: textFieldRightButtonMargins,
|
||||
})}
|
||||
{!this.props.isInline && (
|
||||
<RaisedButton
|
||||
@@ -328,8 +345,8 @@ export default class ExpressionField extends React.Component<Props, State> {
|
||||
primary
|
||||
style={
|
||||
description
|
||||
? styles.functionsButtonWithDescription
|
||||
: styles.functionsButton
|
||||
? textFieldWithLabelRightButtonMargins
|
||||
: textFieldRightButtonMargins
|
||||
}
|
||||
onClick={this._openExpressionPopover}
|
||||
/>
|
||||
|
@@ -7,14 +7,15 @@ import { type ParameterFieldProps } from './ParameterFieldCommons';
|
||||
import classNames from 'classnames';
|
||||
import { icon } from '../EventsTree/ClassNames';
|
||||
import SemiControlledAutoComplete from '../../UI/SemiControlledAutoComplete';
|
||||
import {
|
||||
textFieldRightButtonMargins,
|
||||
textFieldWithLabelRightButtonMargins,
|
||||
} from './GenericExpressionField';
|
||||
|
||||
const styles = {
|
||||
container: {
|
||||
display: 'flex',
|
||||
alignItems: 'baseline',
|
||||
},
|
||||
moreButton: {
|
||||
marginLeft: 10,
|
||||
alignItems: 'flex-start',
|
||||
},
|
||||
};
|
||||
|
||||
@@ -41,12 +42,14 @@ export default class VariableField extends Component<Props, {||}> {
|
||||
variablesContainer,
|
||||
} = this.props;
|
||||
|
||||
const description = parameterMetadata
|
||||
? parameterMetadata.getDescription()
|
||||
: undefined;
|
||||
|
||||
return (
|
||||
<div style={styles.container}>
|
||||
<SemiControlledAutoComplete
|
||||
floatingLabelText={
|
||||
parameterMetadata ? parameterMetadata.getDescription() : undefined
|
||||
}
|
||||
floatingLabelText={description}
|
||||
fullWidth
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
@@ -64,7 +67,11 @@ export default class VariableField extends Component<Props, {||}> {
|
||||
icon={<OpenInNew />}
|
||||
disabled={!this.props.variablesContainer}
|
||||
primary
|
||||
style={styles.moreButton}
|
||||
style={
|
||||
description
|
||||
? textFieldWithLabelRightButtonMargins
|
||||
: textFieldRightButtonMargins
|
||||
}
|
||||
onClick={onOpenDialog}
|
||||
/>
|
||||
)}
|
||||
|
@@ -154,19 +154,22 @@ export default class EventsSheet extends React.Component<Props, State> {
|
||||
_eventsTree: ?EventsTree;
|
||||
_eventSearcher: ?EventsSearcher;
|
||||
_searchPanel: ?SearchPanel;
|
||||
_containerDiv = React.createRef<HTMLDivElement>();
|
||||
_keyboardShortcuts = new KeyboardShortcuts({
|
||||
onDelete: () => {
|
||||
if (this.state.inlineEditing || this.state.editedInstruction.instruction)
|
||||
return;
|
||||
|
||||
this.deleteSelection();
|
||||
isActive: () =>
|
||||
!this.state.inlineEditing &&
|
||||
!this.state.editedInstruction.instruction &&
|
||||
!this.state.analyzedEventsContextResult &&
|
||||
!this.state.serializedEventsToExtract,
|
||||
shortcutCallbacks: {
|
||||
onDelete: () => this.deleteSelection(),
|
||||
onCopy: () => this.copySelection(),
|
||||
onCut: () => this.cutSelection(),
|
||||
onPaste: () => this.pasteEventsOrInstructions(),
|
||||
onSearch: () => this._toggleSearchPanel(),
|
||||
onUndo: () => this.undo(),
|
||||
onRedo: () => this.redo(),
|
||||
},
|
||||
onCopy: () => this.copySelection(),
|
||||
onCut: () => this.cutSelection(),
|
||||
onPaste: () => this.pasteEventsOrInstructions(),
|
||||
onSearch: () => this._toggleSearchPanel(),
|
||||
onUndo: () => this.undo(),
|
||||
onRedo: () => this.redo(),
|
||||
});
|
||||
|
||||
eventContextMenu: ContextMenu;
|
||||
@@ -210,10 +213,6 @@ export default class EventsSheet extends React.Component<Props, State> {
|
||||
this.setState({ allEventsMetadata: enumerateEventsMetadata() });
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
this._keyboardShortcuts.unmount();
|
||||
}
|
||||
|
||||
updateToolbar() {
|
||||
if (!this.props.setToolbar) return;
|
||||
|
||||
@@ -592,7 +591,14 @@ export default class EventsSheet extends React.Component<Props, State> {
|
||||
inlineEditing: false,
|
||||
inlineEditingAnchorEl: null,
|
||||
},
|
||||
() => this._saveChangesToHistory()
|
||||
() => {
|
||||
this._saveChangesToHistory();
|
||||
|
||||
// Deletion of an event/instruction will remove it from the DOM,
|
||||
// potentially losing the focus on the associated DOM elements. Ensure
|
||||
// we keep the focus on the EventsSheet.
|
||||
this._ensureFocused();
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
@@ -915,6 +921,34 @@ export default class EventsSheet extends React.Component<Props, State> {
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Call this to ensure that the events sheet stays focused after a potential
|
||||
* lost of focus (for example, after a scroll, the focused element might have
|
||||
* been scrolled out of the view and so removed from the DOM)
|
||||
*/
|
||||
_ensureFocused = () => {
|
||||
if (!this._containerDiv || !document) return;
|
||||
|
||||
const containerDivElement = this._containerDiv.current;
|
||||
if (document.activeElement === containerDivElement) {
|
||||
// Focus is already on the container
|
||||
return;
|
||||
}
|
||||
if (containerDivElement) {
|
||||
if (
|
||||
document.activeElement !== document.body &&
|
||||
containerDivElement.contains(document.activeElement)
|
||||
) {
|
||||
// Focus is already on an element of the container
|
||||
return;
|
||||
}
|
||||
|
||||
// Focus is not on an element of the container, we probably lost the focus
|
||||
// after scrolling or removing an element. Give back the focus to the container.
|
||||
containerDivElement.focus();
|
||||
}
|
||||
};
|
||||
|
||||
render() {
|
||||
const {
|
||||
project,
|
||||
@@ -953,13 +987,16 @@ export default class EventsSheet extends React.Component<Props, State> {
|
||||
<div
|
||||
className="gd-events-sheet"
|
||||
style={styles.container}
|
||||
onFocus={() => this._keyboardShortcuts.focus()}
|
||||
onBlur={() => this._keyboardShortcuts.blur()}
|
||||
tabIndex={1}
|
||||
onKeyDown={this._keyboardShortcuts.onKeyDown}
|
||||
onKeyUp={this._keyboardShortcuts.onKeyUp}
|
||||
onDragOver={this._keyboardShortcuts.onDragOver}
|
||||
ref={this._containerDiv}
|
||||
tabIndex={0}
|
||||
>
|
||||
<EventsTree
|
||||
ref={eventsTree => (this._eventsTree = eventsTree)}
|
||||
key={events.ptr}
|
||||
onScroll={this._ensureFocused}
|
||||
events={events}
|
||||
project={project}
|
||||
scope={scope}
|
||||
|
178
newIDE/app/src/Export/BrowserExporters/BrowserCocos2dExport.js
Normal file
@@ -0,0 +1,178 @@
|
||||
// @flow
|
||||
import * as React from 'react';
|
||||
import { Trans } from '@lingui/macro';
|
||||
import assignIn from 'lodash/assignIn';
|
||||
import { findGDJS } from './BrowserS3GDJSFinder';
|
||||
import BrowserFileSystem from './BrowserFileSystem';
|
||||
import {
|
||||
type UrlFileDescriptor,
|
||||
type TextFileDescriptor,
|
||||
type BlobFileDescriptor,
|
||||
downloadUrlsToBlobs,
|
||||
archiveFiles,
|
||||
} from '../../Utils/BrowserArchiver';
|
||||
import {
|
||||
type ExportPipeline,
|
||||
type ExportPipelineContext,
|
||||
} from '../ExportPipeline.flow';
|
||||
import RaisedButton from '../../UI/RaisedButton';
|
||||
import { BlobDownloadUrlHolder } from '../../Utils/BlobDownloadUrlHolder';
|
||||
import {
|
||||
ExplanationHeader,
|
||||
DoneFooter,
|
||||
} from '../GenericExporters/Cocos2dExport';
|
||||
import { openBlobDownloadUrl } from '.';
|
||||
import { Column, Line } from '../../UI/Grid';
|
||||
import Toggle from '../../UI/Toggle';
|
||||
const gd = global.gd;
|
||||
|
||||
type ExportState = {
|
||||
debugMode: boolean,
|
||||
};
|
||||
|
||||
type PreparedExporter = {|
|
||||
exporter: gdjsExporter,
|
||||
abstractFileSystem: BrowserFileSystem,
|
||||
outputDir: string,
|
||||
|};
|
||||
|
||||
type ExportOutput = {|
|
||||
textFiles: Array<TextFileDescriptor>,
|
||||
urlFiles: Array<UrlFileDescriptor>,
|
||||
|};
|
||||
|
||||
type ResourcesDownloadOutput = {|
|
||||
textFiles: Array<TextFileDescriptor>,
|
||||
blobFiles: Array<BlobFileDescriptor>,
|
||||
|};
|
||||
|
||||
type CompressionOutput = Blob;
|
||||
|
||||
export const browserCocos2dExportPipeline: ExportPipeline<
|
||||
ExportState,
|
||||
PreparedExporter,
|
||||
ExportOutput,
|
||||
ResourcesDownloadOutput,
|
||||
CompressionOutput
|
||||
> = {
|
||||
name: 'browser-cocos2d',
|
||||
|
||||
getInitialExportState: () => ({
|
||||
debugMode: false,
|
||||
}),
|
||||
|
||||
canLaunchBuild: () => true,
|
||||
|
||||
renderHeader: ({ project, exportState, updateExportState }) => (
|
||||
<Column noMargin>
|
||||
<Line>
|
||||
<ExplanationHeader />
|
||||
</Line>
|
||||
<Line>
|
||||
<Toggle
|
||||
onToggle={(e, check) =>
|
||||
updateExportState(prevState => ({
|
||||
...prevState,
|
||||
debugMode: check,
|
||||
}))
|
||||
}
|
||||
toggled={exportState.debugMode}
|
||||
labelPosition="right"
|
||||
label={
|
||||
<Trans>
|
||||
Debug mode (show FPS counter and stats in the bottom left)
|
||||
</Trans>
|
||||
}
|
||||
/>
|
||||
</Line>
|
||||
</Column>
|
||||
),
|
||||
|
||||
renderLaunchButtonLabel: () => <Trans>Package</Trans>,
|
||||
|
||||
prepareExporter: (
|
||||
context: ExportPipelineContext<ExportState>
|
||||
): Promise<PreparedExporter> => {
|
||||
return findGDJS('cocos2d-js').then(({ gdjsRoot, filesContent }) => {
|
||||
console.info('GDJS found in ', gdjsRoot);
|
||||
|
||||
const outputDir = '/export/';
|
||||
const abstractFileSystem = new BrowserFileSystem({
|
||||
textFiles: filesContent,
|
||||
});
|
||||
// TODO: Memory leak? Check for other exporters too.
|
||||
const fileSystem = assignIn(
|
||||
new gd.AbstractFileSystemJS(),
|
||||
abstractFileSystem
|
||||
);
|
||||
const exporter = new gd.Exporter(fileSystem, gdjsRoot);
|
||||
|
||||
return {
|
||||
exporter,
|
||||
outputDir,
|
||||
abstractFileSystem,
|
||||
};
|
||||
});
|
||||
},
|
||||
|
||||
launchExport: (
|
||||
context: ExportPipelineContext<ExportState>,
|
||||
{ exporter, outputDir, abstractFileSystem }: PreparedExporter
|
||||
): Promise<ExportOutput> => {
|
||||
exporter.exportWholeCocos2dProject(
|
||||
context.project,
|
||||
context.exportState.debugMode,
|
||||
outputDir
|
||||
);
|
||||
exporter.delete();
|
||||
|
||||
return Promise.resolve({
|
||||
textFiles: abstractFileSystem.getAllTextFilesIn(outputDir),
|
||||
urlFiles: abstractFileSystem.getAllUrlFilesIn(outputDir),
|
||||
});
|
||||
},
|
||||
|
||||
launchResourcesDownload: (
|
||||
context: ExportPipelineContext<ExportState>,
|
||||
{ textFiles, urlFiles }: ExportOutput
|
||||
): Promise<ResourcesDownloadOutput> => {
|
||||
return downloadUrlsToBlobs({
|
||||
urlFiles,
|
||||
onProgress: context.updateStepProgress,
|
||||
}).then(blobFiles => ({
|
||||
blobFiles,
|
||||
textFiles,
|
||||
}));
|
||||
},
|
||||
|
||||
launchCompression: (
|
||||
context: ExportPipelineContext<ExportState>,
|
||||
{ textFiles, blobFiles }: ResourcesDownloadOutput
|
||||
): Promise<Blob> => {
|
||||
return archiveFiles({
|
||||
blobFiles,
|
||||
textFiles,
|
||||
basePath: '/export/',
|
||||
onProgress: context.updateStepProgress,
|
||||
});
|
||||
},
|
||||
|
||||
renderDoneFooter: ({ compressionOutput, exportState, onClose }) => {
|
||||
return (
|
||||
<DoneFooter
|
||||
renderGameButton={() => (
|
||||
<BlobDownloadUrlHolder blob={compressionOutput}>
|
||||
{blobDownloadUrl => (
|
||||
<RaisedButton
|
||||
fullWidth
|
||||
primary
|
||||
onClick={() => openBlobDownloadUrl(blobDownloadUrl, 'game.zip')}
|
||||
label={<Trans>Download the exported game</Trans>}
|
||||
/>
|
||||
)}
|
||||
</BlobDownloadUrlHolder>
|
||||
)}
|
||||
/>
|
||||
);
|
||||
},
|
||||
};
|
150
newIDE/app/src/Export/BrowserExporters/BrowserCordovaExport.js
Normal file
@@ -0,0 +1,150 @@
|
||||
// @flow
|
||||
import * as React from 'react';
|
||||
import { Trans } from '@lingui/macro';
|
||||
import assignIn from 'lodash/assignIn';
|
||||
import { findGDJS } from './BrowserS3GDJSFinder';
|
||||
import BrowserFileSystem from './BrowserFileSystem';
|
||||
import {
|
||||
type UrlFileDescriptor,
|
||||
type TextFileDescriptor,
|
||||
type BlobFileDescriptor,
|
||||
downloadUrlsToBlobs,
|
||||
archiveFiles,
|
||||
} from '../../Utils/BrowserArchiver';
|
||||
import {
|
||||
type ExportPipeline,
|
||||
type ExportPipelineContext,
|
||||
} from '../ExportPipeline.flow';
|
||||
import RaisedButton from '../../UI/RaisedButton';
|
||||
import { BlobDownloadUrlHolder } from '../../Utils/BlobDownloadUrlHolder';
|
||||
import {
|
||||
ExplanationHeader,
|
||||
DoneFooter,
|
||||
} from '../GenericExporters/CordovaExport';
|
||||
import { openBlobDownloadUrl } from '.';
|
||||
const gd = global.gd;
|
||||
|
||||
type ExportState = null;
|
||||
|
||||
type PreparedExporter = {|
|
||||
exporter: gdjsExporter,
|
||||
abstractFileSystem: BrowserFileSystem,
|
||||
outputDir: string,
|
||||
|};
|
||||
|
||||
type ExportOutput = {|
|
||||
textFiles: Array<TextFileDescriptor>,
|
||||
urlFiles: Array<UrlFileDescriptor>,
|
||||
|};
|
||||
|
||||
type ResourcesDownloadOutput = {|
|
||||
textFiles: Array<TextFileDescriptor>,
|
||||
blobFiles: Array<BlobFileDescriptor>,
|
||||
|};
|
||||
|
||||
type CompressionOutput = Blob;
|
||||
|
||||
export const browserCordovaExportPipeline: ExportPipeline<
|
||||
ExportState,
|
||||
PreparedExporter,
|
||||
ExportOutput,
|
||||
ResourcesDownloadOutput,
|
||||
CompressionOutput
|
||||
> = {
|
||||
name: 'browser-cordova',
|
||||
|
||||
getInitialExportState: () => null,
|
||||
|
||||
canLaunchBuild: () => true,
|
||||
|
||||
renderHeader: () => <ExplanationHeader />,
|
||||
|
||||
renderLaunchButtonLabel: () => <Trans>Package</Trans>,
|
||||
|
||||
prepareExporter: (
|
||||
context: ExportPipelineContext<ExportState>
|
||||
): Promise<PreparedExporter> => {
|
||||
return findGDJS('cordova').then(({ gdjsRoot, filesContent }) => {
|
||||
console.info('GDJS found in ', gdjsRoot);
|
||||
|
||||
const outputDir = '/export/';
|
||||
const abstractFileSystem = new BrowserFileSystem({
|
||||
textFiles: filesContent,
|
||||
});
|
||||
// TODO: Memory leak? Check for other exporters too.
|
||||
const fileSystem = assignIn(
|
||||
new gd.AbstractFileSystemJS(),
|
||||
abstractFileSystem
|
||||
);
|
||||
const exporter = new gd.Exporter(fileSystem, gdjsRoot);
|
||||
|
||||
return {
|
||||
exporter,
|
||||
outputDir,
|
||||
abstractFileSystem,
|
||||
};
|
||||
});
|
||||
},
|
||||
|
||||
launchExport: (
|
||||
context: ExportPipelineContext<ExportState>,
|
||||
{ exporter, outputDir, abstractFileSystem }: PreparedExporter
|
||||
): Promise<ExportOutput> => {
|
||||
const { project } = context;
|
||||
|
||||
const exportOptions = new gd.MapStringBoolean();
|
||||
exportOptions.set('exportForCordova', true);
|
||||
exporter.exportWholePixiProject(project, outputDir, exportOptions);
|
||||
exportOptions.delete();
|
||||
exporter.delete();
|
||||
|
||||
return Promise.resolve({
|
||||
textFiles: abstractFileSystem.getAllTextFilesIn(outputDir),
|
||||
urlFiles: abstractFileSystem.getAllUrlFilesIn(outputDir),
|
||||
});
|
||||
},
|
||||
|
||||
launchResourcesDownload: (
|
||||
context: ExportPipelineContext<ExportState>,
|
||||
{ textFiles, urlFiles }: ExportOutput
|
||||
): Promise<ResourcesDownloadOutput> => {
|
||||
return downloadUrlsToBlobs({
|
||||
urlFiles,
|
||||
onProgress: context.updateStepProgress,
|
||||
}).then(blobFiles => ({
|
||||
blobFiles,
|
||||
textFiles,
|
||||
}));
|
||||
},
|
||||
|
||||
launchCompression: (
|
||||
context: ExportPipelineContext<ExportState>,
|
||||
{ textFiles, blobFiles }: ResourcesDownloadOutput
|
||||
): Promise<Blob> => {
|
||||
return archiveFiles({
|
||||
blobFiles,
|
||||
textFiles,
|
||||
basePath: '/export/',
|
||||
onProgress: context.updateStepProgress,
|
||||
});
|
||||
},
|
||||
|
||||
renderDoneFooter: ({ compressionOutput, exportState, onClose }) => {
|
||||
return (
|
||||
<DoneFooter
|
||||
renderGameButton={() => (
|
||||
<BlobDownloadUrlHolder blob={compressionOutput}>
|
||||
{blobDownloadUrl => (
|
||||
<RaisedButton
|
||||
fullWidth
|
||||
primary
|
||||
onClick={() => openBlobDownloadUrl(blobDownloadUrl, 'game.zip')}
|
||||
label={<Trans>Download the exported game</Trans>}
|
||||
/>
|
||||
)}
|
||||
</BlobDownloadUrlHolder>
|
||||
)}
|
||||
/>
|
||||
);
|
||||
},
|
||||
};
|
150
newIDE/app/src/Export/BrowserExporters/BrowserElectronExport.js
Normal file
@@ -0,0 +1,150 @@
|
||||
// @flow
|
||||
import * as React from 'react';
|
||||
import { Trans } from '@lingui/macro';
|
||||
import assignIn from 'lodash/assignIn';
|
||||
import { findGDJS } from './BrowserS3GDJSFinder';
|
||||
import BrowserFileSystem from './BrowserFileSystem';
|
||||
import {
|
||||
type UrlFileDescriptor,
|
||||
type TextFileDescriptor,
|
||||
type BlobFileDescriptor,
|
||||
downloadUrlsToBlobs,
|
||||
archiveFiles,
|
||||
} from '../../Utils/BrowserArchiver';
|
||||
import {
|
||||
type ExportPipeline,
|
||||
type ExportPipelineContext,
|
||||
} from '../ExportPipeline.flow';
|
||||
import RaisedButton from '../../UI/RaisedButton';
|
||||
import { BlobDownloadUrlHolder } from '../../Utils/BlobDownloadUrlHolder';
|
||||
import {
|
||||
ExplanationHeader,
|
||||
DoneFooter,
|
||||
} from '../GenericExporters/ElectronExport';
|
||||
import { openBlobDownloadUrl } from '.';
|
||||
const gd = global.gd;
|
||||
|
||||
type ExportState = null;
|
||||
|
||||
type PreparedExporter = {|
|
||||
exporter: gdjsExporter,
|
||||
abstractFileSystem: BrowserFileSystem,
|
||||
outputDir: string,
|
||||
|};
|
||||
|
||||
type ExportOutput = {|
|
||||
textFiles: Array<TextFileDescriptor>,
|
||||
urlFiles: Array<UrlFileDescriptor>,
|
||||
|};
|
||||
|
||||
type ResourcesDownloadOutput = {|
|
||||
textFiles: Array<TextFileDescriptor>,
|
||||
blobFiles: Array<BlobFileDescriptor>,
|
||||
|};
|
||||
|
||||
type CompressionOutput = Blob;
|
||||
|
||||
export const browserElectronExportPipeline: ExportPipeline<
|
||||
ExportState,
|
||||
PreparedExporter,
|
||||
ExportOutput,
|
||||
ResourcesDownloadOutput,
|
||||
CompressionOutput
|
||||
> = {
|
||||
name: 'browser-electron',
|
||||
|
||||
getInitialExportState: () => null,
|
||||
|
||||
canLaunchBuild: () => true,
|
||||
|
||||
renderHeader: () => <ExplanationHeader />,
|
||||
|
||||
renderLaunchButtonLabel: () => <Trans>Package</Trans>,
|
||||
|
||||
prepareExporter: (
|
||||
context: ExportPipelineContext<ExportState>
|
||||
): Promise<PreparedExporter> => {
|
||||
return findGDJS('electron').then(({ gdjsRoot, filesContent }) => {
|
||||
console.info('GDJS found in ', gdjsRoot);
|
||||
|
||||
const outputDir = '/export/';
|
||||
const abstractFileSystem = new BrowserFileSystem({
|
||||
textFiles: filesContent,
|
||||
});
|
||||
// TODO: Memory leak? Check for other exporters too.
|
||||
const fileSystem = assignIn(
|
||||
new gd.AbstractFileSystemJS(),
|
||||
abstractFileSystem
|
||||
);
|
||||
const exporter = new gd.Exporter(fileSystem, gdjsRoot);
|
||||
|
||||
return {
|
||||
exporter,
|
||||
outputDir,
|
||||
abstractFileSystem,
|
||||
};
|
||||
});
|
||||
},
|
||||
|
||||
launchExport: (
|
||||
context: ExportPipelineContext<ExportState>,
|
||||
{ exporter, outputDir, abstractFileSystem }: PreparedExporter
|
||||
): Promise<ExportOutput> => {
|
||||
const { project } = context;
|
||||
|
||||
const exportOptions = new gd.MapStringBoolean();
|
||||
exportOptions.set('exportForElectron', true);
|
||||
exporter.exportWholePixiProject(project, outputDir, exportOptions);
|
||||
exportOptions.delete();
|
||||
exporter.delete();
|
||||
|
||||
return Promise.resolve({
|
||||
textFiles: abstractFileSystem.getAllTextFilesIn(outputDir),
|
||||
urlFiles: abstractFileSystem.getAllUrlFilesIn(outputDir),
|
||||
});
|
||||
},
|
||||
|
||||
launchResourcesDownload: (
|
||||
context: ExportPipelineContext<ExportState>,
|
||||
{ textFiles, urlFiles }: ExportOutput
|
||||
): Promise<ResourcesDownloadOutput> => {
|
||||
return downloadUrlsToBlobs({
|
||||
urlFiles,
|
||||
onProgress: context.updateStepProgress,
|
||||
}).then(blobFiles => ({
|
||||
blobFiles,
|
||||
textFiles,
|
||||
}));
|
||||
},
|
||||
|
||||
launchCompression: (
|
||||
context: ExportPipelineContext<ExportState>,
|
||||
{ textFiles, blobFiles }: ResourcesDownloadOutput
|
||||
): Promise<Blob> => {
|
||||
return archiveFiles({
|
||||
blobFiles,
|
||||
textFiles,
|
||||
basePath: '/export/',
|
||||
onProgress: context.updateStepProgress,
|
||||
});
|
||||
},
|
||||
|
||||
renderDoneFooter: ({ compressionOutput, exportState, onClose }) => {
|
||||
return (
|
||||
<DoneFooter
|
||||
renderGameButton={() => (
|
||||
<BlobDownloadUrlHolder blob={compressionOutput}>
|
||||
{blobDownloadUrl => (
|
||||
<RaisedButton
|
||||
fullWidth
|
||||
primary
|
||||
onClick={() => openBlobDownloadUrl(blobDownloadUrl, 'game.zip')}
|
||||
label={<Trans>Download the exported game</Trans>}
|
||||
/>
|
||||
)}
|
||||
</BlobDownloadUrlHolder>
|
||||
)}
|
||||
/>
|
||||
);
|
||||
},
|
||||
};
|
@@ -1,37 +0,0 @@
|
||||
import { Trans } from '@lingui/macro';
|
||||
import React, { Component } from 'react';
|
||||
import RaisedButton from '../../UI/RaisedButton';
|
||||
import Text from '../../UI/Text';
|
||||
import { Column, Line } from '../../UI/Grid';
|
||||
import Window from '../../Utils/Window';
|
||||
|
||||
export default class BrowserExport extends Component {
|
||||
openWebsite = () => {
|
||||
Window.openExternalURL('http://gdevelop-app.com');
|
||||
};
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div style={{ height: 200 }}>
|
||||
<Column>
|
||||
<Line>
|
||||
<Text>
|
||||
<Trans>
|
||||
Export is not yet available in GDevelop online version. Instead,
|
||||
download the full GDevelop desktop version to export and publish
|
||||
your game!
|
||||
</Trans>
|
||||
</Text>
|
||||
</Line>
|
||||
<Line justifyContent="center">
|
||||
<RaisedButton
|
||||
onClick={this.openWebsite}
|
||||
primary
|
||||
label={<Trans>Download GDevelop</Trans>}
|
||||
/>
|
||||
</Line>
|
||||
</Column>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
@@ -0,0 +1,153 @@
|
||||
// @flow
|
||||
import * as React from 'react';
|
||||
import { Trans } from '@lingui/macro';
|
||||
import assignIn from 'lodash/assignIn';
|
||||
import { findGDJS } from './BrowserS3GDJSFinder';
|
||||
import BrowserFileSystem from './BrowserFileSystem';
|
||||
import {
|
||||
type UrlFileDescriptor,
|
||||
type TextFileDescriptor,
|
||||
type BlobFileDescriptor,
|
||||
downloadUrlsToBlobs,
|
||||
archiveFiles,
|
||||
} from '../../Utils/BrowserArchiver';
|
||||
import {
|
||||
type ExportPipeline,
|
||||
type ExportPipelineContext,
|
||||
} from '../ExportPipeline.flow';
|
||||
import RaisedButton from '../../UI/RaisedButton';
|
||||
import { BlobDownloadUrlHolder } from '../../Utils/BlobDownloadUrlHolder';
|
||||
import {
|
||||
ExplanationHeader,
|
||||
DoneFooter,
|
||||
} from '../GenericExporters/FacebookInstantGamesExport';
|
||||
import { openBlobDownloadUrl } from '.';
|
||||
const gd = global.gd;
|
||||
|
||||
type ExportState = null;
|
||||
|
||||
type PreparedExporter = {|
|
||||
exporter: gdjsExporter,
|
||||
abstractFileSystem: BrowserFileSystem,
|
||||
outputDir: string,
|
||||
|};
|
||||
|
||||
type ExportOutput = {|
|
||||
textFiles: Array<TextFileDescriptor>,
|
||||
urlFiles: Array<UrlFileDescriptor>,
|
||||
|};
|
||||
|
||||
type ResourcesDownloadOutput = {|
|
||||
textFiles: Array<TextFileDescriptor>,
|
||||
blobFiles: Array<BlobFileDescriptor>,
|
||||
|};
|
||||
|
||||
type CompressionOutput = Blob;
|
||||
|
||||
export const browserFacebookInstantGamesExportPipeline: ExportPipeline<
|
||||
ExportState,
|
||||
PreparedExporter,
|
||||
ExportOutput,
|
||||
ResourcesDownloadOutput,
|
||||
CompressionOutput
|
||||
> = {
|
||||
name: 'browser-facebook-instant-games',
|
||||
|
||||
getInitialExportState: () => null,
|
||||
|
||||
canLaunchBuild: () => true,
|
||||
|
||||
renderHeader: () => <ExplanationHeader />,
|
||||
|
||||
renderLaunchButtonLabel: () => <Trans>Package</Trans>,
|
||||
|
||||
prepareExporter: (
|
||||
context: ExportPipelineContext<ExportState>
|
||||
): Promise<PreparedExporter> => {
|
||||
return findGDJS('facebook-instant-games').then(
|
||||
({ gdjsRoot, filesContent }) => {
|
||||
console.info('GDJS found in ', gdjsRoot);
|
||||
|
||||
const outputDir = '/export/';
|
||||
const abstractFileSystem = new BrowserFileSystem({
|
||||
textFiles: filesContent,
|
||||
});
|
||||
// TODO: Memory leak? Check for other exporters too.
|
||||
const fileSystem = assignIn(
|
||||
new gd.AbstractFileSystemJS(),
|
||||
abstractFileSystem
|
||||
);
|
||||
const exporter = new gd.Exporter(fileSystem, gdjsRoot);
|
||||
|
||||
return {
|
||||
exporter,
|
||||
outputDir,
|
||||
abstractFileSystem,
|
||||
};
|
||||
}
|
||||
);
|
||||
},
|
||||
|
||||
launchExport: (
|
||||
context: ExportPipelineContext<ExportState>,
|
||||
{ exporter, outputDir, abstractFileSystem }: PreparedExporter
|
||||
): Promise<ExportOutput> => {
|
||||
const { project } = context;
|
||||
|
||||
const exportOptions = new gd.MapStringBoolean();
|
||||
exportOptions.set('exportForFacebookInstantGames', true);
|
||||
exporter.exportWholePixiProject(project, outputDir, exportOptions);
|
||||
exportOptions.delete();
|
||||
exporter.delete();
|
||||
|
||||
return Promise.resolve({
|
||||
textFiles: abstractFileSystem.getAllTextFilesIn(outputDir),
|
||||
urlFiles: abstractFileSystem.getAllUrlFilesIn(outputDir),
|
||||
});
|
||||
},
|
||||
|
||||
launchResourcesDownload: (
|
||||
context: ExportPipelineContext<ExportState>,
|
||||
{ textFiles, urlFiles }: ExportOutput
|
||||
): Promise<ResourcesDownloadOutput> => {
|
||||
return downloadUrlsToBlobs({
|
||||
urlFiles,
|
||||
onProgress: context.updateStepProgress,
|
||||
}).then(blobFiles => ({
|
||||
blobFiles,
|
||||
textFiles,
|
||||
}));
|
||||
},
|
||||
|
||||
launchCompression: (
|
||||
context: ExportPipelineContext<ExportState>,
|
||||
{ textFiles, blobFiles }: ResourcesDownloadOutput
|
||||
): Promise<Blob> => {
|
||||
return archiveFiles({
|
||||
blobFiles,
|
||||
textFiles,
|
||||
basePath: '/export/',
|
||||
onProgress: context.updateStepProgress,
|
||||
});
|
||||
},
|
||||
|
||||
renderDoneFooter: ({ compressionOutput, exportState, onClose }) => {
|
||||
return (
|
||||
<DoneFooter
|
||||
renderGameButton={() => (
|
||||
<BlobDownloadUrlHolder blob={compressionOutput}>
|
||||
{blobDownloadUrl => (
|
||||
<RaisedButton
|
||||
primary
|
||||
onClick={() =>
|
||||
openBlobDownloadUrl(blobDownloadUrl, 'fb-instant-game.zip')
|
||||
}
|
||||
label={<Trans>Download the Instant Game archive</Trans>}
|
||||
/>
|
||||
)}
|
||||
</BlobDownloadUrlHolder>
|
||||
)}
|
||||
/>
|
||||
);
|
||||
},
|
||||
};
|
213
newIDE/app/src/Export/BrowserExporters/BrowserFileSystem.js
Normal file
@@ -0,0 +1,213 @@
|
||||
// @flow
|
||||
import path from 'path';
|
||||
const gd = global.gd;
|
||||
|
||||
export type BlobFileDescriptor = {|
|
||||
filePath: string,
|
||||
blob: Blob,
|
||||
|};
|
||||
|
||||
export type TextFileDescriptor = {|
|
||||
filePath: string,
|
||||
text: string,
|
||||
|};
|
||||
|
||||
export type UrlFileDescriptor = {|
|
||||
filePath: string,
|
||||
url: string,
|
||||
|};
|
||||
|
||||
type ConstructorArgs = {|
|
||||
textFiles: Array<TextFileDescriptor>,
|
||||
|};
|
||||
|
||||
const isURL = (filename: string) => {
|
||||
return (
|
||||
filename.substr(0, 7) === 'http://' ||
|
||||
filename.substr(0, 8) === 'https://' ||
|
||||
filename.substr(0, 6) === 'ftp://'
|
||||
);
|
||||
};
|
||||
|
||||
// TODO: Merge BrowserS3FileSystem into this?
|
||||
|
||||
/**
|
||||
* An in-memory "file system" that can be used for GDevelop exports.
|
||||
*/
|
||||
export default class BrowserFileSystem {
|
||||
// The representation of the "file system":
|
||||
|
||||
/**
|
||||
* Store all the text files (filepath => content)
|
||||
* @private
|
||||
*/
|
||||
_textFiles: { [string]: string } = {};
|
||||
|
||||
/**
|
||||
* Store all the files that should be downloaded (filepath => url)
|
||||
* @private
|
||||
*/
|
||||
_filesToDownload: { [string]: string } = {};
|
||||
|
||||
/**
|
||||
* Create a new in-memory file system.
|
||||
*/
|
||||
constructor({ textFiles }: ConstructorArgs) {
|
||||
textFiles.forEach(textFileDescriptor => {
|
||||
this._textFiles[textFileDescriptor.filePath] = textFileDescriptor.text;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns all the in memory text files with the specified path prefix.
|
||||
*/
|
||||
getAllTextFilesIn = (pathPrefix: string): Array<TextFileDescriptor> => {
|
||||
return Object.keys(this._textFiles)
|
||||
.filter(filePath => filePath.indexOf(pathPrefix) === 0)
|
||||
.map(filePath => ({
|
||||
filePath,
|
||||
text: this._textFiles[filePath],
|
||||
}));
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns all the files that should be downloaded from a URL, with the specified path prefix.
|
||||
*/
|
||||
getAllUrlFilesIn = (pathPrefix: string): Array<UrlFileDescriptor> => {
|
||||
return Object.keys(this._filesToDownload)
|
||||
.filter(filePath => filePath.indexOf(pathPrefix) === 0)
|
||||
.map(filePath => ({
|
||||
filePath,
|
||||
url: this._filesToDownload[filePath],
|
||||
}));
|
||||
};
|
||||
|
||||
mkDir = (path: string) => {
|
||||
// "Directories" are assumed to exist.
|
||||
return true;
|
||||
};
|
||||
dirExists = (path: string) => {
|
||||
// TODO: To be changed to be EnsureDirExists.
|
||||
// "Directories" are assumed to exist.
|
||||
return true;
|
||||
};
|
||||
clearDir = (path: string) => {
|
||||
// Clear the files to be written in the specified directory.
|
||||
const filePaths = Object.keys(this._textFiles);
|
||||
filePaths.forEach(filePath => {
|
||||
if (filePath.indexOf(path) === 0) {
|
||||
delete this._textFiles[filePath];
|
||||
}
|
||||
});
|
||||
|
||||
return true;
|
||||
};
|
||||
getTempDir = () => {
|
||||
return '/browser-file-system-tmp-dir';
|
||||
};
|
||||
fileNameFrom = (fullpath: string) => {
|
||||
return path.basename(fullpath);
|
||||
};
|
||||
dirNameFrom = (fullpath: string) => {
|
||||
return path.dirname(fullpath);
|
||||
};
|
||||
makeAbsolute = (filePathOrURL: string, baseDirectoryOrURL: string) => {
|
||||
// URLs are always absolute
|
||||
if (isURL(filePathOrURL)) return filePathOrURL;
|
||||
|
||||
if (!this.isAbsolute(baseDirectoryOrURL))
|
||||
baseDirectoryOrURL = path.resolve(baseDirectoryOrURL);
|
||||
|
||||
return path.resolve(baseDirectoryOrURL, path.normalize(filePathOrURL));
|
||||
};
|
||||
makeRelative = (filePathOrURL: string, baseDirectoryOrURL: string) => {
|
||||
if (isURL(filePathOrURL)) {
|
||||
// Cutting the start if the URL is relative to the base URL
|
||||
if (filePathOrURL.indexOf(baseDirectoryOrURL) === 0) {
|
||||
return filePathOrURL.substring(baseDirectoryOrURL.length);
|
||||
}
|
||||
|
||||
// Keep the URL "absolute" if on different domains.
|
||||
console.warn(
|
||||
`${filePathOrURL} cannot be made relative to ${baseDirectoryOrURL}, please double check this behavior is correct.`
|
||||
);
|
||||
return filePathOrURL;
|
||||
}
|
||||
|
||||
// Paths are treated as usual paths.
|
||||
return path.relative(baseDirectoryOrURL, path.normalize(filePathOrURL));
|
||||
};
|
||||
isAbsolute = (fullpath: string) => {
|
||||
// URLs are always absolute
|
||||
if (isURL(fullpath)) return true;
|
||||
|
||||
// Paths are absolute if starting from the root
|
||||
return fullpath.length > 0 && fullpath.charAt(0) === '/';
|
||||
};
|
||||
|
||||
copyFile = (source: string, dest: string) => {
|
||||
// URLs are not copied, but marked as to be downloaded.
|
||||
if (isURL(source)) {
|
||||
if (isURL(dest)) {
|
||||
console.error(
|
||||
`Destination can't be a URL in copyFile (from ${source} to ${dest}).`
|
||||
);
|
||||
return false;
|
||||
}
|
||||
|
||||
this._filesToDownload[path.normalize(dest)] = source;
|
||||
return true;
|
||||
}
|
||||
|
||||
// If this is a file that we have already in memory,
|
||||
// copy its path.
|
||||
if (!!this._textFiles[source]) {
|
||||
this._textFiles[path.normalize(dest)] = this._textFiles[source];
|
||||
return true;
|
||||
}
|
||||
|
||||
console.error(`File not found in copyFile (from ${source} to ${dest}).`);
|
||||
return false;
|
||||
};
|
||||
|
||||
writeToFile = (filePath: string, content: string) => {
|
||||
this._textFiles[path.normalize(filePath)] = content;
|
||||
return true;
|
||||
};
|
||||
|
||||
readFile = (file: string): string => {
|
||||
if (this._textFiles[file]) return this._textFiles[file];
|
||||
|
||||
console.error(`Unknown file ${file}, returning an empty string`);
|
||||
return '';
|
||||
};
|
||||
|
||||
readDir = (path: string, ext: string) => {
|
||||
ext = ext.toUpperCase();
|
||||
var output = new gd.VectorString();
|
||||
|
||||
// Simulate ReadDir by returning all external URLs
|
||||
// with the filename matching the extension.
|
||||
Object.keys(this._filesToDownload).forEach(filePath => {
|
||||
const upperCaseFilePath = filePath.toUpperCase();
|
||||
if (
|
||||
upperCaseFilePath.indexOf(ext) ===
|
||||
upperCaseFilePath.length - ext.length
|
||||
) {
|
||||
output.push_back(filePath);
|
||||
}
|
||||
});
|
||||
|
||||
return output;
|
||||
};
|
||||
|
||||
fileExists = (filePath: string) => {
|
||||
if (isURL(filePath)) return true;
|
||||
|
||||
const normalizedFilePath = path.normalize(filePath);
|
||||
return (
|
||||
!!this._textFiles[normalizedFilePath] ||
|
||||
!!this._filesToDownload[normalizedFilePath]
|
||||
);
|
||||
};
|
||||
}
|
185
newIDE/app/src/Export/BrowserExporters/BrowserFileSystem.spec.js
Normal file
@@ -0,0 +1,185 @@
|
||||
// @flow
|
||||
import BrowserFileSystem from './BrowserFileSystem';
|
||||
|
||||
describe('BrowserFileSystem', () => {
|
||||
describe('file content storing and reading', () => {
|
||||
test('it can read text files originally passed as argument', () => {
|
||||
const browserFileSystem = new BrowserFileSystem({
|
||||
textFiles: [
|
||||
{
|
||||
filePath: '/file1',
|
||||
text: 'content1',
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
expect(browserFileSystem.readFile('/file1')).toBe('content1');
|
||||
});
|
||||
|
||||
test('it can write files in memory and read them', () => {
|
||||
const browserFileSystem = new BrowserFileSystem({ textFiles: [] });
|
||||
|
||||
browserFileSystem.writeToFile('/file1', 'content1');
|
||||
browserFileSystem.writeToFile('/folder/file2', 'content2');
|
||||
expect(browserFileSystem.readFile('/file1')).toBe('content1');
|
||||
expect(browserFileSystem.readFile('/folder/file2')).toBe('content2');
|
||||
});
|
||||
|
||||
test('it can store text files and retrieve them', () => {
|
||||
const browserFileSystem = new BrowserFileSystem({ textFiles: [] });
|
||||
|
||||
browserFileSystem.writeToFile('/folder/file1', 'content1');
|
||||
browserFileSystem.writeToFile('/folder/file2', 'content2');
|
||||
|
||||
const expectedTextFiles = [
|
||||
{ filePath: '/folder/file1', text: 'content1' },
|
||||
{ filePath: '/folder/file2', text: 'content2' },
|
||||
];
|
||||
|
||||
expect(browserFileSystem.getAllTextFilesIn('/')).toEqual(
|
||||
expectedTextFiles
|
||||
);
|
||||
expect(browserFileSystem.getAllTextFilesIn('/folder/')).toEqual(
|
||||
expectedTextFiles
|
||||
);
|
||||
expect(browserFileSystem.getAllTextFilesIn('/another-folder/')).toEqual(
|
||||
[]
|
||||
);
|
||||
});
|
||||
|
||||
test('it can write files in memory and clear them', () => {
|
||||
const browserFileSystem = new BrowserFileSystem({ textFiles: [] });
|
||||
|
||||
browserFileSystem.writeToFile('/folder/file1', 'content1');
|
||||
browserFileSystem.writeToFile('/another-folder/file2', 'content2');
|
||||
|
||||
const expectedTextFiles = [
|
||||
{ filePath: '/another-folder/file2', text: 'content2' },
|
||||
];
|
||||
|
||||
browserFileSystem.clearDir('/folder/');
|
||||
expect(browserFileSystem.getAllTextFilesIn('/')).toEqual(
|
||||
expectedTextFiles
|
||||
);
|
||||
expect(browserFileSystem.getAllTextFilesIn('/another-folder/')).toEqual(
|
||||
expectedTextFiles
|
||||
);
|
||||
expect(browserFileSystem.getAllTextFilesIn('/folder/')).toEqual([]);
|
||||
});
|
||||
|
||||
test('it can copy files previously wrote in memory', () => {
|
||||
const browserFileSystem = new BrowserFileSystem({ textFiles: [] });
|
||||
|
||||
browserFileSystem.writeToFile('/file1', 'content1');
|
||||
browserFileSystem.copyFile('/file1', '/copied-file1');
|
||||
expect(browserFileSystem.readFile('/file1')).toBe('content1');
|
||||
expect(browserFileSystem.readFile('/copied-file1')).toBe('content1');
|
||||
});
|
||||
|
||||
test('it can mark files to be copied from an URL as to be downloaded', () => {
|
||||
const browserFileSystem = new BrowserFileSystem({ textFiles: [] });
|
||||
|
||||
browserFileSystem.copyFile(
|
||||
'http://file.com/from/url',
|
||||
'/folder/downloaded-file'
|
||||
);
|
||||
expect(browserFileSystem.getAllUrlFilesIn('/')).toEqual([
|
||||
{
|
||||
filePath: '/folder/downloaded-file',
|
||||
url: 'http://file.com/from/url',
|
||||
},
|
||||
]);
|
||||
expect(browserFileSystem.getAllUrlFilesIn('/folder/')).toEqual([
|
||||
{
|
||||
filePath: '/folder/downloaded-file',
|
||||
url: 'http://file.com/from/url',
|
||||
},
|
||||
]);
|
||||
expect(browserFileSystem.getAllUrlFilesIn('/another-folder/')).toEqual(
|
||||
[]
|
||||
);
|
||||
});
|
||||
test('it can tell if a file exists', () => {
|
||||
const browserFileSystem = new BrowserFileSystem({
|
||||
textFiles: [
|
||||
{
|
||||
filePath: '/file1',
|
||||
text: 'content1',
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
expect(browserFileSystem.fileExists('/file1')).toBe(true);
|
||||
expect(browserFileSystem.fileExists('/folder/downloaded-file')).toBe(
|
||||
false
|
||||
);
|
||||
|
||||
browserFileSystem.copyFile(
|
||||
'http://file.com/from/url',
|
||||
'/folder/downloaded-file'
|
||||
);
|
||||
|
||||
expect(browserFileSystem.fileExists('/file1')).toBe(true);
|
||||
expect(browserFileSystem.fileExists('/folder/downloaded-file')).toBe(
|
||||
true
|
||||
);
|
||||
|
||||
browserFileSystem.writeToFile('/another-folder/file2', 'content2');
|
||||
|
||||
expect(browserFileSystem.fileExists('/file1')).toBe(true);
|
||||
expect(browserFileSystem.fileExists('/another-folder/file2')).toBe(true);
|
||||
expect(browserFileSystem.fileExists('/folder/downloaded-file')).toBe(
|
||||
true
|
||||
);
|
||||
|
||||
// Paths should be normalized:
|
||||
expect(browserFileSystem.fileExists('///file1')).toBe(true);
|
||||
expect(browserFileSystem.fileExists('/folder///downloaded-file')).toBe(
|
||||
true
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('file path manipulation', () => {
|
||||
test('it can make a path relative to another', () => {
|
||||
const browserFileSystem = new BrowserFileSystem({ textFiles: [] });
|
||||
|
||||
expect(browserFileSystem.makeRelative('/folder/file1', '/folder')).toBe(
|
||||
'file1'
|
||||
);
|
||||
expect(browserFileSystem.makeRelative('/folder/file1', '/')).toBe(
|
||||
'folder/file1'
|
||||
);
|
||||
});
|
||||
test('it can make URL relative to another one if on the same domain', () => {
|
||||
const browserFileSystem = new BrowserFileSystem({ textFiles: [] });
|
||||
|
||||
expect(
|
||||
browserFileSystem.makeRelative(
|
||||
'http://test.com/path/to/file1',
|
||||
'http://test.com/path/'
|
||||
)
|
||||
).toBe('to/file1');
|
||||
});
|
||||
test('it does not make URL relative to another one if not on the same domain', () => {
|
||||
const browserFileSystem = new BrowserFileSystem({ textFiles: [] });
|
||||
|
||||
expect(
|
||||
browserFileSystem.makeRelative(
|
||||
'http://test.com/url1',
|
||||
'http://test2.com/url1'
|
||||
)
|
||||
).toBe('http://test.com/url1');
|
||||
});
|
||||
test('it can make a path absolute', () => {
|
||||
const browserFileSystem = new BrowserFileSystem({ textFiles: [] });
|
||||
|
||||
expect(browserFileSystem.makeAbsolute('subfolder/file1', '/folder')).toBe(
|
||||
'/folder/subfolder/file1'
|
||||
);
|
||||
expect(browserFileSystem.makeAbsolute('/folder/file2', '/')).toBe(
|
||||
'/folder/file2'
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
160
newIDE/app/src/Export/BrowserExporters/BrowserHTML5Export.js
Normal file
@@ -0,0 +1,160 @@
|
||||
// @flow
|
||||
import * as React from 'react';
|
||||
import { Trans } from '@lingui/macro';
|
||||
import assignIn from 'lodash/assignIn';
|
||||
import { findGDJS } from './BrowserS3GDJSFinder';
|
||||
import BrowserFileSystem from './BrowserFileSystem';
|
||||
import {
|
||||
type UrlFileDescriptor,
|
||||
type TextFileDescriptor,
|
||||
type BlobFileDescriptor,
|
||||
downloadUrlsToBlobs,
|
||||
archiveFiles,
|
||||
} from '../../Utils/BrowserArchiver';
|
||||
import {
|
||||
type ExportPipeline,
|
||||
type ExportPipelineContext,
|
||||
} from '../ExportPipeline.flow';
|
||||
import RaisedButton from '../../UI/RaisedButton';
|
||||
import { BlobDownloadUrlHolder } from '../../Utils/BlobDownloadUrlHolder';
|
||||
import { ExplanationHeader, DoneFooter } from '../GenericExporters/HTML5Export';
|
||||
const gd = global.gd;
|
||||
|
||||
type ExportState = null;
|
||||
|
||||
type PreparedExporter = {|
|
||||
exporter: gdjsExporter,
|
||||
abstractFileSystem: BrowserFileSystem,
|
||||
outputDir: string,
|
||||
|};
|
||||
|
||||
type ExportOutput = {|
|
||||
textFiles: Array<TextFileDescriptor>,
|
||||
urlFiles: Array<UrlFileDescriptor>,
|
||||
|};
|
||||
|
||||
type ResourcesDownloadOutput = {|
|
||||
textFiles: Array<TextFileDescriptor>,
|
||||
blobFiles: Array<BlobFileDescriptor>,
|
||||
|};
|
||||
|
||||
type CompressionOutput = Blob;
|
||||
|
||||
const openBlobDownloadUrl = (url: string, filename: string) => {
|
||||
const { body } = document;
|
||||
if (!body) return;
|
||||
|
||||
// Not using Window.openExternalURL because blob urls are blocked
|
||||
// by Adblock Plus (and maybe other ad blockers).
|
||||
const a = document.createElement('a');
|
||||
body.appendChild(a);
|
||||
a.style.display = 'none';
|
||||
a.href = url;
|
||||
a.download = filename;
|
||||
a.click();
|
||||
body.removeChild(a);
|
||||
};
|
||||
|
||||
export const browserHTML5ExportPipeline: ExportPipeline<
|
||||
ExportState,
|
||||
PreparedExporter,
|
||||
ExportOutput,
|
||||
ResourcesDownloadOutput,
|
||||
CompressionOutput
|
||||
> = {
|
||||
name: 'browser-html5',
|
||||
|
||||
getInitialExportState: () => null,
|
||||
|
||||
canLaunchBuild: () => true,
|
||||
|
||||
renderHeader: () => <ExplanationHeader />,
|
||||
|
||||
renderLaunchButtonLabel: () => <Trans>Export as a HTML5 game</Trans>,
|
||||
|
||||
prepareExporter: (
|
||||
context: ExportPipelineContext<ExportState>
|
||||
): Promise<PreparedExporter> => {
|
||||
return findGDJS('web').then(({ gdjsRoot, filesContent }) => {
|
||||
console.info('GDJS found in ', gdjsRoot);
|
||||
|
||||
const outputDir = '/export/';
|
||||
const abstractFileSystem = new BrowserFileSystem({
|
||||
textFiles: filesContent,
|
||||
});
|
||||
// TODO: Memory leak? Check for other exporters too.
|
||||
const fileSystem = assignIn(
|
||||
new gd.AbstractFileSystemJS(),
|
||||
abstractFileSystem
|
||||
);
|
||||
const exporter = new gd.Exporter(fileSystem, gdjsRoot);
|
||||
|
||||
return {
|
||||
exporter,
|
||||
outputDir,
|
||||
abstractFileSystem,
|
||||
};
|
||||
});
|
||||
},
|
||||
|
||||
launchExport: (
|
||||
context: ExportPipelineContext<ExportState>,
|
||||
{ exporter, outputDir, abstractFileSystem }: PreparedExporter
|
||||
): Promise<ExportOutput> => {
|
||||
const { project } = context;
|
||||
|
||||
const exportOptions = new gd.MapStringBoolean();
|
||||
exporter.exportWholePixiProject(project, outputDir, exportOptions);
|
||||
exportOptions.delete();
|
||||
exporter.delete();
|
||||
|
||||
return Promise.resolve({
|
||||
textFiles: abstractFileSystem.getAllTextFilesIn(outputDir),
|
||||
urlFiles: abstractFileSystem.getAllUrlFilesIn(outputDir),
|
||||
});
|
||||
},
|
||||
|
||||
launchResourcesDownload: (
|
||||
context: ExportPipelineContext<ExportState>,
|
||||
{ textFiles, urlFiles }: ExportOutput
|
||||
): Promise<ResourcesDownloadOutput> => {
|
||||
return downloadUrlsToBlobs({
|
||||
urlFiles,
|
||||
onProgress: context.updateStepProgress,
|
||||
}).then(blobFiles => ({
|
||||
blobFiles,
|
||||
textFiles,
|
||||
}));
|
||||
},
|
||||
|
||||
launchCompression: (
|
||||
context: ExportPipelineContext<ExportState>,
|
||||
{ textFiles, blobFiles }: ResourcesDownloadOutput
|
||||
): Promise<Blob> => {
|
||||
return archiveFiles({
|
||||
blobFiles,
|
||||
textFiles,
|
||||
basePath: '/export/',
|
||||
onProgress: context.updateStepProgress,
|
||||
});
|
||||
},
|
||||
|
||||
renderDoneFooter: ({ compressionOutput, exportState, onClose }) => {
|
||||
return (
|
||||
<DoneFooter
|
||||
renderGameButton={() => (
|
||||
<BlobDownloadUrlHolder blob={compressionOutput}>
|
||||
{blobDownloadUrl => (
|
||||
<RaisedButton
|
||||
fullWidth
|
||||
primary
|
||||
onClick={() => openBlobDownloadUrl(blobDownloadUrl, 'game.zip')}
|
||||
label={<Trans>Download the exported game</Trans>}
|
||||
/>
|
||||
)}
|
||||
</BlobDownloadUrlHolder>
|
||||
)}
|
||||
/>
|
||||
);
|
||||
},
|
||||
};
|
@@ -0,0 +1,153 @@
|
||||
// @flow
|
||||
import * as React from 'react';
|
||||
import { Trans } from '@lingui/macro';
|
||||
import assignIn from 'lodash/assignIn';
|
||||
import {
|
||||
type Build,
|
||||
buildCordovaAndroid,
|
||||
uploadBuildFile,
|
||||
} from '../../Utils/GDevelopServices/Build';
|
||||
import { type UserProfile } from '../../Profile/UserProfileContext';
|
||||
import { findGDJS } from './BrowserS3GDJSFinder';
|
||||
import BrowserFileSystem from './BrowserFileSystem';
|
||||
import {
|
||||
type UrlFileDescriptor,
|
||||
type TextFileDescriptor,
|
||||
type BlobFileDescriptor,
|
||||
downloadUrlsToBlobs,
|
||||
archiveFiles,
|
||||
} from '../../Utils/BrowserArchiver';
|
||||
import {
|
||||
type ExportPipeline,
|
||||
type ExportPipelineContext,
|
||||
} from '../ExportPipeline.flow';
|
||||
import { ExplanationHeader } from '../GenericExporters/OnlineCordovaExport';
|
||||
const gd = global.gd;
|
||||
|
||||
type ExportState = null;
|
||||
|
||||
type PreparedExporter = {|
|
||||
exporter: gdjsExporter,
|
||||
abstractFileSystem: BrowserFileSystem,
|
||||
outputDir: string,
|
||||
|};
|
||||
|
||||
type ExportOutput = {|
|
||||
textFiles: Array<TextFileDescriptor>,
|
||||
urlFiles: Array<UrlFileDescriptor>,
|
||||
|};
|
||||
|
||||
type ResourcesDownloadOutput = {|
|
||||
textFiles: Array<TextFileDescriptor>,
|
||||
blobFiles: Array<BlobFileDescriptor>,
|
||||
|};
|
||||
|
||||
type CompressionOutput = Blob;
|
||||
|
||||
export const browserOnlineCordovaExportPipeline: ExportPipeline<
|
||||
ExportState,
|
||||
PreparedExporter,
|
||||
ExportOutput,
|
||||
ResourcesDownloadOutput,
|
||||
CompressionOutput
|
||||
> = {
|
||||
name: 'browser-online-cordova',
|
||||
onlineBuildType: 'cordova-build',
|
||||
|
||||
getInitialExportState: () => null,
|
||||
|
||||
canLaunchBuild: () => true,
|
||||
|
||||
renderHeader: () => <ExplanationHeader />,
|
||||
|
||||
renderLaunchButtonLabel: () => <Trans>Packaging for Android</Trans>,
|
||||
|
||||
prepareExporter: (
|
||||
context: ExportPipelineContext<ExportState>
|
||||
): Promise<PreparedExporter> => {
|
||||
return findGDJS('cordova').then(({ gdjsRoot, filesContent }) => {
|
||||
console.info('GDJS found in ', gdjsRoot);
|
||||
|
||||
const outputDir = '/export/';
|
||||
const abstractFileSystem = new BrowserFileSystem({
|
||||
textFiles: filesContent,
|
||||
});
|
||||
const fileSystem = assignIn(
|
||||
new gd.AbstractFileSystemJS(),
|
||||
abstractFileSystem
|
||||
);
|
||||
const exporter = new gd.Exporter(fileSystem, gdjsRoot);
|
||||
|
||||
return {
|
||||
exporter,
|
||||
outputDir,
|
||||
abstractFileSystem,
|
||||
};
|
||||
});
|
||||
},
|
||||
|
||||
launchExport: (
|
||||
context: ExportPipelineContext<ExportState>,
|
||||
{ exporter, outputDir, abstractFileSystem }: PreparedExporter
|
||||
): Promise<ExportOutput> => {
|
||||
const { project } = context;
|
||||
|
||||
const exportOptions = new gd.MapStringBoolean();
|
||||
exportOptions.set('exportForCordova', true);
|
||||
exporter.exportWholePixiProject(project, outputDir, exportOptions);
|
||||
exportOptions.delete();
|
||||
exporter.delete();
|
||||
|
||||
return Promise.resolve({
|
||||
textFiles: abstractFileSystem.getAllTextFilesIn(outputDir),
|
||||
urlFiles: abstractFileSystem.getAllUrlFilesIn(outputDir),
|
||||
});
|
||||
},
|
||||
|
||||
launchResourcesDownload: (
|
||||
context: ExportPipelineContext<ExportState>,
|
||||
{ textFiles, urlFiles }: ExportOutput
|
||||
): Promise<ResourcesDownloadOutput> => {
|
||||
return downloadUrlsToBlobs({
|
||||
urlFiles,
|
||||
onProgress: context.updateStepProgress,
|
||||
}).then(blobFiles => ({
|
||||
blobFiles,
|
||||
textFiles,
|
||||
}));
|
||||
},
|
||||
|
||||
launchCompression: (
|
||||
context: ExportPipelineContext<ExportState>,
|
||||
{ textFiles, blobFiles }: ResourcesDownloadOutput
|
||||
): Promise<Blob> => {
|
||||
return archiveFiles({
|
||||
blobFiles,
|
||||
textFiles,
|
||||
basePath: '/export/',
|
||||
onProgress: context.updateStepProgress,
|
||||
});
|
||||
},
|
||||
|
||||
launchUpload: (
|
||||
context: ExportPipelineContext<ExportState>,
|
||||
blobFile: Blob
|
||||
): Promise<string> => {
|
||||
return uploadBuildFile(blobFile, context.updateStepProgress);
|
||||
},
|
||||
|
||||
launchOnlineBuild: (
|
||||
exportState: ExportState,
|
||||
userProfile: UserProfile,
|
||||
uploadBucketKey: string
|
||||
): Promise<Build> => {
|
||||
const { getAuthorizationHeader, profile } = userProfile;
|
||||
if (!profile) return Promise.reject(new Error('User is not authenticated'));
|
||||
|
||||
return buildCordovaAndroid(
|
||||
getAuthorizationHeader,
|
||||
profile.uid,
|
||||
uploadBucketKey
|
||||
);
|
||||
},
|
||||
};
|
@@ -0,0 +1,157 @@
|
||||
// @flow
|
||||
import * as React from 'react';
|
||||
import { Trans } from '@lingui/macro';
|
||||
import assignIn from 'lodash/assignIn';
|
||||
import {
|
||||
type Build,
|
||||
buildElectron,
|
||||
uploadBuildFile,
|
||||
} from '../../Utils/GDevelopServices/Build';
|
||||
import { type UserProfile } from '../../Profile/UserProfileContext';
|
||||
import { findGDJS } from './BrowserS3GDJSFinder';
|
||||
import BrowserFileSystem from './BrowserFileSystem';
|
||||
import {
|
||||
type UrlFileDescriptor,
|
||||
type TextFileDescriptor,
|
||||
type BlobFileDescriptor,
|
||||
downloadUrlsToBlobs,
|
||||
archiveFiles,
|
||||
} from '../../Utils/BrowserArchiver';
|
||||
import {
|
||||
type ExportPipeline,
|
||||
type ExportPipelineContext,
|
||||
} from '../ExportPipeline.flow';
|
||||
import {
|
||||
type ExportState,
|
||||
SetupExportHeader,
|
||||
} from '../GenericExporters/OnlineElectronExport';
|
||||
const gd = global.gd;
|
||||
|
||||
type PreparedExporter = {|
|
||||
exporter: gdjsExporter,
|
||||
abstractFileSystem: BrowserFileSystem,
|
||||
outputDir: string,
|
||||
|};
|
||||
|
||||
type ExportOutput = {|
|
||||
textFiles: Array<TextFileDescriptor>,
|
||||
urlFiles: Array<UrlFileDescriptor>,
|
||||
|};
|
||||
|
||||
type ResourcesDownloadOutput = {|
|
||||
textFiles: Array<TextFileDescriptor>,
|
||||
blobFiles: Array<BlobFileDescriptor>,
|
||||
|};
|
||||
|
||||
type CompressionOutput = Blob;
|
||||
|
||||
export const browserOnlineElectronExportPipeline: ExportPipeline<
|
||||
ExportState,
|
||||
PreparedExporter,
|
||||
ExportOutput,
|
||||
ResourcesDownloadOutput,
|
||||
CompressionOutput
|
||||
> = {
|
||||
name: 'browser-online-electron',
|
||||
onlineBuildType: 'electron-build',
|
||||
|
||||
getInitialExportState: () => ({
|
||||
targets: ['winExe'],
|
||||
}),
|
||||
|
||||
canLaunchBuild: (exportState: ExportState) => !!exportState.targets.length,
|
||||
|
||||
renderHeader: props => <SetupExportHeader {...props} />,
|
||||
|
||||
renderLaunchButtonLabel: () => <Trans>Package</Trans>,
|
||||
|
||||
prepareExporter: (
|
||||
context: ExportPipelineContext<ExportState>
|
||||
): Promise<PreparedExporter> => {
|
||||
return findGDJS('electron').then(({ gdjsRoot, filesContent }) => {
|
||||
console.info('GDJS found in ', gdjsRoot);
|
||||
|
||||
const outputDir = '/export/';
|
||||
const abstractFileSystem = new BrowserFileSystem({
|
||||
textFiles: filesContent,
|
||||
});
|
||||
const fileSystem = assignIn(
|
||||
new gd.AbstractFileSystemJS(),
|
||||
abstractFileSystem
|
||||
);
|
||||
const exporter = new gd.Exporter(fileSystem, gdjsRoot);
|
||||
|
||||
return {
|
||||
exporter,
|
||||
outputDir,
|
||||
abstractFileSystem,
|
||||
};
|
||||
});
|
||||
},
|
||||
|
||||
launchExport: (
|
||||
context: ExportPipelineContext<ExportState>,
|
||||
{ exporter, outputDir, abstractFileSystem }: PreparedExporter
|
||||
): Promise<ExportOutput> => {
|
||||
const { project } = context;
|
||||
|
||||
const exportOptions = new gd.MapStringBoolean();
|
||||
exportOptions.set('exportForElectron', true);
|
||||
exporter.exportWholePixiProject(project, outputDir, exportOptions);
|
||||
exportOptions.delete();
|
||||
exporter.delete();
|
||||
|
||||
return Promise.resolve({
|
||||
textFiles: abstractFileSystem.getAllTextFilesIn(outputDir),
|
||||
urlFiles: abstractFileSystem.getAllUrlFilesIn(outputDir),
|
||||
});
|
||||
},
|
||||
|
||||
launchResourcesDownload: (
|
||||
context: ExportPipelineContext<ExportState>,
|
||||
{ textFiles, urlFiles }: ExportOutput
|
||||
): Promise<ResourcesDownloadOutput> => {
|
||||
return downloadUrlsToBlobs({
|
||||
urlFiles,
|
||||
onProgress: context.updateStepProgress,
|
||||
}).then(blobFiles => ({
|
||||
blobFiles,
|
||||
textFiles,
|
||||
}));
|
||||
},
|
||||
|
||||
launchCompression: (
|
||||
context: ExportPipelineContext<ExportState>,
|
||||
{ textFiles, blobFiles }: ResourcesDownloadOutput
|
||||
): Promise<Blob> => {
|
||||
return archiveFiles({
|
||||
blobFiles,
|
||||
textFiles,
|
||||
basePath: '/export/',
|
||||
onProgress: context.updateStepProgress,
|
||||
});
|
||||
},
|
||||
|
||||
launchUpload: (
|
||||
context: ExportPipelineContext<ExportState>,
|
||||
blobFile: Blob
|
||||
): Promise<string> => {
|
||||
return uploadBuildFile(blobFile, context.updateStepProgress);
|
||||
},
|
||||
|
||||
launchOnlineBuild: (
|
||||
exportState: ExportState,
|
||||
userProfile: UserProfile,
|
||||
uploadBucketKey: string
|
||||
): Promise<Build> => {
|
||||
const { getAuthorizationHeader, profile } = userProfile;
|
||||
if (!profile) return Promise.reject(new Error('User is not authenticated'));
|
||||
|
||||
return buildElectron(
|
||||
getAuthorizationHeader,
|
||||
profile.uid,
|
||||
uploadBucketKey,
|
||||
exportState.targets
|
||||
);
|
||||
},
|
||||
};
|
148
newIDE/app/src/Export/BrowserExporters/BrowserOnlineWebExport.js
Normal file
@@ -0,0 +1,148 @@
|
||||
// @flow
|
||||
import * as React from 'react';
|
||||
import { Trans } from '@lingui/macro';
|
||||
import assignIn from 'lodash/assignIn';
|
||||
import {
|
||||
type Build,
|
||||
uploadBuildFile,
|
||||
buildWeb,
|
||||
} from '../../Utils/GDevelopServices/Build';
|
||||
import { type UserProfile } from '../../Profile/UserProfileContext';
|
||||
import { findGDJS } from './BrowserS3GDJSFinder';
|
||||
import BrowserFileSystem from './BrowserFileSystem';
|
||||
import {
|
||||
type UrlFileDescriptor,
|
||||
type TextFileDescriptor,
|
||||
type BlobFileDescriptor,
|
||||
downloadUrlsToBlobs,
|
||||
archiveFiles,
|
||||
} from '../../Utils/BrowserArchiver';
|
||||
import {
|
||||
type ExportPipeline,
|
||||
type ExportPipelineContext,
|
||||
} from '../ExportPipeline.flow';
|
||||
import { ExplanationHeader } from '../GenericExporters/OnlineWebExport';
|
||||
const gd = global.gd;
|
||||
|
||||
type ExportState = null;
|
||||
|
||||
type PreparedExporter = {|
|
||||
exporter: gdjsExporter,
|
||||
abstractFileSystem: BrowserFileSystem,
|
||||
outputDir: string,
|
||||
|};
|
||||
|
||||
type ExportOutput = {|
|
||||
textFiles: Array<TextFileDescriptor>,
|
||||
urlFiles: Array<UrlFileDescriptor>,
|
||||
|};
|
||||
|
||||
type ResourcesDownloadOutput = {|
|
||||
textFiles: Array<TextFileDescriptor>,
|
||||
blobFiles: Array<BlobFileDescriptor>,
|
||||
|};
|
||||
|
||||
type CompressionOutput = Blob;
|
||||
|
||||
export const browserOnlineWebExportPipeline: ExportPipeline<
|
||||
ExportState,
|
||||
PreparedExporter,
|
||||
ExportOutput,
|
||||
ResourcesDownloadOutput,
|
||||
CompressionOutput
|
||||
> = {
|
||||
name: 'browser-online-web',
|
||||
onlineBuildType: 'web-build',
|
||||
|
||||
getInitialExportState: () => null,
|
||||
|
||||
canLaunchBuild: () => true,
|
||||
|
||||
renderHeader: () => <ExplanationHeader />,
|
||||
|
||||
renderLaunchButtonLabel: () => <Trans>Publish online</Trans>,
|
||||
|
||||
prepareExporter: (
|
||||
context: ExportPipelineContext<ExportState>
|
||||
): Promise<PreparedExporter> => {
|
||||
return findGDJS('web').then(({ gdjsRoot, filesContent }) => {
|
||||
console.info('GDJS found in ', gdjsRoot);
|
||||
|
||||
const outputDir = '/export/';
|
||||
const abstractFileSystem = new BrowserFileSystem({
|
||||
textFiles: filesContent,
|
||||
});
|
||||
const fileSystem = assignIn(
|
||||
new gd.AbstractFileSystemJS(),
|
||||
abstractFileSystem
|
||||
);
|
||||
const exporter = new gd.Exporter(fileSystem, gdjsRoot);
|
||||
|
||||
return {
|
||||
exporter,
|
||||
outputDir,
|
||||
abstractFileSystem,
|
||||
};
|
||||
});
|
||||
},
|
||||
|
||||
launchExport: (
|
||||
context: ExportPipelineContext<ExportState>,
|
||||
{ exporter, outputDir, abstractFileSystem }: PreparedExporter
|
||||
): Promise<ExportOutput> => {
|
||||
const { project } = context;
|
||||
|
||||
const exportOptions = new gd.MapStringBoolean();
|
||||
exporter.exportWholePixiProject(project, outputDir, exportOptions);
|
||||
exportOptions.delete();
|
||||
exporter.delete();
|
||||
|
||||
return Promise.resolve({
|
||||
textFiles: abstractFileSystem.getAllTextFilesIn(outputDir),
|
||||
urlFiles: abstractFileSystem.getAllUrlFilesIn(outputDir),
|
||||
});
|
||||
},
|
||||
|
||||
launchResourcesDownload: (
|
||||
context: ExportPipelineContext<ExportState>,
|
||||
{ textFiles, urlFiles }: ExportOutput
|
||||
): Promise<ResourcesDownloadOutput> => {
|
||||
return downloadUrlsToBlobs({
|
||||
urlFiles,
|
||||
onProgress: context.updateStepProgress,
|
||||
}).then(blobFiles => ({
|
||||
blobFiles,
|
||||
textFiles,
|
||||
}));
|
||||
},
|
||||
|
||||
launchCompression: (
|
||||
context: ExportPipelineContext<ExportState>,
|
||||
{ textFiles, blobFiles }: ResourcesDownloadOutput
|
||||
): Promise<Blob> => {
|
||||
return archiveFiles({
|
||||
blobFiles,
|
||||
textFiles,
|
||||
basePath: '/export/',
|
||||
onProgress: context.updateStepProgress,
|
||||
});
|
||||
},
|
||||
|
||||
launchUpload: (
|
||||
context: ExportPipelineContext<ExportState>,
|
||||
blobFile: Blob
|
||||
): Promise<string> => {
|
||||
return uploadBuildFile(blobFile, context.updateStepProgress);
|
||||
},
|
||||
|
||||
launchOnlineBuild: (
|
||||
exportState: ExportState,
|
||||
userProfile: UserProfile,
|
||||
uploadBucketKey: string
|
||||
): Promise<Build> => {
|
||||
const { getAuthorizationHeader, profile } = userProfile;
|
||||
if (!profile) return Promise.reject(new Error('User is not authenticated'));
|
||||
|
||||
return buildWeb(getAuthorizationHeader, profile.uid, uploadBucketKey);
|
||||
},
|
||||
};
|
@@ -1,20 +1,61 @@
|
||||
// @flow
|
||||
import path from 'path';
|
||||
import { uploadObject } from '../../Utils/GDevelopServices/Preview';
|
||||
const gd = global.gd;
|
||||
|
||||
export type TextFileDescriptor = {|
|
||||
filePath: string,
|
||||
text: string,
|
||||
|};
|
||||
|
||||
type PendingUploadFileDescriptor = {|
|
||||
Key: string,
|
||||
Body: string,
|
||||
ContentType: 'text/javascript' | 'text/html',
|
||||
|};
|
||||
|
||||
type ConstructorArgs = {|
|
||||
filesContent: Array<TextFileDescriptor>,
|
||||
prefix: string,
|
||||
bucketBaseUrl: string,
|
||||
|};
|
||||
|
||||
const isURL = (filename: string) => {
|
||||
return (
|
||||
filename.substr(0, 7) === 'http://' ||
|
||||
filename.substr(0, 8) === 'https://' ||
|
||||
filename.substr(0, 6) === 'ftp://'
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* An in-memory "file system" that can be used for GDevelop previews.
|
||||
*/
|
||||
export default class BrowserS3FileSystem {
|
||||
constructor({ filesContent, prefix, bucketBaseUrl }) {
|
||||
this.filesContent = filesContent;
|
||||
prefix: string;
|
||||
bucketBaseUrl: string;
|
||||
|
||||
// Store the content of some files.
|
||||
_indexedFilesContent: { [string]: TextFileDescriptor };
|
||||
|
||||
// Store all the objects that should be written on the S3 bucket.
|
||||
// Call uploadPendingObjects to send them
|
||||
_pendingUploadObjects: Array<PendingUploadFileDescriptor> = [];
|
||||
|
||||
// Store a set of all external URLs copied so that we can simulate
|
||||
// readDir result.
|
||||
_allCopiedExternalUrls = new Set<string>();
|
||||
|
||||
constructor({ filesContent, prefix, bucketBaseUrl }: ConstructorArgs) {
|
||||
this.prefix = prefix;
|
||||
this.bucketBaseUrl = bucketBaseUrl;
|
||||
|
||||
// Store all the objects that should be written on the S3 bucket.
|
||||
// Call uploadPendingObjects to send them
|
||||
this._pendingUploadObjects = [];
|
||||
|
||||
// Store a set of all external URLs copied so that we can simulate
|
||||
// readDir result.
|
||||
this._allCopiedExternalUrls = new Set();
|
||||
this._indexedFilesContent = {};
|
||||
filesContent.forEach(textFileDescriptor => {
|
||||
this._indexedFilesContent[
|
||||
textFileDescriptor.filePath
|
||||
] = textFileDescriptor;
|
||||
});
|
||||
}
|
||||
|
||||
uploadPendingObjects = () => {
|
||||
@@ -30,71 +71,62 @@ export default class BrowserS3FileSystem {
|
||||
);
|
||||
};
|
||||
|
||||
mkDir = path => {
|
||||
mkDir = (path: string) => {
|
||||
// Assume required directories always exist.
|
||||
};
|
||||
dirExists = path => {
|
||||
dirExists = (path: string) => {
|
||||
// Assume required directories always exist.
|
||||
return true;
|
||||
};
|
||||
clearDir = path => {
|
||||
clearDir = (path: string) => {
|
||||
// Assume path is cleared.
|
||||
};
|
||||
getTempDir = () => {
|
||||
return '/virtual-unused-tmp-dir';
|
||||
};
|
||||
fileNameFrom = fullpath => {
|
||||
if (this._isExternalURL(fullpath)) return fullpath;
|
||||
fileNameFrom = (fullpath: string) => {
|
||||
if (isURL(fullpath)) return fullpath;
|
||||
|
||||
fullpath = this._translateURL(fullpath);
|
||||
return path.basename(fullpath);
|
||||
};
|
||||
dirNameFrom = fullpath => {
|
||||
if (this._isExternalURL(fullpath)) return '';
|
||||
dirNameFrom = (fullpath: string) => {
|
||||
if (isURL(fullpath)) return '';
|
||||
|
||||
fullpath = this._translateURL(fullpath);
|
||||
return path.dirname(fullpath);
|
||||
};
|
||||
makeAbsolute = (filename, baseDirectory) => {
|
||||
if (this._isExternalURL(filename)) return filename;
|
||||
makeAbsolute = (filename: string, baseDirectory: string) => {
|
||||
if (isURL(filename)) return filename;
|
||||
|
||||
filename = this._translateURL(filename);
|
||||
if (!this.isAbsolute(baseDirectory))
|
||||
baseDirectory = path.resolve(baseDirectory);
|
||||
|
||||
return path.resolve(baseDirectory, path.normalize(filename));
|
||||
};
|
||||
makeRelative = (filename, baseDirectory) => {
|
||||
if (this._isExternalURL(filename)) return filename;
|
||||
makeRelative = (filename: string, baseDirectory: string) => {
|
||||
if (isURL(filename)) return filename;
|
||||
|
||||
filename = this._translateURL(filename);
|
||||
return path.relative(baseDirectory, path.normalize(filename));
|
||||
};
|
||||
isAbsolute = fullpath => {
|
||||
if (this._isExternalURL(fullpath)) return true;
|
||||
isAbsolute = (fullpath: string) => {
|
||||
if (isURL(fullpath)) return true;
|
||||
|
||||
if (fullpath.length === 0) return true;
|
||||
fullpath = this._translateURL(fullpath);
|
||||
return (
|
||||
(fullpath.length > 0 && fullpath.charAt(0) === '/') ||
|
||||
(fullpath.length > 1 && fullpath.charAt(1) === ':')
|
||||
);
|
||||
};
|
||||
copyFile = (source, dest) => {
|
||||
copyFile = (source: string, dest: string) => {
|
||||
//URL are not copied.
|
||||
if (this._isExternalURL(source)) {
|
||||
if (isURL(source)) {
|
||||
this._allCopiedExternalUrls.add(source);
|
||||
return true;
|
||||
}
|
||||
|
||||
source = this._translateURL(source);
|
||||
console.warn('Copy not done from', source, 'to', dest);
|
||||
return true;
|
||||
};
|
||||
copyDir = (source, dest) => {
|
||||
throw new Error('Not implemented');
|
||||
};
|
||||
writeToFile = (fullPath, contents) => {
|
||||
writeToFile = (fullPath: string, contents: string) => {
|
||||
const key = fullPath.replace(this.bucketBaseUrl, '');
|
||||
const mime = {
|
||||
'.js': 'text/javascript',
|
||||
@@ -112,18 +144,19 @@ export default class BrowserS3FileSystem {
|
||||
return true;
|
||||
};
|
||||
|
||||
readFile = file => {
|
||||
if (this.filesContent.hasOwnProperty(file)) return this.filesContent[file];
|
||||
readFile = (file: string) => {
|
||||
if (!!this._indexedFilesContent[file])
|
||||
return this._indexedFilesContent[file].text;
|
||||
|
||||
console.error(`Unknown file ${file}, returning an empty string`);
|
||||
return '';
|
||||
};
|
||||
|
||||
readDir = (path, ext) => {
|
||||
readDir = (path: string, ext: string) => {
|
||||
ext = ext.toUpperCase();
|
||||
var output = new gd.VectorString();
|
||||
|
||||
// Simulate ReadDir by returning all external URL s
|
||||
// Simulate ReadDir by returning all external URLs
|
||||
// with the filename matching the extension.
|
||||
this._allCopiedExternalUrls.forEach(url => {
|
||||
const upperCaseUrl = url.toUpperCase();
|
||||
@@ -135,29 +168,10 @@ export default class BrowserS3FileSystem {
|
||||
return output;
|
||||
};
|
||||
|
||||
fileExists = filename => {
|
||||
if (this._isExternalURL(filename)) return true;
|
||||
fileExists = (filename: string) => {
|
||||
if (isURL(filename)) return true;
|
||||
|
||||
// Assume all files asked for exists.
|
||||
return true;
|
||||
};
|
||||
|
||||
_isExternalURL = filename => {
|
||||
return (
|
||||
filename.substr(0, 7) === 'http://' ||
|
||||
filename.substr(0, 8) === 'https://' ||
|
||||
filename.substr(0, 6) === 'ftp://'
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Return the filename associated to the URL on the server, relative to the games directory.
|
||||
* (i.e: Transform g/mydirectory/myfile.png to mydirectory/myfile.png).
|
||||
*/
|
||||
_translateURL = filename => {
|
||||
if (filename.substr(0, 2) === 'g/' || filename.substr(0, 2) === 'g\\')
|
||||
filename = filename.substr(2);
|
||||
|
||||
return filename;
|
||||
};
|
||||
}
|
||||
|
@@ -1,14 +1,71 @@
|
||||
import indexHTML from './GDJSindex.html.js';
|
||||
// @flow
|
||||
|
||||
const gdjsRoot =
|
||||
'https://s3-eu-west-1.amazonaws.com/gdevelop-resources/GDJS-5.0.0-beta81';
|
||||
|
||||
export const findGDJS = cb => {
|
||||
return cb({
|
||||
gdjsRoot,
|
||||
filesContent: {
|
||||
//TODO: Request and read it.
|
||||
[gdjsRoot + '/Runtime/index.html']: indexHTML,
|
||||
},
|
||||
type FileSet =
|
||||
| 'preview'
|
||||
| 'cordova'
|
||||
| 'electron'
|
||||
| 'web'
|
||||
| 'cocos2d-js'
|
||||
| 'facebook-instant-games';
|
||||
|
||||
const filesToDownload: { [FileSet]: Array<string> } = {
|
||||
preview: ['/Runtime/index.html'],
|
||||
web: ['/Runtime/index.html'],
|
||||
'cocos2d-js': [
|
||||
'/Runtime/Cocos2d/cocos2d-js-v3.10.js',
|
||||
'/Runtime/Cocos2d/index.html',
|
||||
'/Runtime/Cocos2d/main.js',
|
||||
'/Runtime/Cocos2d/project.json',
|
||||
],
|
||||
'facebook-instant-games': [
|
||||
'/Runtime/FacebookInstantGames/fbapp-config.json',
|
||||
'/Runtime/FacebookInstantGames/index.html',
|
||||
],
|
||||
cordova: [
|
||||
'/Runtime/Cordova/www/index.html',
|
||||
'/Runtime/Cordova/config.xml',
|
||||
'/Runtime/Cordova/package.json',
|
||||
],
|
||||
electron: [
|
||||
'/Runtime/index.html',
|
||||
'/Runtime/Electron/main.js',
|
||||
'/Runtime/Electron/package.json',
|
||||
],
|
||||
};
|
||||
|
||||
export type TextFileDescriptor = {| text: string, filePath: string |};
|
||||
|
||||
export const findGDJS = (
|
||||
fileSet: FileSet
|
||||
): Promise<{|
|
||||
gdjsRoot: string,
|
||||
filesContent: Array<TextFileDescriptor>,
|
||||
|}> => {
|
||||
return Promise.all(
|
||||
filesToDownload[fileSet].map(relativeFilePath => {
|
||||
const url = gdjsRoot + relativeFilePath;
|
||||
|
||||
// Don't do any caching, rely on the browser cache only.
|
||||
return fetch(url).then(response => {
|
||||
if (!response.ok) {
|
||||
console.error(`Error while downloading "${url}"`, response);
|
||||
throw new Error(
|
||||
`Error while downloading "${url}" (status: ${response.status})`
|
||||
);
|
||||
}
|
||||
return response.text().then(text => ({
|
||||
filePath: url,
|
||||
text,
|
||||
}));
|
||||
});
|
||||
})
|
||||
).then(filesContent => {
|
||||
return {
|
||||
gdjsRoot,
|
||||
filesContent,
|
||||
};
|
||||
});
|
||||
};
|
||||
|
@@ -0,0 +1,69 @@
|
||||
// @flow
|
||||
import { Trans } from '@lingui/macro';
|
||||
import { I18n } from '@lingui/react';
|
||||
import React, { Component } from 'react';
|
||||
import Dialog from '../../../UI/Dialog';
|
||||
import FlatButton from '../../../UI/FlatButton';
|
||||
import { Column, Line } from '../../../UI/Grid';
|
||||
import Text from '../../../UI/Text';
|
||||
|
||||
type Props = {|
|
||||
error: Error,
|
||||
onClose: () => void,
|
||||
|};
|
||||
|
||||
export default class BrowserPreviewErrorDialog extends Component<Props> {
|
||||
render() {
|
||||
const { error, onClose } = this.props;
|
||||
|
||||
return (
|
||||
<I18n>
|
||||
{({ i18n }) => (
|
||||
<Dialog
|
||||
actions={[
|
||||
<FlatButton
|
||||
key="close"
|
||||
label={<Trans>Close</Trans>}
|
||||
onClick={onClose}
|
||||
/>,
|
||||
]}
|
||||
title={<Trans>Could not launch the preview</Trans>}
|
||||
onRequestClose={onClose}
|
||||
modal
|
||||
open
|
||||
>
|
||||
<Line>
|
||||
<Column>
|
||||
<Text>
|
||||
{// $FlowFixMe - AWS returned errors can have extra fields
|
||||
error.code === 'NetworkingError' ? (
|
||||
<Trans>
|
||||
The preview could not be launched because you're offline.
|
||||
</Trans>
|
||||
) : (
|
||||
<Trans>
|
||||
The preview could not be launched because an error
|
||||
happened: {error.message}.
|
||||
</Trans>
|
||||
)}
|
||||
</Text>
|
||||
</Column>
|
||||
</Line>
|
||||
<Line>
|
||||
<Column>
|
||||
<Text>
|
||||
<Trans>
|
||||
Make sure you're online, have a proper internet connection
|
||||
and try again. If you download and use GDevelop desktop
|
||||
application, you can also run previews without any internet
|
||||
connection.
|
||||
</Trans>
|
||||
</Text>
|
||||
</Column>
|
||||
</Line>
|
||||
</Dialog>
|
||||
)}
|
||||
</I18n>
|
||||
);
|
||||
}
|
||||
}
|
@@ -5,11 +5,11 @@ import { I18n } from '@lingui/react';
|
||||
import { type I18n as I18nType } from '@lingui/core';
|
||||
|
||||
import React, { Component } from 'react';
|
||||
import Dialog from '../../UI/Dialog';
|
||||
import FlatButton from '../../UI/FlatButton';
|
||||
import { showErrorBox } from '../../UI/Messages/MessageBox';
|
||||
import { Column, Line } from '../../UI/Grid';
|
||||
import Text from '../../UI/Text';
|
||||
import Dialog from '../../../UI/Dialog';
|
||||
import FlatButton from '../../../UI/FlatButton';
|
||||
import { showErrorBox } from '../../../UI/Messages/MessageBox';
|
||||
import { Column, Line } from '../../../UI/Grid';
|
||||
import Text from '../../../UI/Text';
|
||||
|
||||
type Props = {|
|
||||
url: ?string,
|
@@ -1,20 +1,25 @@
|
||||
// @flow
|
||||
import * as React from 'react';
|
||||
import BrowserS3FileSystem from './BrowserS3FileSystem';
|
||||
import BrowserPreviewLinkDialog from './BrowserPreviewLinkDialog';
|
||||
import { findGDJS } from './BrowserS3GDJSFinder';
|
||||
import BrowserPreviewErrorDialog from './BrowserPreviewErrorDialog';
|
||||
import BrowserS3FileSystem from '../BrowserS3FileSystem';
|
||||
import { findGDJS } from '../BrowserS3GDJSFinder';
|
||||
import assignIn from 'lodash/assignIn';
|
||||
import { type PreviewOptions } from '../PreviewLauncher.flow';
|
||||
import { getBaseUrl } from '../../Utils/GDevelopServices/Preview';
|
||||
import { makeTimestampedId } from '../../Utils/TimestampedId';
|
||||
import { type PreviewOptions } from '../../PreviewLauncher.flow';
|
||||
import { getBaseUrl } from '../../../Utils/GDevelopServices/Preview';
|
||||
import { makeTimestampedId } from '../../../Utils/TimestampedId';
|
||||
const gd = global.gd;
|
||||
|
||||
type State = {|
|
||||
showPreviewLinkDialog: boolean,
|
||||
url: ?string,
|
||||
error: ?Error,
|
||||
|};
|
||||
|
||||
type Props = {};
|
||||
type Props = {|
|
||||
onExport?: () => void,
|
||||
onChangeSubscription?: () => void,
|
||||
|};
|
||||
|
||||
export default class BrowserS3PreviewLauncher extends React.Component<
|
||||
Props,
|
||||
@@ -25,6 +30,7 @@ export default class BrowserS3PreviewLauncher extends React.Component<
|
||||
state = {
|
||||
showPreviewLinkDialog: false,
|
||||
url: null,
|
||||
error: null,
|
||||
};
|
||||
|
||||
_openPreviewWindow = (project: gdProject, url: string): any => {
|
||||
@@ -36,35 +42,29 @@ export default class BrowserS3PreviewLauncher extends React.Component<
|
||||
};
|
||||
|
||||
_prepareExporter = (): Promise<any> => {
|
||||
return new Promise((resolve, reject) => {
|
||||
findGDJS(({ gdjsRoot, filesContent }) => {
|
||||
if (!gdjsRoot) {
|
||||
console.error('Could not find GDJS');
|
||||
return reject();
|
||||
}
|
||||
console.info('GDJS found in ', gdjsRoot);
|
||||
return findGDJS('preview').then(({ gdjsRoot, filesContent }) => {
|
||||
console.info('GDJS found in ', gdjsRoot);
|
||||
|
||||
const prefix = makeTimestampedId();
|
||||
const prefix = makeTimestampedId();
|
||||
|
||||
const outputDir = getBaseUrl() + prefix;
|
||||
const browserS3FileSystem = new BrowserS3FileSystem({
|
||||
filesContent,
|
||||
bucketBaseUrl: getBaseUrl(),
|
||||
prefix,
|
||||
});
|
||||
const fileSystem = assignIn(
|
||||
new gd.AbstractFileSystemJS(),
|
||||
browserS3FileSystem
|
||||
);
|
||||
const exporter = new gd.Exporter(fileSystem, gdjsRoot);
|
||||
exporter.setCodeOutputDirectory(getBaseUrl() + prefix);
|
||||
|
||||
resolve({
|
||||
exporter,
|
||||
outputDir,
|
||||
browserS3FileSystem,
|
||||
});
|
||||
const outputDir = getBaseUrl() + prefix;
|
||||
const browserS3FileSystem = new BrowserS3FileSystem({
|
||||
filesContent,
|
||||
bucketBaseUrl: getBaseUrl(),
|
||||
prefix,
|
||||
});
|
||||
const fileSystem = assignIn(
|
||||
new gd.AbstractFileSystemJS(),
|
||||
browserS3FileSystem
|
||||
);
|
||||
const exporter = new gd.Exporter(fileSystem, gdjsRoot);
|
||||
exporter.setCodeOutputDirectory(getBaseUrl() + prefix);
|
||||
|
||||
return {
|
||||
exporter,
|
||||
outputDir,
|
||||
browserS3FileSystem,
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
@@ -73,10 +73,12 @@ export default class BrowserS3PreviewLauncher extends React.Component<
|
||||
layout: gdLayout,
|
||||
options: PreviewOptions
|
||||
): Promise<any> => {
|
||||
if (!project || !layout) return Promise.reject();
|
||||
this.setState({
|
||||
error: null,
|
||||
});
|
||||
|
||||
return this._prepareExporter().then(
|
||||
({ exporter, outputDir, browserS3FileSystem }) => {
|
||||
return this._prepareExporter()
|
||||
.then(({ exporter, outputDir, browserS3FileSystem }) => {
|
||||
exporter.exportLayoutForPixiPreview(project, layout, outputDir);
|
||||
exporter.delete();
|
||||
return browserS3FileSystem
|
||||
@@ -93,8 +95,12 @@ export default class BrowserS3PreviewLauncher extends React.Component<
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
);
|
||||
})
|
||||
.catch((error: Error) => {
|
||||
this.setState({
|
||||
error,
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
launchExternalLayoutPreview = (
|
||||
@@ -103,10 +109,12 @@ export default class BrowserS3PreviewLauncher extends React.Component<
|
||||
externalLayout: gdExternalLayout,
|
||||
options: PreviewOptions
|
||||
): Promise<any> => {
|
||||
if (!project || !layout || !externalLayout) return Promise.reject();
|
||||
this.setState({
|
||||
error: null,
|
||||
});
|
||||
|
||||
return this._prepareExporter().then(
|
||||
({ exporter, outputDir, browserS3FileSystem }) => {
|
||||
return this._prepareExporter()
|
||||
.then(({ exporter, outputDir, browserS3FileSystem }) => {
|
||||
exporter.exportExternalLayoutForPixiPreview(
|
||||
project,
|
||||
layout,
|
||||
@@ -128,23 +136,43 @@ export default class BrowserS3PreviewLauncher extends React.Component<
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
);
|
||||
})
|
||||
.catch((error: Error) => {
|
||||
this.setState({
|
||||
error,
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
render() {
|
||||
const { showPreviewLinkDialog, url } = this.state;
|
||||
if (!showPreviewLinkDialog) return null;
|
||||
const { showPreviewLinkDialog, url, error } = this.state;
|
||||
|
||||
return (
|
||||
<BrowserPreviewLinkDialog
|
||||
url={url}
|
||||
onClose={() =>
|
||||
this.setState({
|
||||
showPreviewLinkDialog: false,
|
||||
})
|
||||
}
|
||||
/>
|
||||
);
|
||||
if (error) {
|
||||
return (
|
||||
<BrowserPreviewErrorDialog
|
||||
error={error}
|
||||
onClose={() =>
|
||||
this.setState({
|
||||
error: null,
|
||||
})
|
||||
}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (showPreviewLinkDialog) {
|
||||
return (
|
||||
<BrowserPreviewLinkDialog
|
||||
url={url}
|
||||
onClose={() =>
|
||||
this.setState({
|
||||
showPreviewLinkDialog: false,
|
||||
})
|
||||
}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
@@ -1,61 +0,0 @@
|
||||
//TODO: Download the file then read it instead of hardcoding it.
|
||||
|
||||
export default `<html><head><meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
|
||||
|
||||
<meta name="apple-mobile-web-app-capable" content="yes">
|
||||
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">
|
||||
|
||||
<meta name="theme-color" content="#000000" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" />
|
||||
|
||||
<style>
|
||||
body {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
background-color: #000000;
|
||||
overflow: hidden;
|
||||
}
|
||||
#canvasArea {
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* GDJS_CUSTOM_STYLE */
|
||||
</style>
|
||||
<!-- Libs and GDJS core files : -->
|
||||
<!-- GDJS_CODE_FILES -->
|
||||
|
||||
</head>
|
||||
<body>
|
||||
<div id="canvasArea"></div>
|
||||
|
||||
<!-- GDJS_CUSTOM_HTML -->
|
||||
<script>
|
||||
|
||||
(function() {
|
||||
//Initialization
|
||||
gdjs.registerObjects();
|
||||
gdjs.registerBehaviors();
|
||||
gdjs.registerGlobalCallbacks();
|
||||
|
||||
var game = new gdjs.RuntimeGame(gdjs.projectData, {}/*GDJS_ADDITIONAL_SPEC*/);
|
||||
|
||||
//Create a renderer
|
||||
var canvasArea = document.getElementById("canvasArea");
|
||||
game.getRenderer().createStandardCanvas(canvasArea);
|
||||
|
||||
//Bind keyboards/mouse/touch events
|
||||
game.getRenderer().bindStandardEvents(game.getInputManager(), window, document);
|
||||
|
||||
//Load all assets and start the game
|
||||
game.loadAllAssets(function() {
|
||||
game.startGameLoop();
|
||||
});
|
||||
})();
|
||||
|
||||
</script>
|
||||
|
||||
|
||||
</body></html>
|
||||
`;
|
@@ -1,5 +1,15 @@
|
||||
// @flow
|
||||
import { Trans } from '@lingui/macro';
|
||||
import * as React from 'react';
|
||||
import BrowserExport from './BrowserExport';
|
||||
import { type Exporter } from '../ExportDialog';
|
||||
import { browserOnlineCordovaExportPipeline } from './BrowserOnlineCordovaExport.js';
|
||||
import { browserOnlineElectronExportPipeline } from './BrowserOnlineElectronExport.js';
|
||||
import { browserOnlineWebExportPipeline } from './BrowserOnlineWebExport';
|
||||
import { browserHTML5ExportPipeline } from './BrowserHTML5Export';
|
||||
import { browserCordovaExportPipeline } from './BrowserCordovaExport';
|
||||
import { browserElectronExportPipeline } from './BrowserElectronExport';
|
||||
import { browserCocos2dExportPipeline } from './BrowserCocos2dExport';
|
||||
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';
|
||||
@@ -7,76 +17,133 @@ import Facebook from '../../UI/CustomSvgIcons/Facebook';
|
||||
import Cordova from '../../UI/CustomSvgIcons/Cordova';
|
||||
import Chrome from '../../UI/CustomSvgIcons/Chrome';
|
||||
|
||||
export const getBrowserExporters = () => [
|
||||
export const getBrowserExporters = (): Array<Exporter> => [
|
||||
{
|
||||
name: 'Android (& iOS coming soon)',
|
||||
name: <Trans>Android (& iOS coming soon)</Trans>,
|
||||
renderIcon: props => <PhoneIphone {...props} />,
|
||||
description:
|
||||
'Package your game for Android directly from GDevelop. iOS support is coming soon!',
|
||||
key: 'localonlinecordovaexport',
|
||||
ExportComponent: BrowserExport,
|
||||
helpPage: '/publishing/android_and_ios',
|
||||
description: (
|
||||
<Trans>
|
||||
Package your game for Android directly from GDevelop. iOS support is
|
||||
coming soon!
|
||||
</Trans>
|
||||
),
|
||||
key: 'browseronlinecordovaexport',
|
||||
exportPipeline: browserOnlineCordovaExportPipeline,
|
||||
},
|
||||
{
|
||||
name: 'Facebook Instant Games',
|
||||
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 Kongregate, Game Jolt, itch.io, Poki...
|
||||
</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:
|
||||
'Package your game as a Facebook Instant Games that can be played on Facebook Messenger.',
|
||||
key: 'localfacebookinstantgames',
|
||||
ExportComponent: BrowserExport,
|
||||
},
|
||||
{
|
||||
name: 'Web (upload online)',
|
||||
renderIcon: props => <Chrome {...props} />,
|
||||
description:
|
||||
'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.',
|
||||
key: 'locals3export',
|
||||
ExportComponent: BrowserExport,
|
||||
},
|
||||
{
|
||||
name: 'Local folder',
|
||||
renderIcon: props => <Folder {...props} />,
|
||||
description:
|
||||
'Build the game locally as a HTML5 game. You can then export it on website like Itch.io or Kongregate.',
|
||||
key: 'localexport',
|
||||
ExportComponent: BrowserExport,
|
||||
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: 'iOS & Android (manual)',
|
||||
name: <Trans>iOS & Android (manual)</Trans>,
|
||||
renderIcon: props => <Cordova {...props} />,
|
||||
description:
|
||||
'Build the game locally as a Cordova project, and export it manually then to iOS or Android with Cordova developers tools.',
|
||||
key: 'localcordovaexport',
|
||||
ExportComponent: BrowserExport,
|
||||
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: 'Windows/macOS/Linux',
|
||||
name: <Trans>Windows/macOS/Linux</Trans>,
|
||||
renderIcon: props => <LaptopMac {...props} />,
|
||||
helpPage: '/publishing/windows-macos-linux',
|
||||
description:
|
||||
'Package your game as an app for Windows, macOS or Linux directly from GDevelop.',
|
||||
key: 'localonlineelectronexport',
|
||||
ExportComponent: BrowserExport,
|
||||
description: (
|
||||
<Trans>
|
||||
Package your game as an app for Windows, macOS or Linux directly from
|
||||
GDevelop.
|
||||
</Trans>
|
||||
),
|
||||
key: 'browseronlineelectronexport',
|
||||
exportPipeline: browserOnlineElectronExportPipeline,
|
||||
},
|
||||
{
|
||||
name: 'Windows/macOS/Linux (manual)',
|
||||
name: <Trans>Windows/macOS/Linux (manual)</Trans>,
|
||||
renderIcon: props => <LaptopMac {...props} />,
|
||||
helpPage: '/publishing/windows-macos-linux-with-electron',
|
||||
description:
|
||||
'Build the game locally and export it manually to Windows, macOS or Linux with third-party developer tools.',
|
||||
key: 'localelectronexport',
|
||||
ExportComponent: BrowserExport,
|
||||
description: (
|
||||
<Trans>
|
||||
Build the game locally and export it manually to Windows, macOS or Linux
|
||||
with third-party developer tools.
|
||||
</Trans>
|
||||
),
|
||||
key: 'browserelectronexport',
|
||||
exportPipeline: browserElectronExportPipeline,
|
||||
advanced: true,
|
||||
},
|
||||
{
|
||||
name: 'Cocos2d-JS',
|
||||
name: <Trans>Cocos2d-JS</Trans>,
|
||||
renderIcon: props => <PhoneIphone {...props} />,
|
||||
description:
|
||||
'Export your game using Cocos2d-JS game engine. The game can be compiled for Android or iOS if you install Cocos2d-JS developer tools.',
|
||||
key: 'localcocos2dexport',
|
||||
ExportComponent: BrowserExport,
|
||||
helpPage: '/publishing/android_and_ios_with_cocos2d-js',
|
||||
description: (
|
||||
<Trans>
|
||||
Export your game using Cocos2d-JS game engine. The game can be compiled
|
||||
for Android or iOS if you install Cocos2d-JS developer tools.
|
||||
</Trans>
|
||||
),
|
||||
key: 'browsercocos2dexport',
|
||||
exportPipeline: browserCocos2dExportPipeline,
|
||||
experimental: true,
|
||||
},
|
||||
];
|
||||
|
||||
/**
|
||||
* Open an URL generated from a blob, to download it with the specified filename.
|
||||
*/
|
||||
export const openBlobDownloadUrl = (url: string, filename: string) => {
|
||||
const { body } = document;
|
||||
if (!body) return;
|
||||
|
||||
// Not using Window.openExternalURL because blob urls are blocked
|
||||
// by Adblock Plus (and maybe other ad blockers).
|
||||
const a = document.createElement('a');
|
||||
body.appendChild(a);
|
||||
a.style.display = 'none';
|
||||
a.href = url;
|
||||
a.download = filename;
|
||||
a.click();
|
||||
body.removeChild(a);
|
||||
};
|
||||
|
@@ -1,3 +1,6 @@
|
||||
// @flow
|
||||
import { t } from '@lingui/macro';
|
||||
import { I18n } from '@lingui/react';
|
||||
import { Trans } from '@lingui/macro';
|
||||
import * as React from 'react';
|
||||
import RaisedButton from '../../UI/RaisedButton';
|
||||
@@ -7,10 +10,14 @@ import EmptyMessage from '../../UI/EmptyMessage';
|
||||
import difference_in_seconds from 'date-fns/difference_in_seconds';
|
||||
import LinearProgress from '@material-ui/core/LinearProgress';
|
||||
import Text from '../../UI/Text';
|
||||
import {
|
||||
type Build,
|
||||
type BuildArtifactKeyName,
|
||||
} from '../../Utils/GDevelopServices/Build';
|
||||
|
||||
const buildTypesConfig = {
|
||||
'cordova-build': {
|
||||
estimatedTimeInSeconds: () => 300,
|
||||
estimatedTimeInSeconds: (build: Build) => 300,
|
||||
completeDescription:
|
||||
'You can download it on your Android phone and install it.',
|
||||
},
|
||||
@@ -19,11 +26,42 @@ const buildTypesConfig = {
|
||||
150 * (build.targets ? build.targets.length : 0),
|
||||
completeDescription: '',
|
||||
},
|
||||
'web-build': {
|
||||
estimatedTimeInSeconds: (build: Build) => 5,
|
||||
completeDescription: '',
|
||||
},
|
||||
};
|
||||
|
||||
const buttons = [
|
||||
{
|
||||
displayName: t`Download`,
|
||||
key: 'apkKey',
|
||||
},
|
||||
{
|
||||
displayName: t`Windows (zip)`,
|
||||
key: 'windowsZipKey',
|
||||
},
|
||||
{
|
||||
displayName: t`Windows (exe)`,
|
||||
key: 'windowsExeKey',
|
||||
},
|
||||
{
|
||||
displayName: t`macOS (zip)`,
|
||||
key: 'macosZipKey',
|
||||
},
|
||||
{
|
||||
displayName: t`Linux (AppImage)`,
|
||||
key: 'linuxAppImageKey',
|
||||
},
|
||||
{
|
||||
displayName: t`Open`,
|
||||
key: 's3Key',
|
||||
},
|
||||
];
|
||||
|
||||
type Props = {|
|
||||
build: Build,
|
||||
onDownload: (key: string) => void,
|
||||
onDownload: (key: BuildArtifactKeyName) => void,
|
||||
|};
|
||||
|
||||
/**
|
||||
@@ -31,29 +69,6 @@ type Props = {|
|
||||
* to download the artifacts.
|
||||
*/
|
||||
export default ({ build, onDownload }: Props) => {
|
||||
const buttons = [
|
||||
{
|
||||
displayName: 'Download',
|
||||
key: 'apkKey',
|
||||
},
|
||||
{
|
||||
displayName: 'Windows (zip)',
|
||||
key: 'windowsZipKey',
|
||||
},
|
||||
{
|
||||
displayName: 'Windows (exe)',
|
||||
key: 'windowsExeKey',
|
||||
},
|
||||
{
|
||||
displayName: 'macOS (zip)',
|
||||
key: 'macosZipKey',
|
||||
},
|
||||
{
|
||||
displayName: 'Linux (AppImage)',
|
||||
key: 'linuxAppImageKey',
|
||||
},
|
||||
];
|
||||
|
||||
const config = buildTypesConfig[build.type];
|
||||
const secondsSinceLastUpdate = Math.abs(
|
||||
difference_in_seconds(build.updatedAt, Date.now())
|
||||
@@ -63,76 +78,89 @@ export default ({ build, onDownload }: Props) => {
|
||||
0
|
||||
);
|
||||
|
||||
return build.status === 'error' ? (
|
||||
<React.Fragment>
|
||||
<Line alignItems="center">
|
||||
<Text>
|
||||
<Trans>Something wrong happened :(</Trans>
|
||||
</Text>
|
||||
<Spacer />
|
||||
<RaisedButton
|
||||
label={<Trans>See logs</Trans>}
|
||||
onClick={() => onDownload('logsKey')}
|
||||
/>
|
||||
</Line>
|
||||
<Line alignItems="center">
|
||||
<EmptyMessage>
|
||||
<Trans>
|
||||
Check the logs to see if there is an explanation about what went
|
||||
wrong, or try again later.
|
||||
</Trans>
|
||||
</EmptyMessage>
|
||||
</Line>
|
||||
</React.Fragment>
|
||||
) : build.status === 'pending' ? (
|
||||
<Line alignItems="center" expand>
|
||||
<LinearProgress
|
||||
style={{ flex: 1 }}
|
||||
value={
|
||||
config.estimatedTimeInSeconds(build) > 0
|
||||
? ((config.estimatedTimeInSeconds(build) - estimatedRemainingTime) /
|
||||
config.estimatedTimeInSeconds(build)) *
|
||||
100
|
||||
: 0
|
||||
}
|
||||
variant={estimatedRemainingTime > 0 ? 'determinate' : 'indeterminate'}
|
||||
/>
|
||||
<Spacer />
|
||||
{estimatedRemainingTime > 0 ? (
|
||||
<Text>
|
||||
<Trans>~{Math.round(estimatedRemainingTime / 60)} minutes.</Trans>
|
||||
</Text>
|
||||
) : (
|
||||
<Text>
|
||||
<Trans>Should finish soon.</Trans>
|
||||
</Text>
|
||||
)}
|
||||
</Line>
|
||||
) : build.status === 'complete' ? (
|
||||
<React.Fragment>
|
||||
<Line expand>
|
||||
{buttons
|
||||
.filter(button => !!build[button.key])
|
||||
.map((button, index) => (
|
||||
<React.Fragment key={button.key}>
|
||||
{index !== 0 && <Spacer />}
|
||||
return (
|
||||
<I18n>
|
||||
{({ i18n }) =>
|
||||
build.status === 'error' ? (
|
||||
<React.Fragment>
|
||||
<Line alignItems="center">
|
||||
<Text>
|
||||
<Trans>Something wrong happened :(</Trans>
|
||||
</Text>
|
||||
<Spacer />
|
||||
<RaisedButton
|
||||
label={button.displayName}
|
||||
primary
|
||||
onClick={() => onDownload(button.key)}
|
||||
label={<Trans>See logs</Trans>}
|
||||
onClick={() => onDownload('logsKey')}
|
||||
/>
|
||||
</React.Fragment>
|
||||
))}
|
||||
<FlatButton
|
||||
label={<Trans>See logs</Trans>}
|
||||
onClick={() => onDownload('logsKey')}
|
||||
/>
|
||||
</Line>
|
||||
<Line expand>{config && <Text>{config.completeDescription}</Text>}</Line>
|
||||
</React.Fragment>
|
||||
) : (
|
||||
<Line>
|
||||
<Trans>Unknown status</Trans>
|
||||
</Line>
|
||||
</Line>
|
||||
<Line alignItems="center">
|
||||
<EmptyMessage>
|
||||
<Trans>
|
||||
Check the logs to see if there is an explanation about what
|
||||
went wrong, or try again later.
|
||||
</Trans>
|
||||
</EmptyMessage>
|
||||
</Line>
|
||||
</React.Fragment>
|
||||
) : build.status === 'pending' ? (
|
||||
<Line alignItems="center" expand>
|
||||
<LinearProgress
|
||||
style={{ flex: 1 }}
|
||||
value={
|
||||
config.estimatedTimeInSeconds(build) > 0
|
||||
? ((config.estimatedTimeInSeconds(build) -
|
||||
estimatedRemainingTime) /
|
||||
config.estimatedTimeInSeconds(build)) *
|
||||
100
|
||||
: 0
|
||||
}
|
||||
variant={
|
||||
estimatedRemainingTime > 0 ? 'determinate' : 'indeterminate'
|
||||
}
|
||||
/>
|
||||
<Spacer />
|
||||
{estimatedRemainingTime > 0 ? (
|
||||
<Text>
|
||||
<Trans>
|
||||
~{Math.round(estimatedRemainingTime / 60)} minutes.
|
||||
</Trans>
|
||||
</Text>
|
||||
) : (
|
||||
<Text>
|
||||
<Trans>Should finish soon.</Trans>
|
||||
</Text>
|
||||
)}
|
||||
</Line>
|
||||
) : build.status === 'complete' ? (
|
||||
<React.Fragment>
|
||||
<Line expand>
|
||||
{buttons
|
||||
.filter(button => !!build[button.key])
|
||||
.map((button, index) => (
|
||||
<React.Fragment key={button.key}>
|
||||
{index !== 0 && <Spacer />}
|
||||
<RaisedButton
|
||||
label={i18n._(button.displayName)}
|
||||
primary
|
||||
onClick={() => onDownload(button.key)}
|
||||
/>
|
||||
</React.Fragment>
|
||||
))}
|
||||
<FlatButton
|
||||
label={<Trans>See logs</Trans>}
|
||||
onClick={() => onDownload('logsKey')}
|
||||
/>
|
||||
</Line>
|
||||
<Line expand>
|
||||
{config && <Text>{config.completeDescription}</Text>}
|
||||
</Line>
|
||||
</React.Fragment>
|
||||
) : (
|
||||
<Line>
|
||||
<Trans>Unknown status</Trans>
|
||||
</Line>
|
||||
)
|
||||
}
|
||||
</I18n>
|
||||
);
|
||||
};
|
||||
|
@@ -8,11 +8,15 @@ import StepLabel from '@material-ui/core/StepLabel';
|
||||
import StepContent from '@material-ui/core/StepContent';
|
||||
import CircularProgress from '@material-ui/core/CircularProgress';
|
||||
import LinearProgress from '@material-ui/core/LinearProgress';
|
||||
import { Line, Spacer } from '../../UI/Grid';
|
||||
import { Line, Spacer, Column } from '../../UI/Grid';
|
||||
import BuildProgress from './BuildProgress';
|
||||
import { type Build } from '../../Utils/GDevelopServices/Build';
|
||||
import {
|
||||
type Build,
|
||||
type BuildArtifactKeyName,
|
||||
} from '../../Utils/GDevelopServices/Build';
|
||||
import EmptyMessage from '../../UI/EmptyMessage';
|
||||
import Text from '../../UI/Text';
|
||||
import AlertMessage from '../../UI/AlertMessage';
|
||||
|
||||
const styles = {
|
||||
stepper: { flex: 1 },
|
||||
@@ -22,19 +26,22 @@ const styles = {
|
||||
export type BuildStep =
|
||||
| ''
|
||||
| 'export'
|
||||
| 'resources-download'
|
||||
| 'compress'
|
||||
| 'upload'
|
||||
| 'waiting-for-build'
|
||||
| 'build';
|
||||
| 'build'
|
||||
| 'done';
|
||||
|
||||
type Props = {|
|
||||
exportStep: BuildStep,
|
||||
onDownload: (key: string) => void,
|
||||
onDownload: (key: BuildArtifactKeyName) => void,
|
||||
build: ?Build,
|
||||
uploadMax: number,
|
||||
uploadProgress: number,
|
||||
stepMaxProgress: number,
|
||||
stepCurrentProgress: number,
|
||||
errored: boolean,
|
||||
showSeeAllMyBuildsExplanation?: boolean,
|
||||
hasBuildStep: boolean,
|
||||
|};
|
||||
|
||||
/**
|
||||
@@ -45,19 +52,24 @@ export default ({
|
||||
exportStep,
|
||||
onDownload,
|
||||
build,
|
||||
uploadMax,
|
||||
uploadProgress,
|
||||
stepMaxProgress,
|
||||
stepCurrentProgress,
|
||||
errored,
|
||||
hasBuildStep,
|
||||
showSeeAllMyBuildsExplanation,
|
||||
}: Props) => (
|
||||
<Stepper
|
||||
activeStep={
|
||||
exportStep === 'export'
|
||||
exportStep === 'export' || exportStep === 'resources-download'
|
||||
? 0
|
||||
: exportStep === 'compress' || exportStep === 'upload'
|
||||
? 1
|
||||
: exportStep === 'waiting-for-build' || exportStep === 'build'
|
||||
? 2
|
||||
: exportStep === 'done'
|
||||
? hasBuildStep
|
||||
? 2
|
||||
: 1
|
||||
: -1
|
||||
}
|
||||
orientation="vertical"
|
||||
@@ -67,76 +79,119 @@ export default ({
|
||||
<StepLabel>
|
||||
<Trans>Game export</Trans>
|
||||
</StepLabel>
|
||||
<StepContent>
|
||||
<Line alignItems="center">
|
||||
<CircularProgress size={20} />
|
||||
<Spacer />
|
||||
<Text>
|
||||
<Trans>Export in progress...</Trans>
|
||||
</Text>
|
||||
</Line>
|
||||
</StepContent>
|
||||
</Step>
|
||||
<Step>
|
||||
<StepLabel>
|
||||
<Trans>Upload to build service</Trans>
|
||||
</StepLabel>
|
||||
<StepContent>
|
||||
{errored ? (
|
||||
<Text>
|
||||
<AlertMessage kind="error">
|
||||
<Trans>Can't properly export the game.</Trans>{' '}
|
||||
<Trans>
|
||||
Can't upload your game to the build service. Please check your
|
||||
internet connection or try again later.
|
||||
Please check your internet connection or try again later.
|
||||
</Trans>
|
||||
</Text>
|
||||
) : exportStep === 'compress' ? (
|
||||
<Line alignItems="center">
|
||||
<CircularProgress size={20} />
|
||||
<Spacer />
|
||||
</AlertMessage>
|
||||
) : exportStep === 'resources-download' ? (
|
||||
<Column expand noMargin>
|
||||
<Text>
|
||||
<Trans>Compressing before upload...</Trans>
|
||||
<Trans>Downloading game resources...</Trans>
|
||||
</Text>
|
||||
</Line>
|
||||
<Line expand>
|
||||
<LinearProgress
|
||||
style={styles.linearProgress}
|
||||
value={
|
||||
stepMaxProgress > 0
|
||||
? (stepCurrentProgress / stepMaxProgress) * 100
|
||||
: 0
|
||||
}
|
||||
variant="determinate"
|
||||
/>
|
||||
</Line>
|
||||
</Column>
|
||||
) : (
|
||||
<Line alignItems="center" expand>
|
||||
<LinearProgress
|
||||
style={styles.linearProgress}
|
||||
value={uploadMax > 0 ? (uploadProgress / uploadMax) * 100 : 0}
|
||||
variant="determinate"
|
||||
/>
|
||||
</Line>
|
||||
)}
|
||||
</StepContent>
|
||||
</Step>
|
||||
<Step>
|
||||
<StepLabel>
|
||||
<Trans>Build and download</Trans>
|
||||
</StepLabel>
|
||||
<StepContent>
|
||||
{errored && (
|
||||
<Text>
|
||||
<Trans>
|
||||
Build could not start or errored. Please check your internet
|
||||
connection or try again later.
|
||||
</Trans>
|
||||
</Text>
|
||||
)}
|
||||
{!build && !errored && (
|
||||
<Text>
|
||||
<Trans>Build is starting...</Trans>
|
||||
</Text>
|
||||
)}
|
||||
{build && <BuildProgress build={build} onDownload={onDownload} />}
|
||||
{showSeeAllMyBuildsExplanation && (
|
||||
<EmptyMessage>
|
||||
<Trans>
|
||||
If you close this window while the build is being done, you can
|
||||
see its progress and download the game later by clicking on See
|
||||
All My Builds below.
|
||||
</Trans>
|
||||
</EmptyMessage>
|
||||
<Column expand noMargin>
|
||||
<Text>
|
||||
<Trans>Export in progress...</Trans>
|
||||
</Text>
|
||||
<Line expand>
|
||||
<LinearProgress style={styles.linearProgress} />
|
||||
</Line>
|
||||
</Column>
|
||||
)}
|
||||
</StepContent>
|
||||
</Step>
|
||||
{hasBuildStep && (
|
||||
<Step>
|
||||
<StepLabel>
|
||||
<Trans>Upload to build service</Trans>
|
||||
</StepLabel>
|
||||
<StepContent>
|
||||
{errored ? (
|
||||
<AlertMessage kind="error">
|
||||
<Trans>Can't upload your game to the build service.</Trans>{' '}
|
||||
<Trans>
|
||||
Please check your internet connection or try again later.
|
||||
</Trans>
|
||||
</AlertMessage>
|
||||
) : exportStep === 'compress' ? (
|
||||
<Line alignItems="center">
|
||||
<CircularProgress size={20} />
|
||||
<Spacer />
|
||||
<Text>
|
||||
<Trans>Compressing before upload...</Trans>
|
||||
</Text>
|
||||
</Line>
|
||||
) : (
|
||||
<Line alignItems="center" expand>
|
||||
<LinearProgress
|
||||
style={styles.linearProgress}
|
||||
value={
|
||||
stepMaxProgress > 0
|
||||
? (stepCurrentProgress / stepMaxProgress) * 100
|
||||
: 0
|
||||
}
|
||||
variant="determinate"
|
||||
/>
|
||||
</Line>
|
||||
)}
|
||||
</StepContent>
|
||||
</Step>
|
||||
)}
|
||||
{hasBuildStep && (
|
||||
<Step>
|
||||
<StepLabel>
|
||||
<Trans>Build and download</Trans>
|
||||
</StepLabel>
|
||||
<StepContent>
|
||||
{errored && (
|
||||
<AlertMessage kind="error">
|
||||
<Trans>
|
||||
Build could not start or errored. Please check your internet
|
||||
connection or try again later.
|
||||
</Trans>
|
||||
</AlertMessage>
|
||||
)}
|
||||
{!build && !errored && (
|
||||
<Text>
|
||||
<Trans>Build is starting...</Trans>
|
||||
</Text>
|
||||
)}
|
||||
{build && <BuildProgress build={build} onDownload={onDownload} />}
|
||||
{showSeeAllMyBuildsExplanation && (
|
||||
<EmptyMessage>
|
||||
<Trans>
|
||||
If you close this window while the build is being done, you can
|
||||
see its progress and download the game later by clicking on See
|
||||
All My Builds below.
|
||||
</Trans>
|
||||
</EmptyMessage>
|
||||
)}
|
||||
</StepContent>
|
||||
</Step>
|
||||
)}
|
||||
{!hasBuildStep && (
|
||||
<Step>
|
||||
<StepLabel>
|
||||
<Trans>Done</Trans>
|
||||
</StepLabel>
|
||||
<StepContent />
|
||||
</Step>
|
||||
)}
|
||||
</Stepper>
|
||||
);
|
||||
|
@@ -3,7 +3,10 @@ import { Trans } from '@lingui/macro';
|
||||
|
||||
import * as React from 'react';
|
||||
import Paper from '@material-ui/core/Paper';
|
||||
import { type Build } from '../../Utils/GDevelopServices/Build';
|
||||
import {
|
||||
type Build,
|
||||
type BuildArtifactKeyName,
|
||||
} from '../../Utils/GDevelopServices/Build';
|
||||
import { Column, Line } from '../../UI/Grid';
|
||||
import EmptyMessage from '../../UI/EmptyMessage';
|
||||
import PlaceholderLoader from '../../UI/PlaceholderLoader';
|
||||
@@ -14,7 +17,7 @@ import Text from '../../UI/Text';
|
||||
|
||||
type Props = {|
|
||||
builds: ?Array<Build>,
|
||||
onDownload: (build: Build, key: string) => void,
|
||||
onDownload: (build: Build, key: BuildArtifactKeyName) => void,
|
||||
|};
|
||||
|
||||
const styles = {
|
||||
|
@@ -5,6 +5,7 @@ import { type UserProfile } from '../../Profile/UserProfileContext';
|
||||
|
||||
const waitTime = 1500;
|
||||
const bulkWaitTime = 5000;
|
||||
const maxTimeBeforeIgnoring = 12 * 60 * 60 * 1000; // 12 hours in milliseconds
|
||||
|
||||
export default class BuildsWatcher {
|
||||
runningWatchers: { [string]: boolean } = {};
|
||||
@@ -27,7 +28,22 @@ export default class BuildsWatcher {
|
||||
|
||||
builds.forEach(build => {
|
||||
if (build.status === 'pending') {
|
||||
this._pollBuild(build.id, builds.length > 1 ? bulkWaitTime : waitTime);
|
||||
if (
|
||||
(!build.createdAt ||
|
||||
build.createdAt < Date.now() - maxTimeBeforeIgnoring) &&
|
||||
(!build.updatedAt ||
|
||||
build.updatedAt < Date.now() - maxTimeBeforeIgnoring)
|
||||
) {
|
||||
console.info(
|
||||
"Ignoring a build for polling as it's too old and still pending",
|
||||
build
|
||||
);
|
||||
} else {
|
||||
this._pollBuild(
|
||||
build.id,
|
||||
builds.length > 1 ? bulkWaitTime : waitTime
|
||||
);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
@@ -5,7 +5,8 @@ import BuildsList from './BuildsList';
|
||||
import {
|
||||
getBuilds,
|
||||
type Build,
|
||||
getUrl,
|
||||
type BuildArtifactKeyName,
|
||||
getBuildArtifactUrl,
|
||||
} from '../../Utils/GDevelopServices/Build';
|
||||
import Window from '../../Utils/Window';
|
||||
import BuildsWatcher from './BuildsWatcher';
|
||||
@@ -74,10 +75,9 @@ export default class Builds extends Component<Props, State> {
|
||||
);
|
||||
};
|
||||
|
||||
_download = (build: Build, key: string) => {
|
||||
if (!build || !build[key]) return;
|
||||
|
||||
Window.openExternalURL(getUrl(build[key]));
|
||||
_download = (build: Build, key: BuildArtifactKeyName) => {
|
||||
const url = getBuildArtifactUrl(build, key);
|
||||
if (url) Window.openExternalURL(url);
|
||||
};
|
||||
|
||||
render() {
|
||||
|
@@ -1,6 +1,6 @@
|
||||
// @flow
|
||||
import { Trans } from '@lingui/macro';
|
||||
import React, { Component } from 'react';
|
||||
import * as React from 'react';
|
||||
import Dialog from '../UI/Dialog';
|
||||
import HelpButton from '../UI/HelpButton';
|
||||
import FlatButton from '../UI/FlatButton';
|
||||
@@ -10,10 +10,13 @@ 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 Authentification from '../Utils/GDevelopServices/Authentification';
|
||||
import UserProfileContext, {
|
||||
type UserProfile,
|
||||
} from '../Profile/UserProfileContext';
|
||||
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 },
|
||||
@@ -21,18 +24,30 @@ const styles = {
|
||||
content: { padding: 24 },
|
||||
};
|
||||
|
||||
export type Exporter = any; // TODO: Add typing
|
||||
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,
|
||||
authentification: Authentification,
|
||||
onChangeSubscription: () => void,
|
||||
|};
|
||||
|
||||
type Props = {|
|
||||
...ExportDialogWithoutExportsProps,
|
||||
exporters: Array<Exporter>,
|
||||
allExportersRequireOnline?: boolean,
|
||||
|};
|
||||
|
||||
type State = {|
|
||||
@@ -41,7 +56,7 @@ type State = {|
|
||||
buildsDialogOpen: boolean,
|
||||
|};
|
||||
|
||||
export default class ExportDialog extends Component<Props, State> {
|
||||
export default class ExportDialog extends React.Component<Props, State> {
|
||||
state = {
|
||||
chosenExporterKey: '',
|
||||
showExperimental: false,
|
||||
@@ -66,12 +81,18 @@ export default class ExportDialog extends Component<Props, State> {
|
||||
});
|
||||
};
|
||||
|
||||
_renderExporterListItem = (exporter: Exporter, index: number) => {
|
||||
_renderExporterListItem = (
|
||||
exporter: Exporter,
|
||||
index: number,
|
||||
forceDisable: boolean
|
||||
) => {
|
||||
return (
|
||||
<ListItem
|
||||
key={exporter.key}
|
||||
disabled={exporter.disabled}
|
||||
style={exporter.disabled ? styles.disabledItem : undefined}
|
||||
disabled={forceDisable || exporter.disabled}
|
||||
style={
|
||||
forceDisable || exporter.disabled ? styles.disabledItem : undefined
|
||||
}
|
||||
leftIcon={exporter.renderIcon({ style: styles.icon })}
|
||||
primaryText={exporter.name}
|
||||
secondaryText={exporter.description}
|
||||
@@ -85,7 +106,7 @@ export default class ExportDialog extends Component<Props, State> {
|
||||
const {
|
||||
project,
|
||||
onClose,
|
||||
authentification, //Still exist?
|
||||
allExportersRequireOnline,
|
||||
onChangeSubscription,
|
||||
exporters,
|
||||
} = this.props;
|
||||
@@ -99,103 +120,135 @@ export default class ExportDialog extends Component<Props, State> {
|
||||
return (
|
||||
<UserProfileContext.Consumer>
|
||||
{(userProfile: UserProfile) => (
|
||||
<Dialog
|
||||
title={<Trans>Export project to a standalone game</Trans>}
|
||||
onRequestClose={onClose}
|
||||
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
|
||||
>
|
||||
{!exporter && (
|
||||
<React.Fragment>
|
||||
<List>
|
||||
{exporters
|
||||
.filter(
|
||||
exporter => !exporter.advanced && !exporter.experimental
|
||||
)
|
||||
.map((exporter, index) =>
|
||||
this._renderExporterListItem(exporter, index)
|
||||
)}
|
||||
|
||||
<Subheader>Advanced</Subheader>
|
||||
{exporters
|
||||
.filter(exporter => exporter.advanced)
|
||||
.map((exporter, index) =>
|
||||
this._renderExporterListItem(exporter, index)
|
||||
)}
|
||||
|
||||
{showExperimental && <Subheader>Experimental</Subheader>}
|
||||
{showExperimental &&
|
||||
exporters
|
||||
.filter(exporter => exporter.experimental)
|
||||
.map((exporter, index) =>
|
||||
this._renderExporterListItem(exporter, index)
|
||||
)}
|
||||
</List>
|
||||
<Line justifyContent="center" alignItems="center">
|
||||
{!showExperimental ? (
|
||||
<OnlineStatus>
|
||||
{onlineStatus => {
|
||||
const cantExportBecauseOffline =
|
||||
!!allExportersRequireOnline && !onlineStatus;
|
||||
return (
|
||||
<Dialog
|
||||
title={<Trans>Export project to a standalone game</Trans>}
|
||||
onRequestClose={onClose}
|
||||
actions={[
|
||||
chosenExporterKey && (
|
||||
<FlatButton
|
||||
label={<Trans>Back</Trans>}
|
||||
key="back"
|
||||
primary={false}
|
||||
onClick={() => this.chooseExporter('')}
|
||||
/>
|
||||
),
|
||||
<FlatButton
|
||||
key="toggle-experimental"
|
||||
icon={<Visibility />}
|
||||
label={<Trans>Close</Trans>}
|
||||
key="close"
|
||||
primary={false}
|
||||
onClick={() => this._showExperimental(true)}
|
||||
label={<Trans>Show experimental exports</Trans>}
|
||||
/>
|
||||
) : (
|
||||
onClick={onClose}
|
||||
/>,
|
||||
]}
|
||||
secondaryActions={[
|
||||
<HelpButton
|
||||
key="help"
|
||||
helpPagePath={
|
||||
(exporter && exporter.helpPage) || '/publishing'
|
||||
}
|
||||
/>,
|
||||
<FlatButton
|
||||
key="toggle-experimental"
|
||||
icon={<VisibilityOff />}
|
||||
primary={false}
|
||||
onClick={() => this._showExperimental(false)}
|
||||
label={<Trans>Hide experimental exports</Trans>}
|
||||
/>
|
||||
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>
|
||||
)}
|
||||
</Line>
|
||||
</React.Fragment>
|
||||
)}
|
||||
{exporter && (
|
||||
<div style={styles.content}>
|
||||
<exporter.ExportComponent
|
||||
project={project}
|
||||
authentification={authentification} //Still exist?
|
||||
onChangeSubscription={onChangeSubscription}
|
||||
onOpenBuildsDialog={this._openBuildsDialog}
|
||||
userProfile={userProfile}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
<BuildsDialog
|
||||
open={this.state.buildsDialogOpen}
|
||||
onClose={() => this._openBuildsDialog(false)}
|
||||
userProfile={userProfile}
|
||||
/>
|
||||
</Dialog>
|
||||
{!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">
|
||||
{!showExperimental ? (
|
||||
<FlatButton
|
||||
key="toggle-experimental"
|
||||
icon={<Visibility />}
|
||||
primary={false}
|
||||
onClick={() => this._showExperimental(true)}
|
||||
label={<Trans>Show experimental exports</Trans>}
|
||||
/>
|
||||
) : (
|
||||
<FlatButton
|
||||
key="toggle-experimental"
|
||||
icon={<VisibilityOff />}
|
||||
primary={false}
|
||||
onClick={() => this._showExperimental(false)}
|
||||
label={<Trans>Hide experimental exports</Trans>}
|
||||
/>
|
||||
)}
|
||||
</Line>
|
||||
</React.Fragment>
|
||||
)}
|
||||
{exporter && exporter.exportPipeline && (
|
||||
<div style={styles.content}>
|
||||
<ExportLauncher
|
||||
exportPipeline={exporter.exportPipeline}
|
||||
project={project}
|
||||
onChangeSubscription={onChangeSubscription}
|
||||
userProfile={userProfile}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
<BuildsDialog
|
||||
open={this.state.buildsDialogOpen}
|
||||
onClose={() => this._openBuildsDialog(false)}
|
||||
userProfile={userProfile}
|
||||
/>
|
||||
</Dialog>
|
||||
);
|
||||
}}
|
||||
</OnlineStatus>
|
||||
)}
|
||||
</UserProfileContext.Consumer>
|
||||
);
|
||||
|
291
newIDE/app/src/Export/ExportLauncher.js
Normal file
@@ -0,0 +1,291 @@
|
||||
// @flow
|
||||
|
||||
import React, { Component } from 'react';
|
||||
import RaisedButton from '../UI/RaisedButton';
|
||||
import { sendExportLaunched } from '../Utils/Analytics/EventSender';
|
||||
import {
|
||||
type Build,
|
||||
type BuildArtifactKeyName,
|
||||
getBuildArtifactUrl,
|
||||
} from '../Utils/GDevelopServices/Build';
|
||||
import { type UserProfile } from '../Profile/UserProfileContext';
|
||||
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';
|
||||
import {
|
||||
displayProjectErrorsBox,
|
||||
getErrors,
|
||||
} from '../ProjectManager/ProjectErrorsChecker';
|
||||
import { type Limit } from '../Utils/GDevelopServices/Usage';
|
||||
import BuildsWatcher from './Builds/BuildsWatcher';
|
||||
import BuildStepsProgress, {
|
||||
type BuildStep,
|
||||
} from './Builds/BuildStepsProgress';
|
||||
import { type ExportPipeline } from './ExportPipeline.flow';
|
||||
|
||||
type State = {|
|
||||
exportStep: BuildStep,
|
||||
compressionOutput: any,
|
||||
build: ?Build,
|
||||
stepCurrentProgress: number,
|
||||
stepMaxProgress: number,
|
||||
errored: boolean,
|
||||
exportState: any,
|
||||
doneFooterOpen: boolean,
|
||||
|};
|
||||
|
||||
type Props = {|
|
||||
project: gdProject,
|
||||
onChangeSubscription: () => void,
|
||||
userProfile: UserProfile,
|
||||
exportPipeline: ExportPipeline<any, any, any, any, any>,
|
||||
|};
|
||||
|
||||
/**
|
||||
* A generic UI to launch, monitor the progres and get the result
|
||||
* of an export.
|
||||
*/
|
||||
export default class ExportLauncher extends Component<Props, State> {
|
||||
state = {
|
||||
exportStep: '',
|
||||
build: null,
|
||||
compressionOutput: null,
|
||||
stepCurrentProgress: 0,
|
||||
stepMaxProgress: 0,
|
||||
doneFooterOpen: false,
|
||||
errored: false,
|
||||
exportState: this.props.exportPipeline.getInitialExportState(
|
||||
this.props.project
|
||||
),
|
||||
};
|
||||
buildsWatcher = new BuildsWatcher();
|
||||
|
||||
componentWillUnmount() {
|
||||
this.buildsWatcher.stop();
|
||||
}
|
||||
|
||||
_updateStepProgress = (
|
||||
stepCurrentProgress: number,
|
||||
stepMaxProgress: number
|
||||
) =>
|
||||
this.setState({
|
||||
stepCurrentProgress,
|
||||
stepMaxProgress,
|
||||
});
|
||||
|
||||
_startBuildWatch = (userProfile: UserProfile) => {
|
||||
if (!this.state.build) return;
|
||||
|
||||
this.buildsWatcher.start({
|
||||
userProfile,
|
||||
builds: [this.state.build],
|
||||
onBuildUpdated: (build: Build) => this.setState({ build }),
|
||||
});
|
||||
};
|
||||
|
||||
launchWholeExport = (userProfile: UserProfile) => {
|
||||
const t = str => str; //TODO;
|
||||
const { project, exportPipeline } = this.props;
|
||||
sendExportLaunched(exportPipeline.name);
|
||||
|
||||
if (!displayProjectErrorsBox(t, getErrors(t, project))) return;
|
||||
|
||||
const handleError = (message: string) => (err: Error) => {
|
||||
if (!this.state.errored) {
|
||||
this.setState({
|
||||
errored: true,
|
||||
});
|
||||
showErrorBox(message + (err.message ? `\n${err.message}` : ''), {
|
||||
exportStep: this.state.exportStep,
|
||||
rawError: err,
|
||||
});
|
||||
}
|
||||
|
||||
throw err;
|
||||
};
|
||||
|
||||
const exportPipelineContext = {
|
||||
project,
|
||||
updateStepProgress: this._updateStepProgress,
|
||||
exportState: this.state.exportState,
|
||||
};
|
||||
|
||||
this.setState({
|
||||
exportStep: 'export',
|
||||
stepCurrentProgress: 0,
|
||||
stepMaxProgress: 0,
|
||||
errored: false,
|
||||
build: null,
|
||||
});
|
||||
exportPipeline
|
||||
.prepareExporter(exportPipelineContext)
|
||||
.then(preparedExporter => {
|
||||
return exportPipeline.launchExport(
|
||||
exportPipelineContext,
|
||||
preparedExporter
|
||||
);
|
||||
}, handleError(t('Error while preparing the exporter.')))
|
||||
.then(exportOutput => {
|
||||
this.setState({
|
||||
exportStep: 'resources-download',
|
||||
});
|
||||
return exportPipeline.launchResourcesDownload(
|
||||
exportPipelineContext,
|
||||
exportOutput
|
||||
);
|
||||
}, handleError(t('Error while exporting the game.')))
|
||||
.then(resourcesDownloadOutput => {
|
||||
this.setState({
|
||||
exportStep: 'compress',
|
||||
});
|
||||
return exportPipeline.launchCompression(
|
||||
exportPipelineContext,
|
||||
resourcesDownloadOutput
|
||||
);
|
||||
}, handleError(t('Error while exporting the game.')))
|
||||
.then(compressionOutput => {
|
||||
const { launchUpload, launchOnlineBuild } = exportPipeline;
|
||||
if (!!launchUpload && !!launchOnlineBuild) {
|
||||
this.setState({
|
||||
exportStep: 'upload',
|
||||
});
|
||||
return launchUpload(exportPipelineContext, compressionOutput)
|
||||
.then((uploadBucketKey: string) => {
|
||||
this.setState({
|
||||
exportStep: 'waiting-for-build',
|
||||
});
|
||||
return launchOnlineBuild(
|
||||
this.state.exportState,
|
||||
userProfile,
|
||||
uploadBucketKey
|
||||
);
|
||||
}, handleError(t('Error while uploading the game. Check your internet connection or try again later.')))
|
||||
.then(build => {
|
||||
this.setState(
|
||||
{
|
||||
build,
|
||||
exportStep: 'build',
|
||||
},
|
||||
() => {
|
||||
this._startBuildWatch(userProfile);
|
||||
}
|
||||
);
|
||||
|
||||
return { compressionOutput };
|
||||
}, handleError(t('Error while lauching the build of the game.')));
|
||||
}
|
||||
|
||||
return { compressionOutput };
|
||||
}, handleError(t('Error while compressing the game.')))
|
||||
.then(({ compressionOutput }) => {
|
||||
this.setState({
|
||||
compressionOutput,
|
||||
doneFooterOpen: true,
|
||||
exportStep: 'done',
|
||||
});
|
||||
})
|
||||
.catch(() => {
|
||||
/* Error handled previously */
|
||||
});
|
||||
};
|
||||
|
||||
_downloadBuild = (key: BuildArtifactKeyName) => {
|
||||
const url = getBuildArtifactUrl(this.state.build, key);
|
||||
if (url) Window.openExternalURL(url);
|
||||
};
|
||||
|
||||
_closeDoneFooter = () =>
|
||||
this.setState({
|
||||
doneFooterOpen: false,
|
||||
});
|
||||
|
||||
_updateExportState = (updater: any => any) => {
|
||||
this.setState(prevState => ({
|
||||
...prevState,
|
||||
exportState: updater(prevState.exportState),
|
||||
}));
|
||||
};
|
||||
|
||||
render() {
|
||||
const {
|
||||
exportStep,
|
||||
compressionOutput,
|
||||
build,
|
||||
stepMaxProgress,
|
||||
stepCurrentProgress,
|
||||
errored,
|
||||
doneFooterOpen,
|
||||
exportState,
|
||||
} = this.state;
|
||||
const { project, userProfile, exportPipeline } = this.props;
|
||||
if (!project) return null;
|
||||
|
||||
const getBuildLimit = (userProfile: UserProfile): ?Limit =>
|
||||
userProfile.limits && exportPipeline.onlineBuildType
|
||||
? userProfile.limits[exportPipeline.onlineBuildType]
|
||||
: null;
|
||||
const canLaunchBuild = (userProfile: UserProfile) => {
|
||||
if (!errored && exportStep !== '' && exportStep !== 'done') return false;
|
||||
|
||||
const limit: ?Limit = getBuildLimit(userProfile);
|
||||
if (limit && limit.limitReached) return false;
|
||||
|
||||
return exportPipeline.canLaunchBuild(exportState);
|
||||
};
|
||||
|
||||
return (
|
||||
<Column noMargin>
|
||||
<Line>
|
||||
{exportPipeline.renderHeader({
|
||||
project,
|
||||
exportState,
|
||||
updateExportState: this._updateExportState,
|
||||
})}
|
||||
</Line>
|
||||
{(!exportPipeline.onlineBuildType || userProfile.authenticated) && (
|
||||
<Line justifyContent="center">
|
||||
<RaisedButton
|
||||
label={exportPipeline.renderLaunchButtonLabel()}
|
||||
primary
|
||||
onClick={() => this.launchWholeExport(userProfile)}
|
||||
disabled={!canLaunchBuild(userProfile)}
|
||||
/>
|
||||
</Line>
|
||||
)}
|
||||
{!!exportPipeline.onlineBuildType && userProfile.authenticated && (
|
||||
<LimitDisplayer
|
||||
subscription={userProfile.subscription}
|
||||
limit={getBuildLimit(userProfile)}
|
||||
onChangeSubscription={this.props.onChangeSubscription}
|
||||
/>
|
||||
)}
|
||||
{!!exportPipeline.onlineBuildType && !userProfile.authenticated && (
|
||||
<CreateProfile
|
||||
onLogin={userProfile.onLogin}
|
||||
onCreateAccount={userProfile.onCreateAccount}
|
||||
/>
|
||||
)}
|
||||
<Line expand>
|
||||
<BuildStepsProgress
|
||||
exportStep={exportStep}
|
||||
hasBuildStep={!!exportPipeline.onlineBuildType}
|
||||
build={build}
|
||||
onDownload={this._downloadBuild}
|
||||
stepMaxProgress={stepMaxProgress}
|
||||
stepCurrentProgress={stepCurrentProgress}
|
||||
errored={errored}
|
||||
/>
|
||||
</Line>
|
||||
{doneFooterOpen &&
|
||||
exportPipeline.renderDoneFooter &&
|
||||
exportPipeline.renderDoneFooter({
|
||||
compressionOutput,
|
||||
exportState,
|
||||
onClose: this._closeDoneFooter,
|
||||
})}
|
||||
</Column>
|
||||
);
|
||||
}
|
||||
}
|
86
newIDE/app/src/Export/ExportPipeline.flow.js
Normal file
@@ -0,0 +1,86 @@
|
||||
// @flow
|
||||
import * as React from 'react';
|
||||
import { type Build } from '../Utils/GDevelopServices/Build';
|
||||
import { type UserProfile } from '../Profile/UserProfileContext';
|
||||
|
||||
export type ExportPipelineContext<ExportState> = {|
|
||||
project: gdProject,
|
||||
exportState: ExportState,
|
||||
updateStepProgress: (count: number, total: number) => void,
|
||||
|};
|
||||
|
||||
/**
|
||||
* An export pipeline describing how to export and build a game.
|
||||
*/
|
||||
export type ExportPipeline<
|
||||
ExportState,
|
||||
PreparedExporter,
|
||||
ExportOutput,
|
||||
ResourcesDownloadOutput,
|
||||
CompressionOutput
|
||||
> = {|
|
||||
name: string,
|
||||
onlineBuildType?: string,
|
||||
|
||||
getInitialExportState: (project: gdProject) => ExportState,
|
||||
|
||||
renderHeader: ({|
|
||||
project: gdProject,
|
||||
exportState: ExportState,
|
||||
updateExportState: (
|
||||
updater: (prevExportState: ExportState) => ExportState
|
||||
) => void,
|
||||
|}) => React.Node,
|
||||
renderLaunchButtonLabel: () => React.Node,
|
||||
|
||||
canLaunchBuild: (exportState: ExportState) => boolean,
|
||||
|
||||
prepareExporter: (
|
||||
context: ExportPipelineContext<ExportState>
|
||||
) => Promise<PreparedExporter>,
|
||||
|
||||
launchExport: (
|
||||
context: ExportPipelineContext<ExportState>,
|
||||
preparedExporter: PreparedExporter
|
||||
) => Promise<ExportOutput>,
|
||||
|
||||
launchResourcesDownload: (
|
||||
context: ExportPipelineContext<ExportState>,
|
||||
exportOutput: ExportOutput
|
||||
) => Promise<ResourcesDownloadOutput>,
|
||||
|
||||
launchCompression: (
|
||||
context: ExportPipelineContext<ExportState>,
|
||||
resourcesDownloadOutput: ResourcesDownloadOutput
|
||||
) => Promise<CompressionOutput>,
|
||||
|
||||
/**
|
||||
* Launch the upload of the archive to the online build service.
|
||||
* This step is only done if `launchUpload` and `launchOnlineBuild`
|
||||
* are defined.
|
||||
*/
|
||||
launchUpload?: (
|
||||
context: ExportPipelineContext<ExportState>,
|
||||
compressionOutput: CompressionOutput
|
||||
) => Promise<string>,
|
||||
|
||||
/**
|
||||
* Launch the online build of the uploaded archive.
|
||||
* This step is only done if `launchUpload` and `launchOnlineBuild`
|
||||
* are defined.
|
||||
*/
|
||||
launchOnlineBuild?: (
|
||||
exportState: ExportState,
|
||||
userProfile: UserProfile,
|
||||
uploadBucketKey: string
|
||||
) => Promise<Build>,
|
||||
|
||||
/**
|
||||
* Render the footer when the whole export (+ online build if any) is done.
|
||||
*/
|
||||
renderDoneFooter?: ({|
|
||||
compressionOutput: CompressionOutput,
|
||||
exportState: ExportState,
|
||||
onClose: () => void,
|
||||
|}) => React.Node,
|
||||
|};
|
43
newIDE/app/src/Export/GenericExporters/Cocos2dExport.js
Normal file
@@ -0,0 +1,43 @@
|
||||
// @flow
|
||||
import { Trans } from '@lingui/macro';
|
||||
import * as React from 'react';
|
||||
import Text from '../../UI/Text';
|
||||
import { Column, Line, Spacer } from '../../UI/Grid';
|
||||
import AlertMessage from '../../UI/AlertMessage';
|
||||
|
||||
export const ExplanationHeader = () => (
|
||||
<Column noMargin>
|
||||
<Text>
|
||||
<Trans>
|
||||
This will export your game using Cocos2d-JS game engine. The game can be
|
||||
compiled for Android or iOS if you install Cocos2d-JS developer tools.
|
||||
</Trans>
|
||||
</Text>
|
||||
<Spacer />
|
||||
<AlertMessage kind="info">
|
||||
<Trans>
|
||||
This export is experimental and not all features are supported by
|
||||
Cocos2D-JS. It's recommended that you thoroughly test your game and be
|
||||
ready to contribute to the game engine if you need to implement missing
|
||||
features.
|
||||
</Trans>
|
||||
</AlertMessage>
|
||||
</Column>
|
||||
);
|
||||
|
||||
export const DoneFooter = ({
|
||||
renderGameButton,
|
||||
}: {|
|
||||
renderGameButton: () => React.Node,
|
||||
|}) => (
|
||||
<Column noMargin>
|
||||
<Text>
|
||||
<Trans>
|
||||
You can now upload the game to a web hosting or use Cocos2d-JS command
|
||||
line tools to export it to other platforms like iOS (XCode is required)
|
||||
or Android (Android SDK is required).
|
||||
</Trans>
|
||||
</Text>
|
||||
<Line justifyContent="center">{renderGameButton()}</Line>
|
||||
</Column>
|
||||
);
|
30
newIDE/app/src/Export/GenericExporters/CordovaExport.js
Normal file
@@ -0,0 +1,30 @@
|
||||
// @flow
|
||||
import { Trans } from '@lingui/macro';
|
||||
import * as React from 'react';
|
||||
import Text from '../../UI/Text';
|
||||
import { Column, Line } from '../../UI/Grid';
|
||||
|
||||
export const ExplanationHeader = () => (
|
||||
<Text>
|
||||
<Trans>
|
||||
This will export your game as a Cordova project. Cordova is a technology
|
||||
that enables HTML5 games to be packaged for iOS and Android.
|
||||
</Trans>
|
||||
</Text>
|
||||
);
|
||||
|
||||
export const DoneFooter = ({
|
||||
renderGameButton,
|
||||
}: {|
|
||||
renderGameButton: () => React.Node,
|
||||
|}) => (
|
||||
<Column noMargin>
|
||||
<Text>
|
||||
<Trans>
|
||||
You can now compile the game by yourself using Cordova command-line tool
|
||||
to iOS (XCode is required) or Android (Android SDK is required).
|
||||
</Trans>
|
||||
</Text>
|
||||
<Line justifyContent="center">{renderGameButton()}</Line>
|
||||
</Column>
|
||||
);
|
32
newIDE/app/src/Export/GenericExporters/ElectronExport.js
Normal file
@@ -0,0 +1,32 @@
|
||||
// @flow
|
||||
import { Trans } from '@lingui/macro';
|
||||
import * as React from 'react';
|
||||
import Text from '../../UI/Text';
|
||||
import { Column, Line } from '../../UI/Grid';
|
||||
|
||||
export const ExplanationHeader = () => (
|
||||
<Text>
|
||||
<Trans>
|
||||
This will export your game so that you can package it for Windows, macOS
|
||||
or Linux. You will need to install third-party tools (Node.js, Electron
|
||||
Builder) to package your game.
|
||||
</Trans>
|
||||
</Text>
|
||||
);
|
||||
|
||||
export const DoneFooter = ({
|
||||
renderGameButton,
|
||||
}: {|
|
||||
renderGameButton: () => React.Node,
|
||||
|}) => (
|
||||
<Column noMargin>
|
||||
<Text>
|
||||
<Trans>
|
||||
The game was properly exported. You can now use Electron Builder (you
|
||||
need Node.js installed and to use the command-line on your computer to
|
||||
run it) to create an executable.
|
||||
</Trans>
|
||||
</Text>
|
||||
<Line justifyContent="center">{renderGameButton()}</Line>
|
||||
</Column>
|
||||
);
|
@@ -0,0 +1,49 @@
|
||||
// @flow
|
||||
import { Trans } from '@lingui/macro';
|
||||
import * as React from 'react';
|
||||
import Text from '../../UI/Text';
|
||||
import { Column, Line } from '../../UI/Grid';
|
||||
import FlatButton from '../../UI/FlatButton';
|
||||
import { getHelpLink } from '../../Utils/HelpLink';
|
||||
import Window from '../../Utils/Window';
|
||||
|
||||
export const ExplanationHeader = () => (
|
||||
<Text>
|
||||
<Trans>
|
||||
Prepare your game for Facebook Instant Games so that it can be play on
|
||||
Facebook Messenger. GDevelop will create a compressed file that you can
|
||||
upload on your Facebook Developer account.
|
||||
</Trans>
|
||||
</Text>
|
||||
);
|
||||
|
||||
export const DoneFooter = ({
|
||||
renderGameButton,
|
||||
}: {|
|
||||
renderGameButton: () => React.Node,
|
||||
|}) => {
|
||||
const openLearnMore = () => {
|
||||
Window.openExternalURL(
|
||||
getHelpLink('/publishing/publishing-to-facebook-instant-games')
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<Column noMargin>
|
||||
<Text>
|
||||
<Trans>
|
||||
You can now create a game on Facebook Instant Games, if not already
|
||||
done, and upload the generated archive.
|
||||
</Trans>
|
||||
</Text>
|
||||
<Line justifyContent="center">
|
||||
{renderGameButton()}
|
||||
<FlatButton
|
||||
label={<Trans>Learn more about Instant Games publication</Trans>}
|
||||
primary
|
||||
onClick={openLearnMore}
|
||||
/>
|
||||
</Line>
|
||||
</Column>
|
||||
);
|
||||
};
|
83
newIDE/app/src/Export/GenericExporters/HTML5Export.js
Normal file
@@ -0,0 +1,83 @@
|
||||
// @flow
|
||||
import { Trans } from '@lingui/macro';
|
||||
import * as React from 'react';
|
||||
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 AlertMessage from '../../UI/AlertMessage';
|
||||
|
||||
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 Kongregate, Game Jolt, itch.io,
|
||||
Poki...
|
||||
</Trans>
|
||||
</Text>
|
||||
);
|
||||
|
||||
export const DoneFooter = ({
|
||||
renderGameButton,
|
||||
}: {|
|
||||
renderGameButton: () => React.Node,
|
||||
|}) => (
|
||||
<Column noMargin>
|
||||
<Text>
|
||||
<Trans>
|
||||
You can now upload the game to a web hosting to play to the game.
|
||||
</Trans>
|
||||
</Text>
|
||||
<AlertMessage kind="warning">
|
||||
<Trans>
|
||||
Your game won't work if you open index.html on your computer. You must
|
||||
upload it to a web hosting (Kongregate, Itch.io, etc...) or a web server
|
||||
to run it.
|
||||
</Trans>
|
||||
</AlertMessage>
|
||||
<Spacer />
|
||||
{renderGameButton()}
|
||||
<Spacer />
|
||||
<FlatButton
|
||||
fullWidth
|
||||
primary
|
||||
onClick={() =>
|
||||
Window.openExternalURL(
|
||||
getHelpLink('/publishing/publishing-to-gamejolt-store')
|
||||
)
|
||||
}
|
||||
label={<Trans>Publish your game on Game Jolt</Trans>}
|
||||
/>
|
||||
<FlatButton
|
||||
fullWidth
|
||||
primary
|
||||
onClick={() =>
|
||||
Window.openExternalURL(
|
||||
getHelpLink('/publishing/publishing-to-kongregate-store')
|
||||
)
|
||||
}
|
||||
label={<Trans>Publish your game on Kongregate</Trans>}
|
||||
/>
|
||||
<FlatButton
|
||||
fullWidth
|
||||
primary
|
||||
onClick={() =>
|
||||
Window.openExternalURL(getHelpLink('/publishing/publishing-to-itch-io'))
|
||||
}
|
||||
label={<Trans>Publish your game on Itch.io</Trans>}
|
||||
/>
|
||||
<FlatButton
|
||||
fullWidth
|
||||
primary
|
||||
onClick={() => Window.openExternalURL('https://gdevelop-app.com/poki')}
|
||||
label={<Trans>Publish your game on Poki.com</Trans>}
|
||||
/>
|
||||
<FlatButton
|
||||
fullWidth
|
||||
onClick={() => Window.openExternalURL(getHelpLink('/publishing'))}
|
||||
label={<Trans>Learn more about publishing</Trans>}
|
||||
/>
|
||||
</Column>
|
||||
);
|
@@ -0,0 +1,13 @@
|
||||
// @flow
|
||||
import { Trans } from '@lingui/macro';
|
||||
import * as React from 'react';
|
||||
import Text from '../../UI/Text';
|
||||
|
||||
export const ExplanationHeader = () => (
|
||||
<Text>
|
||||
<Trans>
|
||||
Packaging your game for Android will create an APK file that can be
|
||||
installed on Android phones or published to the Play Store.
|
||||
</Trans>
|
||||
</Text>
|
||||
);
|
@@ -0,0 +1,80 @@
|
||||
// @flow
|
||||
import { Trans } from '@lingui/macro';
|
||||
import * as React from 'react';
|
||||
import Text from '../../UI/Text';
|
||||
import Checkbox from '../../UI/Checkbox';
|
||||
import { Column, Line } from '../../UI/Grid';
|
||||
import { type TargetName } from '../../Utils/GDevelopServices/Build';
|
||||
|
||||
export type ExportState = {|
|
||||
targets: Array<TargetName>,
|
||||
|};
|
||||
|
||||
type HeaderProps = {|
|
||||
project: gdProject,
|
||||
exportState: ExportState,
|
||||
updateExportState: (
|
||||
updater: (prevExportState: ExportState) => ExportState
|
||||
) => void,
|
||||
|};
|
||||
|
||||
export const SetupExportHeader = ({
|
||||
exportState,
|
||||
updateExportState,
|
||||
}: HeaderProps) => {
|
||||
const setTarget = (targetName: TargetName, enable: boolean) => {
|
||||
updateExportState(prevExportState => {
|
||||
if (enable && prevExportState.targets.indexOf(targetName) === -1) {
|
||||
return {
|
||||
...prevExportState,
|
||||
targets: [...prevExportState.targets, targetName],
|
||||
};
|
||||
} else if (
|
||||
!enable &&
|
||||
prevExportState.targets.indexOf(targetName) !== -1
|
||||
) {
|
||||
return {
|
||||
...prevExportState,
|
||||
targets: prevExportState.targets.filter(name => name !== targetName),
|
||||
};
|
||||
}
|
||||
|
||||
return prevExportState;
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<React.Fragment>
|
||||
<Column noMargin>
|
||||
<Line>
|
||||
<Text>
|
||||
<Trans>
|
||||
Your game will be exported and packaged online as a stand-alone
|
||||
game for Windows, Linux and/or macOS.
|
||||
</Trans>
|
||||
</Text>
|
||||
</Line>
|
||||
<Checkbox
|
||||
label={<Trans>Windows (zip file)</Trans>}
|
||||
checked={exportState.targets.indexOf('winZip') !== -1}
|
||||
onCheck={(e, checked) => setTarget('winZip', checked)}
|
||||
/>
|
||||
<Checkbox
|
||||
label={<Trans>Windows (auto-installer file)</Trans>}
|
||||
checked={exportState.targets.indexOf('winExe') !== -1}
|
||||
onCheck={(e, checked) => setTarget('winExe', checked)}
|
||||
/>
|
||||
<Checkbox
|
||||
label={<Trans>macOS (zip file)</Trans>}
|
||||
checked={exportState.targets.indexOf('macZip') !== -1}
|
||||
onCheck={(e, checked) => setTarget('macZip', checked)}
|
||||
/>
|
||||
<Checkbox
|
||||
label={<Trans>Linux (AppImage)</Trans>}
|
||||
checked={exportState.targets.indexOf('linuxAppImage') !== -1}
|
||||
onCheck={(e, checked) => setTarget('linuxAppImage', checked)}
|
||||
/>
|
||||
</Column>
|
||||
</React.Fragment>
|
||||
);
|
||||
};
|
14
newIDE/app/src/Export/GenericExporters/OnlineWebExport.js
Normal file
@@ -0,0 +1,14 @@
|
||||
// @flow
|
||||
import { Trans } from '@lingui/macro';
|
||||
import * as React from 'react';
|
||||
import Text from '../../UI/Text';
|
||||
|
||||
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>
|
||||
);
|
@@ -1,168 +1,160 @@
|
||||
// @flow
|
||||
import { Trans } from '@lingui/macro';
|
||||
import React, { Component } from 'react';
|
||||
import Dialog from '../../UI/Dialog';
|
||||
import FlatButton from '../../UI/FlatButton';
|
||||
|
||||
import React from 'react';
|
||||
import RaisedButton from '../../UI/RaisedButton';
|
||||
import Toggle from '../../UI/Toggle';
|
||||
import { sendExportLaunched } from '../../Utils/Analytics/EventSender';
|
||||
import { Column, Line, Spacer } from '../../UI/Grid';
|
||||
import HelpButton from '../../UI/HelpButton';
|
||||
import { showErrorBox } from '../../UI/Messages/MessageBox';
|
||||
import { Column, Line } from '../../UI/Grid';
|
||||
import { findGDJS } from './LocalGDJSFinder';
|
||||
import localFileSystem from './LocalFileSystem';
|
||||
import LocalFolderPicker from '../../UI/LocalFolderPicker';
|
||||
import assignIn from 'lodash/assignIn';
|
||||
import optionalRequire from '../../Utils/OptionalRequire';
|
||||
import Text from '../../UI/Text';
|
||||
import {
|
||||
type ExportPipeline,
|
||||
type ExportPipelineContext,
|
||||
} from '../ExportPipeline.flow';
|
||||
import Toggle from '../../UI/Toggle';
|
||||
import {
|
||||
DoneFooter,
|
||||
ExplanationHeader,
|
||||
} from '../GenericExporters/Cocos2dExport';
|
||||
const electron = optionalRequire('electron');
|
||||
const shell = electron ? electron.shell : null;
|
||||
|
||||
const gd = global.gd;
|
||||
|
||||
export default class LocalCocos2dExport extends Component {
|
||||
state = {
|
||||
exportFinishedDialogOpen: false,
|
||||
outputDir: '',
|
||||
type ExportState = {
|
||||
outputDir: string,
|
||||
debugMode: boolean,
|
||||
};
|
||||
|
||||
type PreparedExporter = {|
|
||||
exporter: gdjsExporter,
|
||||
|};
|
||||
|
||||
type ExportOutput = null;
|
||||
|
||||
type ResourcesDownloadOutput = null;
|
||||
|
||||
type CompressionOutput = null;
|
||||
|
||||
export const localCocos2dExportPipeline: ExportPipeline<
|
||||
ExportState,
|
||||
PreparedExporter,
|
||||
ExportOutput,
|
||||
ResourcesDownloadOutput,
|
||||
CompressionOutput
|
||||
> = {
|
||||
name: 'local-cocos2d',
|
||||
|
||||
getInitialExportState: (project: gdProject) => ({
|
||||
outputDir: project.getLastCompilationDirectory(),
|
||||
debugMode: false,
|
||||
};
|
||||
}),
|
||||
|
||||
componentDidMount() {
|
||||
const { project } = this.props;
|
||||
this.setState({
|
||||
outputDir: project ? project.getLastCompilationDirectory() : '',
|
||||
canLaunchBuild: exportState => !!exportState.outputDir,
|
||||
|
||||
renderHeader: ({ project, exportState, updateExportState }) => (
|
||||
<Column noMargin>
|
||||
<Line>
|
||||
<ExplanationHeader />
|
||||
</Line>
|
||||
<Line>
|
||||
<LocalFolderPicker
|
||||
type="export"
|
||||
value={exportState.outputDir}
|
||||
defaultPath={project.getLastCompilationDirectory()}
|
||||
onChange={outputDir => {
|
||||
updateExportState(prevState => ({ ...prevState, outputDir }));
|
||||
}}
|
||||
fullWidth
|
||||
/>
|
||||
</Line>
|
||||
<Line>
|
||||
<Toggle
|
||||
onToggle={(e, check) =>
|
||||
updateExportState(prevState => ({
|
||||
...prevState,
|
||||
debugMode: check,
|
||||
}))
|
||||
}
|
||||
toggled={exportState.debugMode}
|
||||
labelPosition="right"
|
||||
label={
|
||||
<Trans>
|
||||
Debug mode (show FPS counter and stats in the bottom left)
|
||||
</Trans>
|
||||
}
|
||||
/>
|
||||
</Line>
|
||||
</Column>
|
||||
),
|
||||
|
||||
renderLaunchButtonLabel: () => <Trans>Package</Trans>,
|
||||
|
||||
prepareExporter: (
|
||||
context: ExportPipelineContext<ExportState>
|
||||
): Promise<PreparedExporter> => {
|
||||
return findGDJS().then(({ gdjsRoot }) => {
|
||||
console.info('GDJS found in ', gdjsRoot);
|
||||
|
||||
// TODO: Memory leak? Check for other exporters too.
|
||||
const fileSystem = assignIn(
|
||||
new gd.AbstractFileSystemJS(),
|
||||
localFileSystem
|
||||
);
|
||||
const exporter = new gd.Exporter(fileSystem, gdjsRoot);
|
||||
|
||||
return {
|
||||
exporter,
|
||||
};
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
static prepareExporter = (): Promise<any> => {
|
||||
return new Promise((resolve, reject) => {
|
||||
findGDJS(gdjsRoot => {
|
||||
if (!gdjsRoot) {
|
||||
showErrorBox('Could not find GDJS');
|
||||
return reject();
|
||||
}
|
||||
console.info('GDJS found in ', gdjsRoot);
|
||||
launchExport: (
|
||||
context: ExportPipelineContext<ExportState>,
|
||||
{ exporter }: PreparedExporter
|
||||
): Promise<ExportOutput> => {
|
||||
exporter.exportWholeCocos2dProject(
|
||||
context.project,
|
||||
context.exportState.debugMode,
|
||||
context.exportState.outputDir
|
||||
);
|
||||
exporter.delete();
|
||||
|
||||
const fileSystem = assignIn(
|
||||
new gd.AbstractFileSystemJS(),
|
||||
localFileSystem
|
||||
);
|
||||
const exporter = new gd.Exporter(fileSystem, gdjsRoot);
|
||||
return Promise.resolve(null);
|
||||
},
|
||||
|
||||
resolve({
|
||||
exporter,
|
||||
});
|
||||
});
|
||||
});
|
||||
};
|
||||
launchResourcesDownload: (
|
||||
context: ExportPipelineContext<ExportState>,
|
||||
exportOutput: ExportOutput
|
||||
): Promise<ResourcesDownloadOutput> => {
|
||||
return Promise.resolve(null);
|
||||
},
|
||||
|
||||
launchExport = () => {
|
||||
const { project } = this.props;
|
||||
if (!project) return;
|
||||
launchCompression: (
|
||||
context: ExportPipelineContext<ExportState>,
|
||||
exportOutput: ResourcesDownloadOutput
|
||||
): Promise<CompressionOutput> => {
|
||||
return Promise.resolve(null);
|
||||
},
|
||||
|
||||
sendExportLaunched('local-cocos2d');
|
||||
|
||||
const { outputDir, debugMode } = this.state;
|
||||
project.setLastCompilationDirectory(outputDir);
|
||||
|
||||
LocalCocos2dExport.prepareExporter()
|
||||
.then(({ exporter }) => {
|
||||
exporter.exportWholeCocos2dProject(project, debugMode, outputDir);
|
||||
exporter.delete();
|
||||
this.setState({
|
||||
exportFinishedDialogOpen: true,
|
||||
});
|
||||
})
|
||||
.catch(err => {
|
||||
showErrorBox('Unable to export the game with Cocos2d-JS', err);
|
||||
});
|
||||
};
|
||||
|
||||
openExportFolder = () => {
|
||||
shell.openItem(this.state.outputDir);
|
||||
};
|
||||
|
||||
render() {
|
||||
const { project } = this.props;
|
||||
if (!project) return null;
|
||||
renderDoneFooter: ({ exportState, onClose }) => {
|
||||
const openExportFolder = () => {
|
||||
if (shell) shell.openItem(exportState.outputDir);
|
||||
};
|
||||
|
||||
return (
|
||||
<Column noMargin>
|
||||
<Line>
|
||||
<Text>
|
||||
<Trans>
|
||||
This will export your game using Cocos2d-JS game engine. The game
|
||||
can be compiled for Android or iOS if you install Cocos2d-JS
|
||||
developer tools.
|
||||
</Trans>
|
||||
</Text>
|
||||
</Line>
|
||||
<Line>
|
||||
<LocalFolderPicker
|
||||
type="export"
|
||||
value={this.state.outputDir}
|
||||
defaultPath={project.getLastCompilationDirectory()}
|
||||
onChange={value => this.setState({ outputDir: value })}
|
||||
fullWidth
|
||||
/>
|
||||
</Line>
|
||||
<Line>
|
||||
<Toggle
|
||||
onToggle={(e, check) =>
|
||||
this.setState({
|
||||
debugMode: check,
|
||||
})
|
||||
}
|
||||
toggled={this.state.debugMode}
|
||||
labelPosition="right"
|
||||
label={
|
||||
<Trans>
|
||||
Debug mode (show FPS counter and stats in the bottom left)
|
||||
</Trans>
|
||||
}
|
||||
/>
|
||||
</Line>
|
||||
<Line>
|
||||
<Spacer expand />
|
||||
<DoneFooter
|
||||
renderGameButton={() => (
|
||||
<RaisedButton
|
||||
label={<Trans>Export</Trans>}
|
||||
key="open"
|
||||
label={<Trans>Open folder</Trans>}
|
||||
primary={true}
|
||||
onClick={this.launchExport}
|
||||
disabled={!this.state.outputDir}
|
||||
onClick={openExportFolder}
|
||||
/>
|
||||
</Line>
|
||||
<Dialog
|
||||
title={<Trans>Export finished</Trans>}
|
||||
actions={[
|
||||
<FlatButton
|
||||
key="open"
|
||||
label={<Trans>Open folder</Trans>}
|
||||
primary={true}
|
||||
onClick={this.openExportFolder}
|
||||
/>,
|
||||
<FlatButton
|
||||
key="close"
|
||||
label={<Trans>Close</Trans>}
|
||||
primary={false}
|
||||
onClick={() =>
|
||||
this.setState({
|
||||
exportFinishedDialogOpen: false,
|
||||
})
|
||||
}
|
||||
/>,
|
||||
]}
|
||||
secondaryActions={
|
||||
<HelpButton key="help" helpPagePath="/publishing" />
|
||||
}
|
||||
modal
|
||||
open={this.state.exportFinishedDialogOpen}
|
||||
>
|
||||
<Text>
|
||||
You can now upload the game to a web hosting or use Cocos2d-JS
|
||||
command line tools to export it to other platforms like iOS (XCode
|
||||
is required) or Android (Android SDK is required).
|
||||
</Text>
|
||||
</Dialog>
|
||||
</Column>
|
||||
)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
},
|
||||
};
|
||||
|
@@ -1,198 +1,145 @@
|
||||
// @flow
|
||||
import { Trans } from '@lingui/macro';
|
||||
|
||||
import React, { Component } from 'react';
|
||||
import Dialog from '../../UI/Dialog';
|
||||
import FlatButton from '../../UI/FlatButton';
|
||||
import React from 'react';
|
||||
import RaisedButton from '../../UI/RaisedButton';
|
||||
import { sendExportLaunched } from '../../Utils/Analytics/EventSender';
|
||||
import { Column, Line, Spacer } from '../../UI/Grid';
|
||||
import { showErrorBox } from '../../UI/Messages/MessageBox';
|
||||
import { Column, Line } from '../../UI/Grid';
|
||||
import { findGDJS } from './LocalGDJSFinder';
|
||||
import localFileSystem from './LocalFileSystem';
|
||||
import LocalFolderPicker from '../../UI/LocalFolderPicker';
|
||||
import HelpButton from '../../UI/HelpButton';
|
||||
import {
|
||||
displayProjectErrorsBox,
|
||||
getErrors,
|
||||
} from '../../ProjectManager/ProjectErrorsChecker';
|
||||
import assignIn from 'lodash/assignIn';
|
||||
import optionalRequire from '../../Utils/OptionalRequire';
|
||||
import Window from '../../Utils/Window';
|
||||
import Text from '../../UI/Text';
|
||||
import {
|
||||
type ExportPipeline,
|
||||
type ExportPipelineContext,
|
||||
} from '../ExportPipeline.flow';
|
||||
import {
|
||||
ExplanationHeader,
|
||||
DoneFooter,
|
||||
} from '../GenericExporters/CordovaExport';
|
||||
const electron = optionalRequire('electron');
|
||||
const shell = electron ? electron.shell : null;
|
||||
|
||||
const gd = global.gd;
|
||||
|
||||
type Props = {|
|
||||
project: gdProject,
|
||||
|};
|
||||
|
||||
type State = {|
|
||||
type ExportState = {
|
||||
outputDir: string,
|
||||
exportFinishedDialogOpen: boolean,
|
||||
};
|
||||
|
||||
type PreparedExporter = {|
|
||||
exporter: gdjsExporter,
|
||||
|};
|
||||
|
||||
class LocalCordovaExport extends Component<Props, State> {
|
||||
state = {
|
||||
exportFinishedDialogOpen: false,
|
||||
outputDir: '',
|
||||
};
|
||||
type ExportOutput = null;
|
||||
|
||||
componentDidMount() {
|
||||
const { project } = this.props;
|
||||
this.setState({
|
||||
outputDir: project ? project.getLastCompilationDirectory() : '',
|
||||
type ResourcesDownloadOutput = null;
|
||||
|
||||
type CompressionOutput = null;
|
||||
|
||||
export const localCordovaExportPipeline: ExportPipeline<
|
||||
ExportState,
|
||||
PreparedExporter,
|
||||
ExportOutput,
|
||||
ResourcesDownloadOutput,
|
||||
CompressionOutput
|
||||
> = {
|
||||
name: 'local-cordova',
|
||||
|
||||
getInitialExportState: (project: gdProject) => ({
|
||||
outputDir: project.getLastCompilationDirectory(),
|
||||
}),
|
||||
|
||||
canLaunchBuild: exportState => !!exportState.outputDir,
|
||||
|
||||
renderHeader: ({ project, exportState, updateExportState }) => (
|
||||
<Column noMargin>
|
||||
<Line>
|
||||
<Column noMargin>
|
||||
<ExplanationHeader />
|
||||
</Column>
|
||||
</Line>
|
||||
<Line>
|
||||
<LocalFolderPicker
|
||||
type="export"
|
||||
value={exportState.outputDir}
|
||||
defaultPath={project.getLastCompilationDirectory()}
|
||||
onChange={outputDir => {
|
||||
updateExportState(() => ({ outputDir }));
|
||||
}}
|
||||
fullWidth
|
||||
/>
|
||||
</Line>
|
||||
</Column>
|
||||
),
|
||||
|
||||
renderLaunchButtonLabel: () => <Trans>Package</Trans>,
|
||||
|
||||
prepareExporter: (
|
||||
context: ExportPipelineContext<ExportState>
|
||||
): Promise<PreparedExporter> => {
|
||||
return findGDJS().then(({ gdjsRoot }) => {
|
||||
console.info('GDJS found in ', gdjsRoot);
|
||||
|
||||
// TODO: Memory leak? Check for other exporters too.
|
||||
const fileSystem = assignIn(
|
||||
new gd.AbstractFileSystemJS(),
|
||||
localFileSystem
|
||||
);
|
||||
const exporter = new gd.Exporter(fileSystem, gdjsRoot);
|
||||
|
||||
return {
|
||||
exporter,
|
||||
};
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
static prepareExporter = (): Promise<any> => {
|
||||
return new Promise((resolve, reject) => {
|
||||
findGDJS(gdjsRoot => {
|
||||
if (!gdjsRoot) {
|
||||
showErrorBox('Could not find GDJS');
|
||||
return reject();
|
||||
}
|
||||
console.info('GDJS found in ', gdjsRoot);
|
||||
launchExport: (
|
||||
context: ExportPipelineContext<ExportState>,
|
||||
{ exporter }: PreparedExporter
|
||||
): Promise<ExportOutput> => {
|
||||
const exportOptions = new gd.MapStringBoolean();
|
||||
exportOptions.set('exportForCordova', true);
|
||||
exporter.exportWholePixiProject(
|
||||
context.project,
|
||||
context.exportState.outputDir,
|
||||
exportOptions
|
||||
);
|
||||
exportOptions.delete();
|
||||
exporter.delete();
|
||||
|
||||
const fileSystem = assignIn(
|
||||
new gd.AbstractFileSystemJS(),
|
||||
localFileSystem
|
||||
);
|
||||
const exporter = new gd.Exporter(fileSystem, gdjsRoot);
|
||||
return Promise.resolve(null);
|
||||
},
|
||||
|
||||
resolve({
|
||||
exporter,
|
||||
});
|
||||
});
|
||||
});
|
||||
};
|
||||
launchResourcesDownload: (
|
||||
context: ExportPipelineContext<ExportState>,
|
||||
exportOutput: ExportOutput
|
||||
): Promise<ResourcesDownloadOutput> => {
|
||||
return Promise.resolve(null);
|
||||
},
|
||||
|
||||
launchExport = () => {
|
||||
const t = str => str; //TODO;
|
||||
const { project } = this.props;
|
||||
if (!project) return;
|
||||
launchCompression: (
|
||||
context: ExportPipelineContext<ExportState>,
|
||||
exportOutput: ResourcesDownloadOutput
|
||||
): Promise<CompressionOutput> => {
|
||||
return Promise.resolve(null);
|
||||
},
|
||||
|
||||
sendExportLaunched('local-cordova');
|
||||
|
||||
if (!displayProjectErrorsBox(t, getErrors(t, project))) return;
|
||||
|
||||
const outputDir = this.state.outputDir;
|
||||
project.setLastCompilationDirectory(outputDir);
|
||||
|
||||
LocalCordovaExport.prepareExporter()
|
||||
.then(({ exporter }) => {
|
||||
const exportOptions = new gd.MapStringBoolean();
|
||||
exportOptions.set('exportForCordova', true);
|
||||
exporter.exportWholePixiProject(project, outputDir, exportOptions);
|
||||
exportOptions.delete();
|
||||
exporter.delete();
|
||||
|
||||
this.setState({
|
||||
exportFinishedDialogOpen: true,
|
||||
});
|
||||
})
|
||||
.catch(err => {
|
||||
showErrorBox('Unable to export the game', err);
|
||||
});
|
||||
};
|
||||
|
||||
openExportFolder = () => {
|
||||
if (shell) shell.openItem(this.state.outputDir);
|
||||
};
|
||||
|
||||
openPhoneGapBuild = () => {
|
||||
Window.openExternalURL('https://build.phonegap.com');
|
||||
};
|
||||
|
||||
render() {
|
||||
const t = str => str; //TODO;
|
||||
const { project } = this.props;
|
||||
if (!project) return null;
|
||||
renderDoneFooter: ({ exportState, onClose }) => {
|
||||
const openExportFolder = () => {
|
||||
if (shell) shell.openItem(exportState.outputDir);
|
||||
};
|
||||
|
||||
return (
|
||||
<Column noMargin>
|
||||
<Line>
|
||||
<Column noMargin>
|
||||
<Text>
|
||||
This will export your game as a Cordova project. Cordova is a
|
||||
technology that enables HTML5 games to be packaged for <b>iOS</b>,{' '}
|
||||
<b>Android</b> and more.
|
||||
</Text>
|
||||
<Text>
|
||||
Third-party tools like <b>Adobe PhoneGap Build</b> allow game
|
||||
developers to bundle their games using Cordova.
|
||||
</Text>
|
||||
</Column>
|
||||
</Line>
|
||||
<Line>
|
||||
<LocalFolderPicker
|
||||
type="export"
|
||||
value={this.state.outputDir}
|
||||
defaultPath={project.getLastCompilationDirectory()}
|
||||
onChange={value => this.setState({ outputDir: value })}
|
||||
fullWidth
|
||||
/>
|
||||
</Line>
|
||||
<Line>
|
||||
<Spacer expand />
|
||||
<DoneFooter
|
||||
renderGameButton={() => (
|
||||
<RaisedButton
|
||||
label={<Trans>Export</Trans>}
|
||||
key="open"
|
||||
label={<Trans>Open folder</Trans>}
|
||||
primary={true}
|
||||
onClick={this.launchExport}
|
||||
disabled={!this.state.outputDir}
|
||||
onClick={openExportFolder}
|
||||
/>
|
||||
</Line>
|
||||
<Dialog
|
||||
title={t('Export finished')}
|
||||
actions={[
|
||||
<FlatButton
|
||||
key="open"
|
||||
label={<Trans>Open folder</Trans>}
|
||||
primary={true}
|
||||
onClick={this.openExportFolder}
|
||||
/>,
|
||||
<FlatButton
|
||||
key="close"
|
||||
label={<Trans>Close</Trans>}
|
||||
primary={false}
|
||||
onClick={() =>
|
||||
this.setState({
|
||||
exportFinishedDialogOpen: false,
|
||||
})
|
||||
}
|
||||
/>,
|
||||
]}
|
||||
secondaryActions={
|
||||
<HelpButton key="help" helpPagePath="/publishing" />
|
||||
}
|
||||
modal
|
||||
open={this.state.exportFinishedDialogOpen}
|
||||
>
|
||||
<Text>
|
||||
<Trans>
|
||||
You can now compress and upload the game to PhoneGap Build which
|
||||
will compile it for you to an iOS and Android app.
|
||||
</Trans>
|
||||
</Text>
|
||||
<Text>
|
||||
<Trans>
|
||||
You can also compile the game by yourself using Cordova
|
||||
command-line tool to iOS (XCode is required) or Android (Android
|
||||
SDK is required).
|
||||
</Trans>
|
||||
</Text>
|
||||
<RaisedButton
|
||||
fullWidth
|
||||
primary
|
||||
onClick={() => this.openPhoneGapBuild()}
|
||||
label={t('Open PhoneGap Build')}
|
||||
/>
|
||||
</Dialog>
|
||||
</Column>
|
||||
)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default LocalCordovaExport;
|
||||
},
|
||||
};
|
||||
|
@@ -1,180 +1,145 @@
|
||||
// @flow
|
||||
import { Trans } from '@lingui/macro';
|
||||
|
||||
import React, { Component } from 'react';
|
||||
import Dialog from '../../UI/Dialog';
|
||||
import FlatButton from '../../UI/FlatButton';
|
||||
import React from 'react';
|
||||
import RaisedButton from '../../UI/RaisedButton';
|
||||
import { sendExportLaunched } from '../../Utils/Analytics/EventSender';
|
||||
import { Column, Line, Spacer } from '../../UI/Grid';
|
||||
import { showErrorBox } from '../../UI/Messages/MessageBox';
|
||||
import { Column, Line } from '../../UI/Grid';
|
||||
import { findGDJS } from './LocalGDJSFinder';
|
||||
import localFileSystem from './LocalFileSystem';
|
||||
import LocalFolderPicker from '../../UI/LocalFolderPicker';
|
||||
import HelpButton from '../../UI/HelpButton';
|
||||
import {
|
||||
displayProjectErrorsBox,
|
||||
getErrors,
|
||||
} from '../../ProjectManager/ProjectErrorsChecker';
|
||||
import assignIn from 'lodash/assignIn';
|
||||
import optionalRequire from '../../Utils/OptionalRequire';
|
||||
import Text from '../../UI/Text';
|
||||
import {
|
||||
type ExportPipeline,
|
||||
type ExportPipelineContext,
|
||||
} from '../ExportPipeline.flow';
|
||||
import {
|
||||
ExplanationHeader,
|
||||
DoneFooter,
|
||||
} from '../GenericExporters/ElectronExport';
|
||||
const electron = optionalRequire('electron');
|
||||
const shell = electron ? electron.shell : null;
|
||||
|
||||
const gd = global.gd;
|
||||
|
||||
type Props = {|
|
||||
project: gdProject,
|
||||
|};
|
||||
|
||||
type State = {|
|
||||
type ExportState = {
|
||||
outputDir: string,
|
||||
exportFinishedDialogOpen: boolean,
|
||||
};
|
||||
|
||||
type PreparedExporter = {|
|
||||
exporter: gdjsExporter,
|
||||
|};
|
||||
|
||||
class LocalElectronExport extends Component<Props, State> {
|
||||
state = {
|
||||
exportFinishedDialogOpen: false,
|
||||
outputDir: '',
|
||||
};
|
||||
type ExportOutput = null;
|
||||
|
||||
componentDidMount() {
|
||||
const { project } = this.props;
|
||||
this.setState({
|
||||
outputDir: project ? project.getLastCompilationDirectory() : '',
|
||||
type ResourcesDownloadOutput = null;
|
||||
|
||||
type CompressionOutput = null;
|
||||
|
||||
export const localElectronExportPipeline: ExportPipeline<
|
||||
ExportState,
|
||||
PreparedExporter,
|
||||
ExportOutput,
|
||||
ResourcesDownloadOutput,
|
||||
CompressionOutput
|
||||
> = {
|
||||
name: 'local-electron',
|
||||
|
||||
getInitialExportState: (project: gdProject) => ({
|
||||
outputDir: project.getLastCompilationDirectory(),
|
||||
}),
|
||||
|
||||
canLaunchBuild: exportState => !!exportState.outputDir,
|
||||
|
||||
renderHeader: ({ project, exportState, updateExportState }) => (
|
||||
<Column noMargin>
|
||||
<Line>
|
||||
<Column noMargin>
|
||||
<ExplanationHeader />
|
||||
</Column>
|
||||
</Line>
|
||||
<Line>
|
||||
<LocalFolderPicker
|
||||
type="export"
|
||||
value={exportState.outputDir}
|
||||
defaultPath={project.getLastCompilationDirectory()}
|
||||
onChange={outputDir => {
|
||||
updateExportState(() => ({ outputDir }));
|
||||
}}
|
||||
fullWidth
|
||||
/>
|
||||
</Line>
|
||||
</Column>
|
||||
),
|
||||
|
||||
renderLaunchButtonLabel: () => <Trans>Package</Trans>,
|
||||
|
||||
prepareExporter: (
|
||||
context: ExportPipelineContext<ExportState>
|
||||
): Promise<PreparedExporter> => {
|
||||
return findGDJS().then(({ gdjsRoot }) => {
|
||||
console.info('GDJS found in ', gdjsRoot);
|
||||
|
||||
// TODO: Memory leak? Check for other exporters too.
|
||||
const fileSystem = assignIn(
|
||||
new gd.AbstractFileSystemJS(),
|
||||
localFileSystem
|
||||
);
|
||||
const exporter = new gd.Exporter(fileSystem, gdjsRoot);
|
||||
|
||||
return {
|
||||
exporter,
|
||||
};
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
static prepareExporter = (): Promise<any> => {
|
||||
return new Promise((resolve, reject) => {
|
||||
findGDJS(gdjsRoot => {
|
||||
if (!gdjsRoot) {
|
||||
showErrorBox('Could not find GDJS');
|
||||
return reject();
|
||||
}
|
||||
console.info('GDJS found in ', gdjsRoot);
|
||||
launchExport: (
|
||||
context: ExportPipelineContext<ExportState>,
|
||||
{ exporter }: PreparedExporter
|
||||
): Promise<ExportOutput> => {
|
||||
const exportOptions = new gd.MapStringBoolean();
|
||||
exportOptions.set('exportForElectron', true);
|
||||
exporter.exportWholePixiProject(
|
||||
context.project,
|
||||
context.exportState.outputDir,
|
||||
exportOptions
|
||||
);
|
||||
exportOptions.delete();
|
||||
exporter.delete();
|
||||
|
||||
const fileSystem = assignIn(
|
||||
new gd.AbstractFileSystemJS(),
|
||||
localFileSystem
|
||||
);
|
||||
const exporter = new gd.Exporter(fileSystem, gdjsRoot);
|
||||
return Promise.resolve(null);
|
||||
},
|
||||
|
||||
resolve({
|
||||
exporter,
|
||||
});
|
||||
});
|
||||
});
|
||||
};
|
||||
launchResourcesDownload: (
|
||||
context: ExportPipelineContext<ExportState>,
|
||||
exportOutput: ExportOutput
|
||||
): Promise<ResourcesDownloadOutput> => {
|
||||
return Promise.resolve(null);
|
||||
},
|
||||
|
||||
launchExport = () => {
|
||||
const t = str => str; //TODO;
|
||||
const { project } = this.props;
|
||||
if (!project) return;
|
||||
launchCompression: (
|
||||
context: ExportPipelineContext<ExportState>,
|
||||
exportOutput: ResourcesDownloadOutput
|
||||
): Promise<CompressionOutput> => {
|
||||
return Promise.resolve(null);
|
||||
},
|
||||
|
||||
sendExportLaunched('local-electron');
|
||||
|
||||
if (!displayProjectErrorsBox(t, getErrors(t, project))) return;
|
||||
|
||||
const outputDir = this.state.outputDir;
|
||||
project.setLastCompilationDirectory(outputDir);
|
||||
|
||||
LocalElectronExport.prepareExporter()
|
||||
.then(({ exporter }) => {
|
||||
const exportOptions = new gd.MapStringBoolean();
|
||||
exportOptions.set('exportForElectron', true);
|
||||
exporter.exportWholePixiProject(project, outputDir, exportOptions);
|
||||
exportOptions.delete();
|
||||
exporter.delete();
|
||||
|
||||
this.setState({
|
||||
exportFinishedDialogOpen: true,
|
||||
});
|
||||
})
|
||||
.catch(err => {
|
||||
showErrorBox('Unable to export the game', err);
|
||||
});
|
||||
};
|
||||
|
||||
openExportFolder = () => {
|
||||
if (shell) shell.openItem(this.state.outputDir);
|
||||
};
|
||||
|
||||
render() {
|
||||
const t = str => str; //TODO;
|
||||
const { project } = this.props;
|
||||
if (!project) return null;
|
||||
renderDoneFooter: ({ exportState, onClose }) => {
|
||||
const openExportFolder = () => {
|
||||
if (shell) shell.openItem(exportState.outputDir);
|
||||
};
|
||||
|
||||
return (
|
||||
<Column noMargin>
|
||||
<Line>
|
||||
<Column noMargin>
|
||||
<Text>
|
||||
<Trans>
|
||||
This will export your game so that you can package it for
|
||||
Windows, macOS or Linux. You will need to install third-party
|
||||
tools (Node.js, Electron Builder) to package your game by
|
||||
yourself.
|
||||
</Trans>
|
||||
</Text>
|
||||
</Column>
|
||||
</Line>
|
||||
<Line>
|
||||
<LocalFolderPicker
|
||||
type="export"
|
||||
value={this.state.outputDir}
|
||||
defaultPath={project.getLastCompilationDirectory()}
|
||||
onChange={value => this.setState({ outputDir: value })}
|
||||
fullWidth
|
||||
/>
|
||||
</Line>
|
||||
<Line>
|
||||
<Spacer expand />
|
||||
<DoneFooter
|
||||
renderGameButton={() => (
|
||||
<RaisedButton
|
||||
label={<Trans>Export</Trans>}
|
||||
key="open"
|
||||
label={<Trans>Open folder</Trans>}
|
||||
primary={true}
|
||||
onClick={this.launchExport}
|
||||
disabled={!this.state.outputDir}
|
||||
onClick={openExportFolder}
|
||||
/>
|
||||
</Line>
|
||||
<Dialog
|
||||
title={t('Export finished')}
|
||||
actions={[
|
||||
<FlatButton
|
||||
key="open"
|
||||
label={<Trans>Open folder</Trans>}
|
||||
primary={true}
|
||||
onClick={this.openExportFolder}
|
||||
/>,
|
||||
<FlatButton
|
||||
key="close"
|
||||
label={<Trans>Close</Trans>}
|
||||
primary={false}
|
||||
onClick={() =>
|
||||
this.setState({
|
||||
exportFinishedDialogOpen: false,
|
||||
})
|
||||
}
|
||||
/>,
|
||||
]}
|
||||
secondaryActions={
|
||||
<HelpButton key="help" helpPagePath="/publishing" />
|
||||
}
|
||||
modal
|
||||
open={this.state.exportFinishedDialogOpen}
|
||||
>
|
||||
<Text>
|
||||
<Trans>
|
||||
The game was properly exported. You can now use Electron Builder
|
||||
(you need Node.js installed and to use the command-line to run it)
|
||||
to create an executable.
|
||||
</Trans>
|
||||
</Text>
|
||||
</Dialog>
|
||||
</Column>
|
||||
)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default LocalElectronExport;
|
||||
},
|
||||
};
|
||||
|
@@ -1,194 +0,0 @@
|
||||
import { Trans } from '@lingui/macro';
|
||||
import React, { Component } from 'react';
|
||||
import Dialog from '../../UI/Dialog';
|
||||
import FlatButton from '../../UI/FlatButton';
|
||||
import RaisedButton from '../../UI/RaisedButton';
|
||||
import { sendExportLaunched } from '../../Utils/Analytics/EventSender';
|
||||
import { Column, Line, Spacer } from '../../UI/Grid';
|
||||
import { showErrorBox } from '../../UI/Messages/MessageBox';
|
||||
import { findGDJS } from './LocalGDJSFinder';
|
||||
import localFileSystem from './LocalFileSystem';
|
||||
import LocalFolderPicker from '../../UI/LocalFolderPicker';
|
||||
import assignIn from 'lodash/assignIn';
|
||||
import optionalRequire from '../../Utils/OptionalRequire';
|
||||
import Window from '../../Utils/Window';
|
||||
import { getHelpLink } from '../../Utils/HelpLink';
|
||||
import AlertMessage from '../../UI/AlertMessage';
|
||||
import Text from '../../UI/Text';
|
||||
const electron = optionalRequire('electron');
|
||||
const shell = electron ? electron.shell : null;
|
||||
|
||||
const gd = global.gd;
|
||||
|
||||
export default class LocalExport extends Component {
|
||||
state = {
|
||||
exportFinishedDialogOpen: false,
|
||||
outputDir: '',
|
||||
};
|
||||
|
||||
componentDidMount() {
|
||||
const { project } = this.props;
|
||||
this.setState({
|
||||
outputDir: project ? project.getLastCompilationDirectory() : '',
|
||||
});
|
||||
}
|
||||
|
||||
static prepareExporter = (): Promise<any> => {
|
||||
return new Promise((resolve, reject) => {
|
||||
findGDJS(gdjsRoot => {
|
||||
if (!gdjsRoot) {
|
||||
showErrorBox('Could not find GDJS');
|
||||
return reject();
|
||||
}
|
||||
console.info('GDJS found in ', gdjsRoot);
|
||||
|
||||
const fileSystem = assignIn(
|
||||
new gd.AbstractFileSystemJS(),
|
||||
localFileSystem
|
||||
);
|
||||
const exporter = new gd.Exporter(fileSystem, gdjsRoot);
|
||||
|
||||
resolve({
|
||||
exporter,
|
||||
});
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
launchExport = () => {
|
||||
const { project } = this.props;
|
||||
if (!project) return;
|
||||
|
||||
sendExportLaunched('local');
|
||||
|
||||
const outputDir = this.state.outputDir;
|
||||
project.setLastCompilationDirectory(outputDir);
|
||||
|
||||
LocalExport.prepareExporter()
|
||||
.then(({ exporter }) => {
|
||||
const exportOptions = new gd.MapStringBoolean();
|
||||
exporter.exportWholePixiProject(project, outputDir, exportOptions);
|
||||
exportOptions.delete();
|
||||
exporter.delete();
|
||||
|
||||
this.setState({
|
||||
exportFinishedDialogOpen: true,
|
||||
});
|
||||
})
|
||||
.catch(err => {
|
||||
showErrorBox('Unable to export the game', err);
|
||||
});
|
||||
};
|
||||
|
||||
openExportFolder = () => {
|
||||
if (shell) shell.openItem(this.state.outputDir);
|
||||
};
|
||||
|
||||
render() {
|
||||
const { project } = this.props;
|
||||
if (!project) return null;
|
||||
|
||||
return (
|
||||
<Column noMargin>
|
||||
<Line>
|
||||
<Text>
|
||||
<Trans>
|
||||
This will export your game to a folder that you can then upload on
|
||||
a website or on game hosting like itch.io.
|
||||
</Trans>
|
||||
</Text>
|
||||
</Line>
|
||||
<Line>
|
||||
<LocalFolderPicker
|
||||
type="export"
|
||||
value={this.state.outputDir}
|
||||
defaultPath={project.getLastCompilationDirectory()}
|
||||
onChange={value => this.setState({ outputDir: value })}
|
||||
fullWidth
|
||||
/>
|
||||
</Line>
|
||||
<Line>
|
||||
<Spacer expand />
|
||||
<RaisedButton
|
||||
label={<Trans>Export</Trans>}
|
||||
primary={true}
|
||||
onClick={this.launchExport}
|
||||
disabled={!this.state.outputDir}
|
||||
/>
|
||||
</Line>
|
||||
<Dialog
|
||||
title={<Trans>Export finished</Trans>}
|
||||
actions={[
|
||||
<FlatButton
|
||||
key="open"
|
||||
label={<Trans>Open folder</Trans>}
|
||||
primary={true}
|
||||
onClick={this.openExportFolder}
|
||||
/>,
|
||||
<FlatButton
|
||||
key="close"
|
||||
label={<Trans>Close</Trans>}
|
||||
primary={false}
|
||||
onClick={() =>
|
||||
this.setState({
|
||||
exportFinishedDialogOpen: false,
|
||||
})
|
||||
}
|
||||
/>,
|
||||
]}
|
||||
modal
|
||||
open={this.state.exportFinishedDialogOpen}
|
||||
>
|
||||
<Text>
|
||||
<Trans>
|
||||
You can now upload the game to a web hosting to play to the game.
|
||||
</Trans>
|
||||
</Text>
|
||||
<AlertMessage kind="warning">
|
||||
<Trans>
|
||||
Your game won't work if you open index.html on your computer. You
|
||||
must upload it to a web hosting (Kongregate, Itch.io, etc...) or a
|
||||
web server to run it.
|
||||
</Trans>
|
||||
</AlertMessage>
|
||||
<Spacer />
|
||||
<RaisedButton
|
||||
fullWidth
|
||||
primary
|
||||
onClick={() =>
|
||||
Window.openExternalURL(
|
||||
getHelpLink('/publishing/publishing-to-gamejolt-store')
|
||||
)
|
||||
}
|
||||
label={<Trans>Publish your game on Game Jolt</Trans>}
|
||||
/>
|
||||
<RaisedButton
|
||||
fullWidth
|
||||
primary
|
||||
onClick={() =>
|
||||
Window.openExternalURL(
|
||||
getHelpLink('/publishing/publishing-to-kongregate-store')
|
||||
)
|
||||
}
|
||||
label={<Trans>Publish your game on Kongregate</Trans>}
|
||||
/>
|
||||
<RaisedButton
|
||||
fullWidth
|
||||
primary
|
||||
onClick={() =>
|
||||
Window.openExternalURL(
|
||||
getHelpLink('/publishing/publishing-to-itch-io')
|
||||
)
|
||||
}
|
||||
label={<Trans>Publish your game on Itch.io</Trans>}
|
||||
/>
|
||||
<FlatButton
|
||||
fullWidth
|
||||
onClick={() => Window.openExternalURL(getHelpLink('/publishing'))}
|
||||
label={<Trans>Learn more about publishing</Trans>}
|
||||
/>
|
||||
</Dialog>
|
||||
</Column>
|
||||
);
|
||||
}
|
||||
}
|
@@ -0,0 +1,173 @@
|
||||
// @flow
|
||||
import { Trans } from '@lingui/macro';
|
||||
|
||||
import React from 'react';
|
||||
import RaisedButton from '../../UI/RaisedButton';
|
||||
import { Column, Line } from '../../UI/Grid';
|
||||
import { findGDJS } from './LocalGDJSFinder';
|
||||
import localFileSystem from './LocalFileSystem';
|
||||
import assignIn from 'lodash/assignIn';
|
||||
import optionalRequire from '../../Utils/OptionalRequire';
|
||||
import {
|
||||
type ExportPipeline,
|
||||
type ExportPipelineContext,
|
||||
} from '../ExportPipeline.flow';
|
||||
import LocalFilePicker from '../../UI/LocalFilePicker';
|
||||
import { archiveLocalFolder } from '../../Utils/LocalArchiver';
|
||||
import {
|
||||
ExplanationHeader,
|
||||
DoneFooter,
|
||||
} from '../GenericExporters/FacebookInstantGamesExport';
|
||||
const path = optionalRequire('path');
|
||||
const electron = optionalRequire('electron');
|
||||
const app = electron ? electron.remote.app : null;
|
||||
const shell = electron ? electron.shell : null;
|
||||
|
||||
const gd = global.gd;
|
||||
|
||||
type ExportState = {
|
||||
archiveOutputFilename: string,
|
||||
};
|
||||
|
||||
type PreparedExporter = {|
|
||||
exporter: gdjsExporter,
|
||||
temporaryOutputDir: string,
|
||||
|};
|
||||
|
||||
type ExportOutput = {|
|
||||
temporaryOutputDir: string,
|
||||
|};
|
||||
|
||||
type ResourcesDownloadOutput = {|
|
||||
temporaryOutputDir: string,
|
||||
|};
|
||||
|
||||
type CompressionOutput = string;
|
||||
|
||||
export const localFacebookInstantGamesExportPipeline: ExportPipeline<
|
||||
ExportState,
|
||||
PreparedExporter,
|
||||
ExportOutput,
|
||||
ResourcesDownloadOutput,
|
||||
CompressionOutput
|
||||
> = {
|
||||
name: 'local-facebook-instant-games',
|
||||
|
||||
getInitialExportState: (project: gdProject) => ({
|
||||
archiveOutputFilename: app
|
||||
? path.join(app.getPath('documents'), 'fb-instant-game.zip')
|
||||
: '',
|
||||
}),
|
||||
|
||||
canLaunchBuild: exportState => !!exportState.archiveOutputFilename,
|
||||
|
||||
renderHeader: ({ project, exportState, updateExportState }) => (
|
||||
<Column noMargin>
|
||||
<Line>
|
||||
<ExplanationHeader />
|
||||
</Line>
|
||||
<Line>
|
||||
<LocalFilePicker
|
||||
title={'Facebook Instant Games export zip file'}
|
||||
message={
|
||||
'Choose where to save the exported file for Facebook Instant Games'
|
||||
}
|
||||
filters={[
|
||||
{
|
||||
name: 'Compressed file for Facebook Instant Games',
|
||||
extensions: ['zip'],
|
||||
},
|
||||
]}
|
||||
value={exportState.archiveOutputFilename}
|
||||
defaultPath={app ? app.getPath('documents') : ''}
|
||||
onChange={value =>
|
||||
updateExportState(() => ({ archiveOutputFilename: value }))
|
||||
}
|
||||
fullWidth
|
||||
/>
|
||||
</Line>
|
||||
</Column>
|
||||
),
|
||||
|
||||
renderLaunchButtonLabel: () => <Trans>Package</Trans>,
|
||||
|
||||
prepareExporter: (
|
||||
context: ExportPipelineContext<ExportState>
|
||||
): Promise<PreparedExporter> => {
|
||||
return findGDJS().then(({ gdjsRoot }) => {
|
||||
console.info('GDJS found in ', gdjsRoot);
|
||||
|
||||
// TODO: Memory leak? Check for other exporters too.
|
||||
const fileSystem = assignIn(
|
||||
new gd.AbstractFileSystemJS(),
|
||||
localFileSystem
|
||||
);
|
||||
const exporter = new gd.Exporter(fileSystem, gdjsRoot);
|
||||
const temporaryOutputDir = path.join(
|
||||
fileSystem.getTempDir(),
|
||||
'FacebookInstantGamesExport'
|
||||
);
|
||||
fileSystem.mkDir(temporaryOutputDir);
|
||||
fileSystem.clearDir(temporaryOutputDir);
|
||||
|
||||
return {
|
||||
exporter,
|
||||
temporaryOutputDir,
|
||||
};
|
||||
});
|
||||
},
|
||||
|
||||
launchExport: (
|
||||
context: ExportPipelineContext<ExportState>,
|
||||
{ exporter, temporaryOutputDir }: PreparedExporter
|
||||
): Promise<ExportOutput> => {
|
||||
const exportOptions = new gd.MapStringBoolean();
|
||||
exportOptions.set('exportForFacebookInstantGames', true);
|
||||
exporter.exportWholePixiProject(
|
||||
context.project,
|
||||
temporaryOutputDir,
|
||||
exportOptions
|
||||
);
|
||||
exportOptions.delete();
|
||||
exporter.delete();
|
||||
|
||||
return Promise.resolve({ temporaryOutputDir });
|
||||
},
|
||||
|
||||
launchResourcesDownload: (
|
||||
context: ExportPipelineContext<ExportState>,
|
||||
{ temporaryOutputDir }: ExportOutput
|
||||
): Promise<ResourcesDownloadOutput> => {
|
||||
return Promise.resolve({ temporaryOutputDir });
|
||||
},
|
||||
|
||||
launchCompression: (
|
||||
context: ExportPipelineContext<ExportState>,
|
||||
{ temporaryOutputDir }: ResourcesDownloadOutput
|
||||
): Promise<CompressionOutput> => {
|
||||
return archiveLocalFolder({
|
||||
path: temporaryOutputDir,
|
||||
outputFilename: context.exportState.archiveOutputFilename,
|
||||
});
|
||||
},
|
||||
|
||||
renderDoneFooter: ({ exportState, onClose }) => {
|
||||
const openExportFolder = () => {
|
||||
if (shell && path)
|
||||
shell.openItem(path.dirname(exportState.archiveOutputFilename));
|
||||
};
|
||||
|
||||
return (
|
||||
<DoneFooter
|
||||
renderGameButton={() => (
|
||||
<RaisedButton
|
||||
key="open"
|
||||
label={<Trans>Open folder</Trans>}
|
||||
primary={true}
|
||||
onClick={openExportFolder}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
);
|
||||
},
|
||||
};
|
@@ -1,105 +0,0 @@
|
||||
// @flow
|
||||
import { Trans } from '@lingui/macro';
|
||||
|
||||
import * as React from 'react';
|
||||
import Stepper from '@material-ui/core/Stepper';
|
||||
import Step from '@material-ui/core/Step';
|
||||
import StepLabel from '@material-ui/core/StepLabel';
|
||||
import StepContent from '@material-ui/core/StepContent';
|
||||
import CircularProgress from '@material-ui/core/CircularProgress';
|
||||
import RaisedButton from '../../../UI/RaisedButton';
|
||||
import FlatButton from '../../../UI/FlatButton';
|
||||
import Text from '../../../UI/Text';
|
||||
import { Line, Spacer } from '../../../UI/Grid';
|
||||
import { type LocalFacebookInstantGamesExportStep } from '.';
|
||||
|
||||
type Props = {|
|
||||
exportStep: LocalFacebookInstantGamesExportStep,
|
||||
errored: boolean,
|
||||
onOpenExportFolder: () => void,
|
||||
onOpenLearnMore: () => void,
|
||||
|};
|
||||
|
||||
export default ({
|
||||
exportStep,
|
||||
errored,
|
||||
onOpenExportFolder,
|
||||
onOpenLearnMore,
|
||||
}: Props) => (
|
||||
<Stepper
|
||||
activeStep={
|
||||
exportStep === 'export'
|
||||
? 0
|
||||
: exportStep === 'compress'
|
||||
? 1
|
||||
: exportStep === 'done'
|
||||
? 2
|
||||
: -1
|
||||
}
|
||||
orientation="vertical"
|
||||
>
|
||||
<Step>
|
||||
<StepLabel>
|
||||
<Trans>Game export</Trans>
|
||||
</StepLabel>
|
||||
<StepContent>
|
||||
<Line alignItems="center">
|
||||
<CircularProgress size={20} />
|
||||
<Spacer />
|
||||
<Text>
|
||||
<Trans>Export in progress...</Trans>
|
||||
</Text>
|
||||
</Line>
|
||||
</StepContent>
|
||||
</Step>
|
||||
<Step>
|
||||
<StepLabel>
|
||||
<Trans>Upload to build service</Trans>
|
||||
</StepLabel>
|
||||
<StepContent>
|
||||
{errored ? (
|
||||
<Text>
|
||||
<Trans>
|
||||
Can't compress the game. Please check that you have rights to
|
||||
write on this computer.
|
||||
</Trans>
|
||||
</Text>
|
||||
) : (
|
||||
<Line alignItems="center">
|
||||
<CircularProgress size={20} />
|
||||
<Spacer />
|
||||
<Text>
|
||||
<Trans>Compressing...</Trans>
|
||||
</Text>
|
||||
</Line>
|
||||
)}
|
||||
</StepContent>
|
||||
</Step>
|
||||
<Step>
|
||||
<StepLabel>
|
||||
<Trans>Export finished</Trans>
|
||||
</StepLabel>
|
||||
<StepContent>
|
||||
<Line expand>
|
||||
<Text>
|
||||
<Trans>
|
||||
You can now create a game on Facebook Instant Games, if not
|
||||
already done, and upload the archive generated.
|
||||
</Trans>
|
||||
</Text>
|
||||
</Line>
|
||||
<Line expand>
|
||||
<FlatButton
|
||||
label={<Trans>Open folder</Trans>}
|
||||
onClick={onOpenExportFolder}
|
||||
/>
|
||||
<RaisedButton
|
||||
label={<Trans>Learn more about Instant Games publication</Trans>}
|
||||
primary
|
||||
onClick={onOpenLearnMore}
|
||||
/>
|
||||
</Line>
|
||||
</StepContent>
|
||||
</Step>
|
||||
</Stepper>
|
||||
);
|
@@ -1,212 +0,0 @@
|
||||
// @flow
|
||||
|
||||
import React, { Component } from 'react';
|
||||
import assignIn from 'lodash/assignIn';
|
||||
import RaisedButton from '../../../UI/RaisedButton';
|
||||
import { sendExportLaunched } from '../../../Utils/Analytics/EventSender';
|
||||
import { Column, Line, Spacer } from '../../../UI/Grid';
|
||||
import LocalFilePicker from '../../../UI/LocalFilePicker';
|
||||
import { showErrorBox } from '../../../UI/Messages/MessageBox';
|
||||
import { findGDJS } from '../LocalGDJSFinder';
|
||||
import localFileSystem from '../LocalFileSystem';
|
||||
import Progress from './Progress';
|
||||
import { archiveLocalFolder } from '../../../Utils/LocalArchiver';
|
||||
import optionalRequire from '../../../Utils/OptionalRequire.js';
|
||||
import Window from '../../../Utils/Window';
|
||||
import { getHelpLink } from '../../../Utils/HelpLink';
|
||||
import Text from '../../../UI/Text';
|
||||
const path = optionalRequire('path');
|
||||
const electron = optionalRequire('electron');
|
||||
const app = electron ? electron.remote.app : null;
|
||||
const shell = electron ? electron.shell : null;
|
||||
|
||||
const gd = global.gd;
|
||||
|
||||
export type LocalFacebookInstantGamesExportStep =
|
||||
| ''
|
||||
| 'export'
|
||||
| 'compress'
|
||||
| 'done';
|
||||
|
||||
type State = {|
|
||||
archiveOutputFilename: string,
|
||||
exportStep: LocalFacebookInstantGamesExportStep,
|
||||
errored: boolean,
|
||||
|};
|
||||
|
||||
type Props = {
|
||||
project: gdProject,
|
||||
onChangeSubscription: Function,
|
||||
};
|
||||
|
||||
class LocalFacebookInstantGamesExport extends Component<Props, State> {
|
||||
state = {
|
||||
archiveOutputFilename: app
|
||||
? path.join(app.getPath('documents'), 'fb-instant-game.zip')
|
||||
: '',
|
||||
exportStep: '',
|
||||
errored: false,
|
||||
};
|
||||
|
||||
static prepareExporter = (): Promise<any> => {
|
||||
return new Promise((resolve, reject) => {
|
||||
findGDJS(gdjsRoot => {
|
||||
if (!gdjsRoot) {
|
||||
showErrorBox('Could not find GDJS');
|
||||
return reject();
|
||||
}
|
||||
console.info('GDJS found in ', gdjsRoot);
|
||||
|
||||
const fileSystem = assignIn(
|
||||
new gd.AbstractFileSystemJS(),
|
||||
localFileSystem
|
||||
);
|
||||
const exporter = new gd.Exporter(fileSystem, gdjsRoot);
|
||||
const outputDir = path.join(
|
||||
fileSystem.getTempDir(),
|
||||
'FacebookInstantGamesExport'
|
||||
);
|
||||
fileSystem.mkDir(outputDir);
|
||||
fileSystem.clearDir(outputDir);
|
||||
|
||||
resolve({
|
||||
exporter,
|
||||
outputDir,
|
||||
});
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
launchExport = (): Promise<string> => {
|
||||
const t = str => str; //TODO;
|
||||
const { project } = this.props;
|
||||
if (!project) return Promise.reject();
|
||||
|
||||
return LocalFacebookInstantGamesExport.prepareExporter()
|
||||
.then(({ exporter, outputDir }) => {
|
||||
const exportOptions = new gd.MapStringBoolean();
|
||||
exportOptions.set('exportForFacebookInstantGames', true);
|
||||
exporter.exportWholePixiProject(project, outputDir, exportOptions);
|
||||
exportOptions.delete();
|
||||
exporter.delete();
|
||||
|
||||
return outputDir;
|
||||
})
|
||||
.catch(err => {
|
||||
showErrorBox(t('Unable to export the game'), err);
|
||||
throw err;
|
||||
});
|
||||
};
|
||||
|
||||
launchCompression = (outputDir: string): Promise<string> => {
|
||||
return archiveLocalFolder({
|
||||
path: outputDir,
|
||||
outputFilename: this.state.archiveOutputFilename,
|
||||
});
|
||||
};
|
||||
|
||||
launchWholeExport = () => {
|
||||
const t = str => str; //TODO
|
||||
sendExportLaunched('local-facebook-instant-games');
|
||||
|
||||
const handleError = (message: string) => err => {
|
||||
if (!this.state.errored) {
|
||||
this.setState({
|
||||
errored: true,
|
||||
});
|
||||
showErrorBox(message, {
|
||||
exportStep: this.state.exportStep,
|
||||
rawError: err,
|
||||
});
|
||||
}
|
||||
|
||||
throw err;
|
||||
};
|
||||
|
||||
this.setState({
|
||||
exportStep: 'export',
|
||||
errored: false,
|
||||
});
|
||||
this.launchExport()
|
||||
.then(outputDir => {
|
||||
this.setState({
|
||||
exportStep: 'compress',
|
||||
});
|
||||
return this.launchCompression(outputDir);
|
||||
}, handleError(t('Error while exporting the game.')))
|
||||
.then(() => {
|
||||
this.setState({
|
||||
exportStep: 'done',
|
||||
});
|
||||
}, handleError(t('Error while building the game.')))
|
||||
.catch(() => {
|
||||
/* Error handled previously */
|
||||
});
|
||||
};
|
||||
|
||||
openExportFolder = () => {
|
||||
if (shell) shell.openItem(path.dirname(this.state.archiveOutputFilename));
|
||||
};
|
||||
|
||||
openLearnMore = () => {
|
||||
Window.openExternalURL(
|
||||
getHelpLink('/publishing/publishing-to-facebook-instant-games')
|
||||
);
|
||||
};
|
||||
|
||||
render() {
|
||||
const { exportStep, errored } = this.state;
|
||||
const t = str => str; //TODO;
|
||||
const { project } = this.props;
|
||||
if (!project) return null;
|
||||
|
||||
return (
|
||||
<Column noMargin>
|
||||
<Line>
|
||||
<Text>
|
||||
{t(
|
||||
'Prepare your game for Facebook Instant Games so that it can be play on Facebook Messenger. GDevelop will create a compressed file that you can upload on your Facebook Developer account.'
|
||||
)}
|
||||
</Text>
|
||||
</Line>
|
||||
<Line>
|
||||
<LocalFilePicker
|
||||
title={t('Facebook Instant Games export zip file')}
|
||||
message={t(
|
||||
'Choose where to save the exported file for Facebook Instant Games'
|
||||
)}
|
||||
filters={[
|
||||
{
|
||||
name: t('Compressed file for Facebook Instant Games'),
|
||||
extensions: ['zip'],
|
||||
},
|
||||
]}
|
||||
value={this.state.archiveOutputFilename}
|
||||
defaultPath={app ? app.getPath('documents') : ''}
|
||||
onChange={value => this.setState({ archiveOutputFilename: value })}
|
||||
fullWidth
|
||||
/>
|
||||
</Line>
|
||||
<Line>
|
||||
<Spacer expand />
|
||||
<RaisedButton
|
||||
label={t('Export')}
|
||||
primary
|
||||
onClick={this.launchWholeExport}
|
||||
disabled={!this.state.archiveOutputFilename}
|
||||
/>
|
||||
</Line>
|
||||
<Line>
|
||||
<Progress
|
||||
exportStep={exportStep}
|
||||
errored={errored}
|
||||
onOpenExportFolder={this.openExportFolder}
|
||||
onOpenLearnMore={this.openLearnMore}
|
||||
/>
|
||||
</Line>
|
||||
</Column>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default LocalFacebookInstantGamesExport;
|
@@ -87,22 +87,6 @@ export default {
|
||||
}
|
||||
return true;
|
||||
},
|
||||
copyDir: function(source, dest) {
|
||||
throw new Error(
|
||||
'copyDir was never tested, please check that it does what you want to.'
|
||||
);
|
||||
|
||||
// try {
|
||||
// if (source !== dest)
|
||||
// fs.copySync(source, dest, {
|
||||
// clobber: true,
|
||||
// });
|
||||
// } catch (e) {
|
||||
// console.log('copyDir(' + source + ', ' + dest + ') failed: ' + e);
|
||||
// return false;
|
||||
// }
|
||||
// return true;
|
||||
},
|
||||
writeToFile: function(file, contents) {
|
||||
try {
|
||||
fs.outputFileSync(file, contents);
|
||||
|
@@ -1,3 +1,4 @@
|
||||
// @flow
|
||||
// Note: this file don't use export/imports nor Flow to allow its usage from Node.js
|
||||
|
||||
const optionalRequire = require('../../Utils/OptionalRequire.js');
|
||||
@@ -18,8 +19,8 @@ const tryPath = (
|
||||
else onNoAccess();
|
||||
});
|
||||
|
||||
const findGDJS = (cb /*: (?string) => void*/) => {
|
||||
if (!path || !process || !fs) return '';
|
||||
const findGDJS = () /*: Promise<{|gdjsRoot: string|}> */ => {
|
||||
if (!path || !process || !fs) return Promise.reject(new Error('Unsupported'));
|
||||
|
||||
const appPath = app ? app.getAppPath() : process.cwd();
|
||||
|
||||
@@ -28,21 +29,24 @@ const findGDJS = (cb /*: (?string) => void*/) => {
|
||||
const pathToRoot = isDarwin ? '../../../../' : path.join('..', '..');
|
||||
const rootPath = path.join(appPath, pathToRoot);
|
||||
|
||||
// First try to find GDJS in the parent folder (when newIDE is inside IDE)
|
||||
tryPath(path.join(rootPath, '..', 'JsPlatform'), cb, () => {
|
||||
// Or in the resources (for a standalone newIDE)
|
||||
tryPath(path.join(appPath, '..', 'GDJS'), cb, () => {
|
||||
// Or in the resources when developing with Electron
|
||||
const devPath = path.join(
|
||||
appPath,
|
||||
'..',
|
||||
'..',
|
||||
'app',
|
||||
'resources',
|
||||
'GDJS'
|
||||
);
|
||||
tryPath(devPath, cb, () => {
|
||||
cb(null);
|
||||
return new Promise((resolve, reject) => {
|
||||
const onFound = gdjsRoot => resolve({ gdjsRoot });
|
||||
const onNotFound = () => reject(new Error('Could not find GDJS'));
|
||||
|
||||
// First try to find GDJS in the parent folder (when newIDE is inside IDE)
|
||||
tryPath(path.join(rootPath, '..', 'JsPlatform'), onFound, () => {
|
||||
// Or in the resources (for a standalone newIDE)
|
||||
tryPath(path.join(appPath, '..', 'GDJS'), onFound, () => {
|
||||
// Or in the resources when developing with Electron
|
||||
const devPath = path.join(
|
||||
appPath,
|
||||
'..',
|
||||
'..',
|
||||
'app',
|
||||
'resources',
|
||||
'GDJS'
|
||||
);
|
||||
tryPath(devPath, onFound, onNotFound);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
139
newIDE/app/src/Export/LocalExporters/LocalHTML5Export.js
Normal file
@@ -0,0 +1,139 @@
|
||||
// @flow
|
||||
import { Trans } from '@lingui/macro';
|
||||
|
||||
import React from 'react';
|
||||
import RaisedButton from '../../UI/RaisedButton';
|
||||
import { Column, Line } from '../../UI/Grid';
|
||||
import { findGDJS } from './LocalGDJSFinder';
|
||||
import localFileSystem from './LocalFileSystem';
|
||||
import LocalFolderPicker from '../../UI/LocalFolderPicker';
|
||||
import assignIn from 'lodash/assignIn';
|
||||
import {
|
||||
type ExportPipeline,
|
||||
type ExportPipelineContext,
|
||||
} from '../ExportPipeline.flow';
|
||||
import optionalRequire from '../../Utils/OptionalRequire';
|
||||
import { ExplanationHeader, DoneFooter } from '../GenericExporters/HTML5Export';
|
||||
const electron = optionalRequire('electron');
|
||||
const shell = electron ? electron.shell : null;
|
||||
|
||||
const gd = global.gd;
|
||||
|
||||
type ExportState = {
|
||||
outputDir: string,
|
||||
};
|
||||
|
||||
type PreparedExporter = {|
|
||||
exporter: gdjsExporter,
|
||||
|};
|
||||
|
||||
type ExportOutput = null;
|
||||
|
||||
type ResourcesDownloadOutput = null;
|
||||
|
||||
type CompressionOutput = null;
|
||||
|
||||
export const localHTML5ExportPipeline: ExportPipeline<
|
||||
ExportState,
|
||||
PreparedExporter,
|
||||
ExportOutput,
|
||||
ResourcesDownloadOutput,
|
||||
CompressionOutput
|
||||
> = {
|
||||
name: 'local-html5',
|
||||
|
||||
getInitialExportState: (project: gdProject) => ({
|
||||
outputDir: project.getLastCompilationDirectory(),
|
||||
}),
|
||||
|
||||
canLaunchBuild: exportState => !!exportState.outputDir,
|
||||
|
||||
renderHeader: ({ project, exportState, updateExportState }) => (
|
||||
<Column noMargin>
|
||||
<Line>
|
||||
<ExplanationHeader />
|
||||
</Line>
|
||||
<Line>
|
||||
<LocalFolderPicker
|
||||
type="export"
|
||||
value={exportState.outputDir}
|
||||
defaultPath={project.getLastCompilationDirectory()}
|
||||
onChange={outputDir => {
|
||||
updateExportState(() => ({ outputDir }));
|
||||
}}
|
||||
fullWidth
|
||||
/>
|
||||
</Line>
|
||||
</Column>
|
||||
),
|
||||
|
||||
renderLaunchButtonLabel: () => <Trans>Export as a HTML5 game</Trans>,
|
||||
|
||||
prepareExporter: (
|
||||
context: ExportPipelineContext<ExportState>
|
||||
): Promise<PreparedExporter> => {
|
||||
return findGDJS().then(({ gdjsRoot }) => {
|
||||
console.info('GDJS found in ', gdjsRoot);
|
||||
|
||||
// TODO: Memory leak? Check for other exporters too.
|
||||
const fileSystem = assignIn(
|
||||
new gd.AbstractFileSystemJS(),
|
||||
localFileSystem
|
||||
);
|
||||
const exporter = new gd.Exporter(fileSystem, gdjsRoot);
|
||||
|
||||
return {
|
||||
exporter,
|
||||
};
|
||||
});
|
||||
},
|
||||
|
||||
launchExport: (
|
||||
context: ExportPipelineContext<ExportState>,
|
||||
{ exporter }: PreparedExporter
|
||||
): Promise<ExportOutput> => {
|
||||
const exportOptions = new gd.MapStringBoolean();
|
||||
exporter.exportWholePixiProject(
|
||||
context.project,
|
||||
context.exportState.outputDir,
|
||||
exportOptions
|
||||
);
|
||||
exportOptions.delete();
|
||||
exporter.delete();
|
||||
|
||||
return Promise.resolve(null);
|
||||
},
|
||||
|
||||
launchResourcesDownload: (
|
||||
context: ExportPipelineContext<ExportState>,
|
||||
exportOutput: ExportOutput
|
||||
): Promise<ResourcesDownloadOutput> => {
|
||||
return Promise.resolve(null);
|
||||
},
|
||||
|
||||
launchCompression: (
|
||||
context: ExportPipelineContext<ExportState>,
|
||||
exportOutput: ResourcesDownloadOutput
|
||||
): Promise<CompressionOutput> => {
|
||||
return Promise.resolve(null);
|
||||
},
|
||||
|
||||
renderDoneFooter: ({ exportState, onClose }) => {
|
||||
const openExportFolder = () => {
|
||||
if (shell) shell.openItem(exportState.outputDir);
|
||||
};
|
||||
|
||||
return (
|
||||
<DoneFooter
|
||||
renderGameButton={() => (
|
||||
<RaisedButton
|
||||
fullWidth
|
||||
primary
|
||||
onClick={() => openExportFolder()}
|
||||
label={<Trans>Open the exported game folder</Trans>}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
);
|
||||
},
|
||||
};
|
158
newIDE/app/src/Export/LocalExporters/LocalOnlineCordovaExport.js
Normal file
@@ -0,0 +1,158 @@
|
||||
// @flow
|
||||
import * as React from 'react';
|
||||
import { Trans } from '@lingui/macro';
|
||||
import assignIn from 'lodash/assignIn';
|
||||
import {
|
||||
type Build,
|
||||
buildCordovaAndroid,
|
||||
} from '../../Utils/GDevelopServices/Build';
|
||||
import { type UserProfile } from '../../Profile/UserProfileContext';
|
||||
import { findGDJS } from './LocalGDJSFinder';
|
||||
import { archiveLocalFolder } from '../../Utils/LocalArchiver';
|
||||
import optionalRequire from '../../Utils/OptionalRequire.js';
|
||||
import localFileSystem from './LocalFileSystem';
|
||||
import {
|
||||
type ExportPipeline,
|
||||
type ExportPipelineContext,
|
||||
} from '../ExportPipeline.flow';
|
||||
import { ExplanationHeader } from '../GenericExporters/OnlineCordovaExport';
|
||||
const path = optionalRequire('path');
|
||||
const os = optionalRequire('os');
|
||||
const electron = optionalRequire('electron');
|
||||
const ipcRenderer = electron ? electron.ipcRenderer : null;
|
||||
const gd = global.gd;
|
||||
|
||||
type ExportState = null;
|
||||
|
||||
type PreparedExporter = {|
|
||||
exporter: gdjsExporter,
|
||||
temporaryOutputDir: string,
|
||||
|};
|
||||
|
||||
type ExportOutput = {|
|
||||
temporaryOutputDir: string,
|
||||
|};
|
||||
|
||||
type ResourcesDownloadOutput = {|
|
||||
temporaryOutputDir: string,
|
||||
|};
|
||||
|
||||
type CompressionOutput = string;
|
||||
|
||||
export const localOnlineCordovaExportPipeline: ExportPipeline<
|
||||
ExportState,
|
||||
PreparedExporter,
|
||||
ExportOutput,
|
||||
ResourcesDownloadOutput,
|
||||
CompressionOutput
|
||||
> = {
|
||||
name: 'local-online-cordova',
|
||||
onlineBuildType: 'cordova-build',
|
||||
|
||||
getInitialExportState: () => null,
|
||||
|
||||
canLaunchBuild: () => true,
|
||||
|
||||
renderHeader: () => <ExplanationHeader />,
|
||||
|
||||
renderLaunchButtonLabel: () => <Trans>Packaging for Android</Trans>,
|
||||
|
||||
prepareExporter: (
|
||||
context: ExportPipelineContext<ExportState>
|
||||
): Promise<PreparedExporter> => {
|
||||
return findGDJS().then(({ gdjsRoot }) => {
|
||||
console.info('GDJS found in ', gdjsRoot);
|
||||
|
||||
const fileSystem = assignIn(
|
||||
new gd.AbstractFileSystemJS(),
|
||||
localFileSystem
|
||||
);
|
||||
const exporter = new gd.Exporter(fileSystem, gdjsRoot);
|
||||
const temporaryOutputDir = path.join(
|
||||
fileSystem.getTempDir(),
|
||||
'OnlineCordovaExport'
|
||||
);
|
||||
fileSystem.mkDir(temporaryOutputDir);
|
||||
fileSystem.clearDir(temporaryOutputDir);
|
||||
|
||||
return {
|
||||
exporter,
|
||||
temporaryOutputDir,
|
||||
};
|
||||
});
|
||||
},
|
||||
|
||||
launchExport: (
|
||||
context: ExportPipelineContext<ExportState>,
|
||||
{ exporter, temporaryOutputDir }: PreparedExporter
|
||||
): Promise<ExportOutput> => {
|
||||
const exportOptions = new gd.MapStringBoolean();
|
||||
exportOptions.set('exportForCordova', true);
|
||||
exporter.exportWholePixiProject(
|
||||
context.project,
|
||||
temporaryOutputDir,
|
||||
exportOptions
|
||||
);
|
||||
exportOptions.delete();
|
||||
exporter.delete();
|
||||
|
||||
return Promise.resolve({ temporaryOutputDir });
|
||||
},
|
||||
|
||||
launchResourcesDownload: (
|
||||
context: ExportPipelineContext<ExportState>,
|
||||
{ temporaryOutputDir }: ExportOutput
|
||||
): Promise<ResourcesDownloadOutput> => {
|
||||
return Promise.resolve({ temporaryOutputDir });
|
||||
},
|
||||
|
||||
launchCompression: (
|
||||
context: ExportPipelineContext<ExportState>,
|
||||
{ temporaryOutputDir }: ResourcesDownloadOutput
|
||||
): Promise<CompressionOutput> => {
|
||||
const archiveOutputDir = os.tmpdir();
|
||||
return archiveLocalFolder({
|
||||
path: temporaryOutputDir,
|
||||
outputFilename: path.join(archiveOutputDir, 'game-archive.zip'),
|
||||
});
|
||||
},
|
||||
|
||||
launchUpload: (
|
||||
context: ExportPipelineContext<ExportState>,
|
||||
outputFile: CompressionOutput
|
||||
): Promise<string> => {
|
||||
if (!ipcRenderer) return Promise.reject('No support for upload');
|
||||
|
||||
ipcRenderer.removeAllListeners('s3-file-upload-progress');
|
||||
ipcRenderer.removeAllListeners('s3-file-upload-done');
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
ipcRenderer.on(
|
||||
's3-file-upload-progress',
|
||||
(event, stepCurrentProgress, stepMaxProgress) => {
|
||||
context.updateStepProgress(stepCurrentProgress, stepMaxProgress);
|
||||
}
|
||||
);
|
||||
ipcRenderer.on('s3-file-upload-done', (event, err, prefix) => {
|
||||
if (err) return reject(err);
|
||||
resolve(prefix);
|
||||
});
|
||||
ipcRenderer.send('s3-file-upload', outputFile);
|
||||
});
|
||||
},
|
||||
|
||||
launchOnlineBuild: (
|
||||
exportState: ExportState,
|
||||
userProfile: UserProfile,
|
||||
uploadBucketKey: string
|
||||
): Promise<Build> => {
|
||||
const { getAuthorizationHeader, profile } = userProfile;
|
||||
if (!profile) return Promise.reject(new Error('User is not authenticated'));
|
||||
|
||||
return buildCordovaAndroid(
|
||||
getAuthorizationHeader,
|
||||
profile.uid,
|
||||
uploadBucketKey
|
||||
);
|
||||
},
|
||||
};
|
@@ -1,313 +0,0 @@
|
||||
// @flow
|
||||
|
||||
import React, { Component } from 'react';
|
||||
import assignIn from 'lodash/assignIn';
|
||||
import RaisedButton from '../../../UI/RaisedButton';
|
||||
import { sendExportLaunched } from '../../../Utils/Analytics/EventSender';
|
||||
import {
|
||||
type Build,
|
||||
buildCordovaAndroid,
|
||||
getUrl,
|
||||
} from '../../../Utils/GDevelopServices/Build';
|
||||
import { type UserProfile } from '../../../Profile/UserProfileContext';
|
||||
import { Column, Line } from '../../../UI/Grid';
|
||||
import { showErrorBox } from '../../../UI/Messages/MessageBox';
|
||||
import { findGDJS } from '../LocalGDJSFinder';
|
||||
import localFileSystem from '../LocalFileSystem';
|
||||
import { archiveLocalFolder } from '../../../Utils/LocalArchiver';
|
||||
import optionalRequire from '../../../Utils/OptionalRequire.js';
|
||||
import Window from '../../../Utils/Window';
|
||||
import CreateProfile from '../../../Profile/CreateProfile';
|
||||
import LimitDisplayer from '../../../Profile/LimitDisplayer';
|
||||
import {
|
||||
displayProjectErrorsBox,
|
||||
getErrors,
|
||||
} from '../../../ProjectManager/ProjectErrorsChecker';
|
||||
import { type Limit } from '../../../Utils/GDevelopServices/Usage';
|
||||
import BuildsWatcher from '../../Builds/BuildsWatcher';
|
||||
import BuildStepsProgress, {
|
||||
type BuildStep,
|
||||
} from '../../Builds/BuildStepsProgress';
|
||||
import Text from '../../../UI/Text';
|
||||
const path = optionalRequire('path');
|
||||
const os = optionalRequire('os');
|
||||
const electron = optionalRequire('electron');
|
||||
const ipcRenderer = electron ? electron.ipcRenderer : null;
|
||||
const gd = global.gd;
|
||||
|
||||
type State = {
|
||||
exportStep: BuildStep,
|
||||
build: ?Build,
|
||||
uploadProgress: number,
|
||||
uploadMax: number,
|
||||
errored: boolean,
|
||||
};
|
||||
|
||||
type Props = {
|
||||
project: gdProject,
|
||||
onChangeSubscription: Function,
|
||||
userProfile: UserProfile,
|
||||
};
|
||||
|
||||
class LocalOnlineCordovaExport extends Component<Props, State> {
|
||||
state = {
|
||||
exportStep: '',
|
||||
build: null,
|
||||
uploadProgress: 0,
|
||||
uploadMax: 0,
|
||||
errored: false,
|
||||
};
|
||||
buildsWatcher = new BuildsWatcher();
|
||||
|
||||
static prepareExporter = (): Promise<any> => {
|
||||
return new Promise((resolve, reject) => {
|
||||
findGDJS(gdjsRoot => {
|
||||
if (!gdjsRoot) {
|
||||
showErrorBox('Could not find GDJS');
|
||||
return reject();
|
||||
}
|
||||
console.info('GDJS found in ', gdjsRoot);
|
||||
|
||||
const fileSystem = assignIn(
|
||||
new gd.AbstractFileSystemJS(),
|
||||
localFileSystem
|
||||
);
|
||||
const exporter = new gd.Exporter(fileSystem, gdjsRoot);
|
||||
const outputDir = path.join(
|
||||
fileSystem.getTempDir(),
|
||||
'OnlineCordovaExport'
|
||||
);
|
||||
fileSystem.mkDir(outputDir);
|
||||
fileSystem.clearDir(outputDir);
|
||||
|
||||
resolve({
|
||||
exporter,
|
||||
outputDir,
|
||||
});
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
componentWillUnmount() {
|
||||
this.buildsWatcher.stop();
|
||||
}
|
||||
|
||||
launchExport = (): Promise<string> => {
|
||||
const t = str => str; //TODO;
|
||||
const { project } = this.props;
|
||||
if (!project) return Promise.reject();
|
||||
|
||||
return LocalOnlineCordovaExport.prepareExporter()
|
||||
.then(({ exporter, outputDir }) => {
|
||||
const exportOptions = new gd.MapStringBoolean();
|
||||
exportOptions.set('exportForCordova', true);
|
||||
exporter.exportWholePixiProject(project, outputDir, exportOptions);
|
||||
exportOptions.delete();
|
||||
exporter.delete();
|
||||
|
||||
return outputDir;
|
||||
})
|
||||
.catch(err => {
|
||||
showErrorBox(t('Unable to export the game'), err);
|
||||
throw err;
|
||||
});
|
||||
};
|
||||
|
||||
launchCompression = (outputDir: string): Promise<string> => {
|
||||
const archiveOutputDir = os.tmpdir();
|
||||
return archiveLocalFolder({
|
||||
path: outputDir,
|
||||
outputFilename: path.join(archiveOutputDir, 'game-archive.zip'),
|
||||
});
|
||||
};
|
||||
|
||||
launchUpload = (outputFile: string): Promise<string> => {
|
||||
if (!ipcRenderer) return Promise.reject('No support for upload');
|
||||
|
||||
ipcRenderer.removeAllListeners('s3-file-upload-progress');
|
||||
ipcRenderer.removeAllListeners('s3-file-upload-done');
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
ipcRenderer.on(
|
||||
's3-file-upload-progress',
|
||||
(event, uploadProgress, uploadMax) => {
|
||||
this.setState({
|
||||
uploadProgress,
|
||||
uploadMax,
|
||||
});
|
||||
}
|
||||
);
|
||||
ipcRenderer.on('s3-file-upload-done', (event, err, prefix) => {
|
||||
if (err) return reject(err);
|
||||
resolve(prefix);
|
||||
});
|
||||
ipcRenderer.send('s3-file-upload', outputFile);
|
||||
});
|
||||
};
|
||||
|
||||
launchBuild = (
|
||||
userProfile: UserProfile,
|
||||
uploadBucketKey: string
|
||||
): Promise<Build> => {
|
||||
const { getAuthorizationHeader, profile } = userProfile;
|
||||
if (!profile) return Promise.reject(new Error('User is not authenticated'));
|
||||
|
||||
return buildCordovaAndroid(
|
||||
getAuthorizationHeader,
|
||||
profile.uid,
|
||||
uploadBucketKey
|
||||
);
|
||||
};
|
||||
|
||||
startBuildWatch = (userProfile: UserProfile) => {
|
||||
if (!this.state.build) return;
|
||||
|
||||
this.buildsWatcher.start({
|
||||
userProfile,
|
||||
builds: [this.state.build],
|
||||
onBuildUpdated: (build: Build) => this.setState({ build }),
|
||||
});
|
||||
};
|
||||
|
||||
launchWholeExport = (userProfile: UserProfile) => {
|
||||
const t = str => str; //TODO;
|
||||
const { project } = this.props;
|
||||
sendExportLaunched('local-online-cordova');
|
||||
|
||||
if (!displayProjectErrorsBox(t, getErrors(t, project))) return;
|
||||
|
||||
const handleError = (message: string) => err => {
|
||||
if (!this.state.errored) {
|
||||
this.setState({
|
||||
errored: true,
|
||||
});
|
||||
showErrorBox(message + (err.message ? `\n${err.message}` : ''), {
|
||||
exportStep: this.state.exportStep,
|
||||
rawError: err,
|
||||
});
|
||||
}
|
||||
|
||||
throw err;
|
||||
};
|
||||
|
||||
this.setState({
|
||||
exportStep: 'export',
|
||||
uploadProgress: 0,
|
||||
uploadMax: 0,
|
||||
errored: false,
|
||||
build: null,
|
||||
});
|
||||
this.launchExport()
|
||||
.then(outputDir => {
|
||||
this.setState({
|
||||
exportStep: 'compress',
|
||||
});
|
||||
return this.launchCompression(outputDir);
|
||||
}, handleError(t('Error while exporting the game.')))
|
||||
.then(outputFile => {
|
||||
this.setState({
|
||||
exportStep: 'upload',
|
||||
});
|
||||
return this.launchUpload(outputFile);
|
||||
}, handleError(t('Error while compressing the game.')))
|
||||
.then((uploadBucketKey: string) => {
|
||||
this.setState({
|
||||
exportStep: 'waiting-for-build',
|
||||
});
|
||||
return this.launchBuild(userProfile, uploadBucketKey);
|
||||
}, handleError(t('Error while uploading the game. Check your internet connection or try again later.')))
|
||||
.then(build => {
|
||||
this.setState(
|
||||
{
|
||||
build,
|
||||
exportStep: 'build',
|
||||
},
|
||||
() => {
|
||||
this.startBuildWatch(userProfile);
|
||||
}
|
||||
);
|
||||
}, handleError(t('Error while lauching the build of the game.')))
|
||||
.catch(() => {
|
||||
/* Error handled previously */
|
||||
});
|
||||
};
|
||||
|
||||
_download = (key: string) => {
|
||||
if (!this.state.build || !this.state.build[key]) return;
|
||||
|
||||
Window.openExternalURL(getUrl(this.state.build[key]));
|
||||
};
|
||||
|
||||
render() {
|
||||
const {
|
||||
exportStep,
|
||||
build,
|
||||
uploadMax,
|
||||
uploadProgress,
|
||||
errored,
|
||||
} = this.state;
|
||||
const t = str => str; //TODO;
|
||||
const { project, userProfile } = this.props;
|
||||
if (!project) return null;
|
||||
|
||||
const getBuildLimit = (userProfile: UserProfile): ?Limit =>
|
||||
userProfile.limits ? userProfile.limits['cordova-build'] : null;
|
||||
const canLaunchBuild = (userProfile: UserProfile) => {
|
||||
if (!errored && exportStep !== '' && exportStep !== 'build') return false;
|
||||
|
||||
const limit: ?Limit = getBuildLimit(userProfile);
|
||||
if (limit && limit.limitReached) return false;
|
||||
|
||||
return true;
|
||||
};
|
||||
|
||||
return (
|
||||
<Column noMargin>
|
||||
<Line>
|
||||
<Text>
|
||||
{t(
|
||||
'Packaging your game for Android will create an APK file that can be installed on Android phones, based on Cordova framework.'
|
||||
)}
|
||||
</Text>
|
||||
</Line>
|
||||
{userProfile.authenticated && (
|
||||
<Line justifyContent="center">
|
||||
<RaisedButton
|
||||
label={t('Package for Android')}
|
||||
primary
|
||||
onClick={() => this.launchWholeExport(userProfile)}
|
||||
disabled={!canLaunchBuild(userProfile)}
|
||||
/>
|
||||
</Line>
|
||||
)}
|
||||
{userProfile.authenticated && (
|
||||
<LimitDisplayer
|
||||
subscription={userProfile.subscription}
|
||||
limit={getBuildLimit(userProfile)}
|
||||
onChangeSubscription={this.props.onChangeSubscription}
|
||||
/>
|
||||
)}
|
||||
{!userProfile.authenticated && (
|
||||
<CreateProfile
|
||||
message={t(
|
||||
'Create an account to build your game for Android in one-click:'
|
||||
)}
|
||||
onLogin={userProfile.onLogin}
|
||||
/>
|
||||
)}
|
||||
<Line expand>
|
||||
<BuildStepsProgress
|
||||
exportStep={exportStep}
|
||||
build={build}
|
||||
onDownload={this._download}
|
||||
uploadMax={uploadMax}
|
||||
uploadProgress={uploadProgress}
|
||||
errored={errored}
|
||||
/>
|
||||
</Line>
|
||||
</Column>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default LocalOnlineCordovaExport;
|
@@ -0,0 +1,159 @@
|
||||
// @flow
|
||||
import * as React from 'react';
|
||||
import { Trans } from '@lingui/macro';
|
||||
import assignIn from 'lodash/assignIn';
|
||||
import { type Build, buildElectron } from '../../Utils/GDevelopServices/Build';
|
||||
import { type UserProfile } from '../../Profile/UserProfileContext';
|
||||
import { findGDJS } from './LocalGDJSFinder';
|
||||
import { archiveLocalFolder } from '../../Utils/LocalArchiver';
|
||||
import optionalRequire from '../../Utils/OptionalRequire.js';
|
||||
import localFileSystem from './LocalFileSystem';
|
||||
import {
|
||||
type ExportPipeline,
|
||||
type ExportPipelineContext,
|
||||
} from '../ExportPipeline.flow';
|
||||
import {
|
||||
type ExportState,
|
||||
SetupExportHeader,
|
||||
} from '../GenericExporters/OnlineElectronExport';
|
||||
const path = optionalRequire('path');
|
||||
const os = optionalRequire('os');
|
||||
const electron = optionalRequire('electron');
|
||||
const ipcRenderer = electron ? electron.ipcRenderer : null;
|
||||
const gd = global.gd;
|
||||
|
||||
type PreparedExporter = {|
|
||||
exporter: gdjsExporter,
|
||||
temporaryOutputDir: string,
|
||||
|};
|
||||
|
||||
type ExportOutput = {|
|
||||
temporaryOutputDir: string,
|
||||
|};
|
||||
|
||||
type ResourcesDownloadOutput = {|
|
||||
temporaryOutputDir: string,
|
||||
|};
|
||||
|
||||
type CompressionOutput = string;
|
||||
|
||||
export const localOnlineElectronExportPipeline: ExportPipeline<
|
||||
ExportState,
|
||||
PreparedExporter,
|
||||
ExportOutput,
|
||||
ResourcesDownloadOutput,
|
||||
CompressionOutput
|
||||
> = {
|
||||
name: 'local-online-electron',
|
||||
onlineBuildType: 'electron-build',
|
||||
|
||||
getInitialExportState: () => ({
|
||||
targets: ['winExe'],
|
||||
}),
|
||||
|
||||
canLaunchBuild: (exportState: ExportState) => !!exportState.targets.length,
|
||||
|
||||
renderHeader: props => <SetupExportHeader {...props} />,
|
||||
|
||||
renderLaunchButtonLabel: () => <Trans>Package</Trans>,
|
||||
|
||||
prepareExporter: (
|
||||
context: ExportPipelineContext<ExportState>
|
||||
): Promise<PreparedExporter> => {
|
||||
return findGDJS().then(({ gdjsRoot }) => {
|
||||
console.info('GDJS found in ', gdjsRoot);
|
||||
|
||||
const fileSystem = assignIn(
|
||||
new gd.AbstractFileSystemJS(),
|
||||
localFileSystem
|
||||
);
|
||||
const exporter = new gd.Exporter(fileSystem, gdjsRoot);
|
||||
const temporaryOutputDir = path.join(
|
||||
fileSystem.getTempDir(),
|
||||
'OnlineElectronExport'
|
||||
);
|
||||
fileSystem.mkDir(temporaryOutputDir);
|
||||
fileSystem.clearDir(temporaryOutputDir);
|
||||
|
||||
return {
|
||||
exporter,
|
||||
temporaryOutputDir,
|
||||
};
|
||||
});
|
||||
},
|
||||
|
||||
launchExport: (
|
||||
context: ExportPipelineContext<ExportState>,
|
||||
{ exporter, temporaryOutputDir }: PreparedExporter
|
||||
): Promise<ExportOutput> => {
|
||||
const exportOptions = new gd.MapStringBoolean();
|
||||
exportOptions.set('exportForElectron', true);
|
||||
exporter.exportWholePixiProject(
|
||||
context.project,
|
||||
temporaryOutputDir,
|
||||
exportOptions
|
||||
);
|
||||
exportOptions.delete();
|
||||
exporter.delete();
|
||||
|
||||
return Promise.resolve({ temporaryOutputDir });
|
||||
},
|
||||
|
||||
launchResourcesDownload: (
|
||||
context: ExportPipelineContext<ExportState>,
|
||||
{ temporaryOutputDir }: ExportOutput
|
||||
): Promise<ResourcesDownloadOutput> => {
|
||||
return Promise.resolve({ temporaryOutputDir });
|
||||
},
|
||||
|
||||
launchCompression: (
|
||||
context: ExportPipelineContext<ExportState>,
|
||||
{ temporaryOutputDir }: ResourcesDownloadOutput
|
||||
): Promise<CompressionOutput> => {
|
||||
const archiveOutputDir = os.tmpdir();
|
||||
return archiveLocalFolder({
|
||||
path: temporaryOutputDir,
|
||||
outputFilename: path.join(archiveOutputDir, 'game-archive.zip'),
|
||||
});
|
||||
},
|
||||
|
||||
launchUpload: (
|
||||
context: ExportPipelineContext<ExportState>,
|
||||
outputFile: CompressionOutput
|
||||
): Promise<string> => {
|
||||
if (!ipcRenderer) return Promise.reject('No support for upload');
|
||||
|
||||
ipcRenderer.removeAllListeners('s3-file-upload-progress');
|
||||
ipcRenderer.removeAllListeners('s3-file-upload-done');
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
ipcRenderer.on(
|
||||
's3-file-upload-progress',
|
||||
(event, stepCurrentProgress, stepMaxProgress) => {
|
||||
context.updateStepProgress(stepCurrentProgress, stepMaxProgress);
|
||||
}
|
||||
);
|
||||
ipcRenderer.on('s3-file-upload-done', (event, err, prefix) => {
|
||||
if (err) return reject(err);
|
||||
resolve(prefix);
|
||||
});
|
||||
ipcRenderer.send('s3-file-upload', outputFile);
|
||||
});
|
||||
},
|
||||
|
||||
launchOnlineBuild: (
|
||||
exportState: ExportState,
|
||||
userProfile: UserProfile,
|
||||
uploadBucketKey: string
|
||||
): Promise<Build> => {
|
||||
const { getAuthorizationHeader, profile } = userProfile;
|
||||
if (!profile) return Promise.reject(new Error('User is not authenticated'));
|
||||
|
||||
return buildElectron(
|
||||
getAuthorizationHeader,
|
||||
profile.uid,
|
||||
uploadBucketKey,
|
||||
exportState.targets
|
||||
);
|
||||
},
|
||||
};
|
@@ -1,354 +0,0 @@
|
||||
// @flow
|
||||
import { Trans } from '@lingui/macro';
|
||||
|
||||
import React, { Component } from 'react';
|
||||
import assignIn from 'lodash/assignIn';
|
||||
import RaisedButton from '../../../UI/RaisedButton';
|
||||
import Checkbox from '../../../UI/Checkbox';
|
||||
import { sendExportLaunched } from '../../../Utils/Analytics/EventSender';
|
||||
import {
|
||||
type Build,
|
||||
buildElectron,
|
||||
getUrl,
|
||||
} from '../../../Utils/GDevelopServices/Build';
|
||||
import { type UserProfile } from '../../../Profile/UserProfileContext';
|
||||
import { Column, Line } from '../../../UI/Grid';
|
||||
import { showErrorBox } from '../../../UI/Messages/MessageBox';
|
||||
import { findGDJS } from '../LocalGDJSFinder';
|
||||
import localFileSystem from '../LocalFileSystem';
|
||||
import { archiveLocalFolder } from '../../../Utils/LocalArchiver';
|
||||
import optionalRequire from '../../../Utils/OptionalRequire.js';
|
||||
import Window from '../../../Utils/Window';
|
||||
import CreateProfile from '../../../Profile/CreateProfile';
|
||||
import LimitDisplayer from '../../../Profile/LimitDisplayer';
|
||||
import {
|
||||
displayProjectErrorsBox,
|
||||
getErrors,
|
||||
} from '../../../ProjectManager/ProjectErrorsChecker';
|
||||
import { type Limit } from '../../../Utils/GDevelopServices/Usage';
|
||||
import { type TargetName } from '../../../Utils/GDevelopServices/Build';
|
||||
import BuildsWatcher from '../../Builds/BuildsWatcher';
|
||||
import BuildStepsProgress, {
|
||||
type BuildStep,
|
||||
} from '../../Builds/BuildStepsProgress';
|
||||
import Text from '../../../UI/Text';
|
||||
const path = optionalRequire('path');
|
||||
const os = optionalRequire('os');
|
||||
const electron = optionalRequire('electron');
|
||||
const ipcRenderer = electron ? electron.ipcRenderer : null;
|
||||
const gd = global.gd;
|
||||
|
||||
type State = {
|
||||
exportStep: BuildStep,
|
||||
build: ?Build,
|
||||
uploadProgress: number,
|
||||
uploadMax: number,
|
||||
errored: boolean,
|
||||
targets: Array<TargetName>,
|
||||
};
|
||||
|
||||
type Props = {
|
||||
project: gdProject,
|
||||
onChangeSubscription: Function,
|
||||
userProfile: UserProfile,
|
||||
};
|
||||
|
||||
class LocalOnlineElectronExport extends Component<Props, State> {
|
||||
state = {
|
||||
exportStep: '',
|
||||
build: null,
|
||||
uploadProgress: 0,
|
||||
uploadMax: 0,
|
||||
errored: false,
|
||||
targets: ['winExe'],
|
||||
};
|
||||
buildsWatcher = new BuildsWatcher();
|
||||
|
||||
static prepareExporter = (): Promise<any> => {
|
||||
return new Promise((resolve, reject) => {
|
||||
findGDJS(gdjsRoot => {
|
||||
if (!gdjsRoot) {
|
||||
showErrorBox('Could not find GDJS');
|
||||
return reject();
|
||||
}
|
||||
console.info('GDJS found in ', gdjsRoot);
|
||||
|
||||
const fileSystem = assignIn(
|
||||
new gd.AbstractFileSystemJS(),
|
||||
localFileSystem
|
||||
);
|
||||
const exporter = new gd.Exporter(fileSystem, gdjsRoot);
|
||||
const outputDir = path.join(
|
||||
fileSystem.getTempDir(),
|
||||
'OnlineElectronExport'
|
||||
);
|
||||
fileSystem.mkDir(outputDir);
|
||||
fileSystem.clearDir(outputDir);
|
||||
|
||||
resolve({
|
||||
exporter,
|
||||
outputDir,
|
||||
});
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
componentWillUnmount() {
|
||||
this.buildsWatcher.stop();
|
||||
}
|
||||
|
||||
launchExport = (): Promise<string> => {
|
||||
const t = str => str; //TODO
|
||||
const { project } = this.props;
|
||||
if (!project) return Promise.reject();
|
||||
|
||||
return LocalOnlineElectronExport.prepareExporter()
|
||||
.then(({ exporter, outputDir }) => {
|
||||
const exportOptions = new gd.MapStringBoolean();
|
||||
exportOptions.set('exportForElectron', true);
|
||||
exporter.exportWholePixiProject(project, outputDir, exportOptions);
|
||||
exportOptions.delete();
|
||||
exporter.delete();
|
||||
|
||||
return outputDir;
|
||||
})
|
||||
.catch(err => {
|
||||
showErrorBox(t('Unable to export the game'), err);
|
||||
throw err;
|
||||
});
|
||||
};
|
||||
|
||||
launchCompression = (outputDir: string): Promise<string> => {
|
||||
const archiveOutputDir = os.tmpdir();
|
||||
return archiveLocalFolder({
|
||||
path: outputDir,
|
||||
outputFilename: path.join(archiveOutputDir, 'game-archive.zip'),
|
||||
});
|
||||
};
|
||||
|
||||
launchUpload = (outputFile: string): Promise<string> => {
|
||||
if (!ipcRenderer) return Promise.reject('No support for upload');
|
||||
|
||||
ipcRenderer.removeAllListeners('s3-file-upload-progress');
|
||||
ipcRenderer.removeAllListeners('s3-file-upload-done');
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
ipcRenderer.on(
|
||||
's3-file-upload-progress',
|
||||
(event, uploadProgress, uploadMax) => {
|
||||
this.setState({
|
||||
uploadProgress,
|
||||
uploadMax,
|
||||
});
|
||||
}
|
||||
);
|
||||
ipcRenderer.on('s3-file-upload-done', (event, err, prefix) => {
|
||||
if (err) return reject(err);
|
||||
resolve(prefix);
|
||||
});
|
||||
ipcRenderer.send('s3-file-upload', outputFile);
|
||||
});
|
||||
};
|
||||
|
||||
launchBuild = (
|
||||
userProfile: UserProfile,
|
||||
uploadBucketKey: string
|
||||
): Promise<Build> => {
|
||||
const { getAuthorizationHeader, profile } = userProfile;
|
||||
if (!profile) return Promise.reject(new Error('User is not authenticated'));
|
||||
|
||||
return buildElectron(
|
||||
getAuthorizationHeader,
|
||||
profile.uid,
|
||||
uploadBucketKey,
|
||||
this.state.targets
|
||||
);
|
||||
};
|
||||
|
||||
startBuildWatch = (userProfile: UserProfile) => {
|
||||
if (!this.state.build) return;
|
||||
|
||||
this.buildsWatcher.start({
|
||||
userProfile,
|
||||
builds: [this.state.build],
|
||||
onBuildUpdated: (build: Build) => this.setState({ build }),
|
||||
});
|
||||
};
|
||||
|
||||
launchWholeExport = (userProfile: UserProfile) => {
|
||||
const t = str => str; //TODO;
|
||||
const { project } = this.props;
|
||||
sendExportLaunched('local-online-electron');
|
||||
|
||||
if (!displayProjectErrorsBox(t, getErrors(t, project))) return;
|
||||
|
||||
const handleError = (message: string) => err => {
|
||||
if (!this.state.errored) {
|
||||
this.setState({
|
||||
errored: true,
|
||||
});
|
||||
showErrorBox(message + (err.message ? `\n${err.message}` : ''), {
|
||||
exportStep: this.state.exportStep,
|
||||
rawError: err,
|
||||
});
|
||||
}
|
||||
|
||||
throw err;
|
||||
};
|
||||
|
||||
this.setState({
|
||||
exportStep: 'export',
|
||||
uploadProgress: 0,
|
||||
uploadMax: 0,
|
||||
errored: false,
|
||||
build: null,
|
||||
});
|
||||
this.launchExport()
|
||||
.then(outputDir => {
|
||||
this.setState({
|
||||
exportStep: 'compress',
|
||||
});
|
||||
return this.launchCompression(outputDir);
|
||||
}, handleError(t('Error while exporting the game.')))
|
||||
.then(outputFile => {
|
||||
this.setState({
|
||||
exportStep: 'upload',
|
||||
});
|
||||
return this.launchUpload(outputFile);
|
||||
}, handleError(t('Error while compressing the game.')))
|
||||
.then((uploadBucketKey: string) => {
|
||||
this.setState({
|
||||
exportStep: 'waiting-for-build',
|
||||
});
|
||||
return this.launchBuild(userProfile, uploadBucketKey);
|
||||
}, handleError(t('Error while uploading the game. Check your internet connection or try again later.')))
|
||||
.then(build => {
|
||||
this.setState(
|
||||
{
|
||||
build,
|
||||
exportStep: 'build',
|
||||
},
|
||||
() => {
|
||||
this.startBuildWatch(userProfile);
|
||||
}
|
||||
);
|
||||
}, handleError(t('Error while lauching the build of the game.')))
|
||||
.catch(() => {
|
||||
/* Error handled previously */
|
||||
});
|
||||
};
|
||||
|
||||
_download = (key: string) => {
|
||||
if (!this.state.build || !this.state.build[key]) return;
|
||||
|
||||
Window.openExternalURL(getUrl(this.state.build[key]));
|
||||
};
|
||||
|
||||
_setTarget = (targetName: TargetName, enable: boolean) => {
|
||||
if (enable && this.state.targets.indexOf(targetName) === -1) {
|
||||
this.setState({
|
||||
targets: [...this.state.targets, targetName],
|
||||
});
|
||||
} else if (!enable && this.state.targets.indexOf(targetName) !== -1) {
|
||||
this.setState({
|
||||
targets: this.state.targets.filter(name => name !== targetName),
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
render() {
|
||||
const {
|
||||
exportStep,
|
||||
build,
|
||||
uploadMax,
|
||||
uploadProgress,
|
||||
errored,
|
||||
} = this.state;
|
||||
const t = str => str; //TODO;
|
||||
const { project, userProfile } = this.props;
|
||||
if (!project) return null;
|
||||
|
||||
const getBuildLimit = (userProfile: UserProfile): ?Limit =>
|
||||
userProfile.limits ? userProfile.limits['electron-build'] : null;
|
||||
const canLaunchBuild = (userProfile: UserProfile) => {
|
||||
if (!errored && exportStep !== '' && exportStep !== 'build') return false;
|
||||
|
||||
const limit: ?Limit = getBuildLimit(userProfile);
|
||||
if (limit && limit.limitReached) return false;
|
||||
|
||||
if (!this.state.targets.length) return false;
|
||||
|
||||
return true;
|
||||
};
|
||||
|
||||
return (
|
||||
<Column noMargin>
|
||||
<Line>
|
||||
<Text>
|
||||
{t(
|
||||
'Your game will be exported and packaged online as an stand-alone game for Windows, Linux and/or macOS.'
|
||||
)}
|
||||
</Text>
|
||||
</Line>
|
||||
<Checkbox
|
||||
label={<Trans>Windows (zip file)</Trans>}
|
||||
checked={this.state.targets.indexOf('winZip') !== -1}
|
||||
onCheck={(e, checked) => this._setTarget('winZip', checked)}
|
||||
/>
|
||||
<Checkbox
|
||||
label={<Trans>Windows (auto-installer file)</Trans>}
|
||||
checked={this.state.targets.indexOf('winExe') !== -1}
|
||||
onCheck={(e, checked) => this._setTarget('winExe', checked)}
|
||||
/>
|
||||
<Checkbox
|
||||
label={<Trans>macOS (zip file)</Trans>}
|
||||
checked={this.state.targets.indexOf('macZip') !== -1}
|
||||
onCheck={(e, checked) => this._setTarget('macZip', checked)}
|
||||
/>
|
||||
<Checkbox
|
||||
label={<Trans>Linux (AppImage)</Trans>}
|
||||
checked={this.state.targets.indexOf('linuxAppImage') !== -1}
|
||||
onCheck={(e, checked) => this._setTarget('linuxAppImage', checked)}
|
||||
/>
|
||||
{userProfile.authenticated && (
|
||||
<Line justifyContent="center">
|
||||
<RaisedButton
|
||||
label={t('Export')}
|
||||
primary
|
||||
onClick={() => this.launchWholeExport(userProfile)}
|
||||
disabled={!canLaunchBuild(userProfile)}
|
||||
/>
|
||||
</Line>
|
||||
)}
|
||||
{userProfile.authenticated && (
|
||||
<LimitDisplayer
|
||||
subscription={userProfile.subscription}
|
||||
limit={getBuildLimit(userProfile)}
|
||||
onChangeSubscription={this.props.onChangeSubscription}
|
||||
/>
|
||||
)}
|
||||
{!userProfile.authenticated && (
|
||||
<CreateProfile
|
||||
message={t(
|
||||
'Create an account to build your game for Windows, Linux and macOS in one-click:'
|
||||
)}
|
||||
onLogin={userProfile.onLogin}
|
||||
/>
|
||||
)}
|
||||
<Line expand>
|
||||
<BuildStepsProgress
|
||||
exportStep={exportStep}
|
||||
build={build}
|
||||
onDownload={this._download}
|
||||
uploadMax={uploadMax}
|
||||
uploadProgress={uploadProgress}
|
||||
errored={errored}
|
||||
showSeeAllMyBuildsExplanation
|
||||
/>
|
||||
</Line>
|
||||
</Column>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default LocalOnlineElectronExport;
|