Compare commits

...

55 Commits

Author SHA1 Message Date
Florian Rival
5e3e0db9f3 Re-validate expression for errors after inserting a function from expression selector 2019-11-21 12:22:28 +00:00
Florian Rival
bfee61e9e8 Remove forgotten console.logs 2019-11-20 23:00:41 +00:00
Florian Rival
097dd779f6 Fix scrollbar position in InlinePopover 2019-11-20 22:57:19 +00:00
Florian Rival
52453b7c58 Allow scroll in InlinePopover if content is too large 2019-11-20 22:30:28 +00:00
Florian Rival
dd2ac68dab Fix color of ForEach/While/Repeat events header texts for the dark theme 2019-11-20 22:18:20 +00:00
Florian Rival
6ea50f8114 Fix InlinePopover hidden behind Mosaic windows 2019-11-20 22:17:32 +00:00
Florian Rival
ec05ec6ea1 Fix duplicate React keys when searching for expressions 2019-11-20 21:45:43 +00:00
Florian Rival
3e7c5394ce Fix search bar not working in ExpressionSelector
This is because the Modal was stealing the focus.
"Fix" the issue by disabling the portal, so that the expression selector is inside the modal. The selector can not go outside of the modal anymore though, but it's less of a problem.
2019-11-20 21:07:59 +00:00
Florian Rival
55aae059ff Fix shortcuts issues with KeyboardShortcuts
* Ensure keyboard events are restricted to the EventsSheet by listening only for key presses and other events from the EventsSheet container. No listener on the document with a "focused" boolean that can be not synchronized with the real element having the focus (happened in the previous implementation if you scroll too much in EventsSheet or delete the selection: the blur() method of KeyboardShortcuts would never be called).
  * Additional care must be done when an element is removed (which can happen after deletion of the selection or scroll) to put back the focus to the container div (see _ensureFocus). Otherwise we lose the focus so keyboard shortcuts don't work anymore until you click again on some elemnt (which would not be a huge bug by itself).
* Improve shortcuts for drag'n'drop by properly listening to modifier keys when something is dragged.
2019-11-20 19:52:16 +00:00
Florian Rival
61a20f1af8 Add version number with git hash in Sentry error reporting 2019-11-17 14:58:20 +00:00
Florian Rival
60170609e9 Fix internal example loading from url on web-app 2019-11-17 13:17:27 +00:00
Florian Rival
0c55b3b8d4 Fix VariableField button position, warning and BackgroundColorRow size 2019-11-17 13:01:31 +00:00
Florian Rival
6b16934184 Fix drag'n'drop of points/polygons with touch 2019-11-16 17:42:28 +00:00
Florian Rival
c15acdde83 Hide external editor button in DirectionTools on small screens 2019-11-16 16:40:01 +00:00
Florian Rival
209dafc269 Disable overscroll in browsers 2019-11-16 00:05:01 +00:00
Florian Rival
757c9502ef Mark scripts in exported game to be loaded with cross-origin enabled 2019-11-15 23:48:56 +00:00
Florian Rival
be8ab96c4c Rework tabs and mosaic window titlebars to use same font, not uppercased and same button size 2019-11-15 22:47:54 +00:00
Florian Rival
1600091249 Fix inconsistent font for "Instance Variables" in InstancePropertiesEditor 2019-11-15 22:47:04 +00:00
Florian Rival
45b6caa06e Fix optimal system font not always being used on Windows/Linux 2019-11-15 21:56:17 +00:00
Florian Rival
59754d1c27 Call make-version-metadata after install 2019-11-14 23:51:26 +00:00
Florian Rival
656881a11a Display alert if trying to export from web-app while offline 2019-11-14 23:51:26 +00:00
Florian Rival
5e91bdf811 Update BrowserResourceSources library 2019-11-14 23:51:26 +00:00
Florian Rival
e6d0f6ed4c Update title bar color according to the theme when installed as PWA 2019-11-14 23:51:26 +00:00
Florian Rival
864acd3988 Add error explanation dialog for BrowserS3PreviewLauncher 2019-11-14 23:51:26 +00:00
Florian Rival
630ece0f7e Add service-worker for base app and assets 2019-11-14 23:51:26 +00:00
Florian Rival
3b102a74b7 Update subscription details and add missing translation markers 2019-11-14 23:51:26 +00:00
Florian Rival
5096ecee24 Expose version metadata to web-app 2019-11-14 23:51:26 +00:00
Florian Rival
29f1d873a6 Always show export button in Toolbar 2019-11-14 23:51:26 +00:00
Florian Rival
088e3acae2 Add missing browser exporters 2019-11-14 23:51:26 +00:00
Florian Rival
0fbe7a62e8 Refactor common parts of exports into GenericExports 2019-11-14 23:51:26 +00:00
Florian Rival
9e4849f218 Add BrowserOnlineWebExport 2019-11-14 23:51:26 +00:00
Florian Rival
db536894a4 Add profile button in web-app ProjectManager and remove unecessary indent 2019-11-14 23:51:26 +00:00
Florian Rival
a63c44b5ca Refactor LocalOnlineCordovaExport and LocalOnlineElectronExport to use ExporterPipeline 2019-11-14 23:51:26 +00:00
Florian Rival
8756276793 Fix include files for events based behavior methods 2019-11-14 23:51:26 +00:00
Florian Rival
878f64d024 Refactor LocalFacebookInstantGamesExport to use ExporterPipeline 2019-11-14 23:51:26 +00:00
Florian Rival
bd09c58439 Refactor some exporters to use ExportPipeline 2019-11-14 23:51:26 +00:00
Florian Rival
c645d4a2d6 Add BrowserOnlineElectronExport 2019-11-14 23:51:26 +00:00
Florian Rival
45e03d61a0 Make ExportPipeline more flexible 2019-11-14 23:51:26 +00:00
Florian Rival
d924cd90bd Add BrowserOnlineCordovaExport
* Add zip.js as an external library + typings + BrowserArchiver.
* Add BrowserFileSystem and unit tests.
* Make a first step toward having a generic ExportPipeline.
* Add BrowserOnlineCordovaExport.
* Various refactorings
2019-11-14 23:51:26 +00:00
Florian Rival
455fe8193a Remove useless function in BrowserS3FileSystem 2019-11-14 23:51:26 +00:00
Florian Rival
1eb5113283 Remove useless CopyDir from AbstractFileSystem 2019-11-14 23:51:26 +00:00
Florian Rival
3687c24e71 Enable Google Drive opening/saving in production build 2019-11-12 20:10:37 +00:00
Bouh
8d75a8373d Add "clamp" function (#1283) 2019-11-10 13:08:10 +00:00
Florian Rival
3fed377c6c Fix Travis build (update libstdc++6 version) 2019-11-10 13:03:14 +00:00
Florian Rival
957ab3f53b Fix apple-touch-icon.png 2019-11-07 23:20:56 +00:00
Florian Rival
e7e338805b Update favion/home screen icons 2019-11-07 22:57:06 +00:00
Florian Rival
83d0c4cc65 Fix ResourceSelector not showing the proper file when choosing Cancel when asked if the resource must be copied in project directory 2019-11-06 21:46:57 +00:00
Florian Rival
13461429f2 Separate login and account creation in two different dialogs 2019-11-05 23:44:38 +00:00
Florian Rival
b0fd36daff Add registerModule and requireModule to ObjectsRenderingService, to bundle third party libraries 2019-11-03 22:40:31 +00:00
Florian Rival
2e84b2e648 Fix typo 2019-10-29 19:30:29 +01:00
Bouh
2b4d263410 Fix selection dismissed when panning + some icons and descriptions (#1269) 2019-10-29 18:28:22 +00:00
dependabot[bot]
64343d7043 Bump axios from 0.16.2 to 0.18.1 in /newIDE/app (#1272)
Bumps [axios](https://github.com/axios/axios) from 0.16.2 to 0.18.1.
- [Release notes](https://github.com/axios/axios/releases)
- [Changelog](https://github.com/axios/axios/blob/v0.18.1/CHANGELOG.md)
- [Commits](https://github.com/axios/axios/compare/v0.16.2...v0.18.1)

Signed-off-by: dependabot[bot] <support@github.com>
2019-10-28 23:00:22 +00:00
Florian Rival
3e9c4d594c Improve typing of PreviewLauncher 2019-10-28 22:43:31 +00:00
Florian Rival
657ca85324 Refactor findGDJS and add flow typings 2019-10-27 21:32:42 +00:00
Florian Rival
6ac9c0a0ee Avoid watching old builds that are still somehow in pending state 2019-10-27 16:49:52 +00:00
166 changed files with 7863 additions and 3562 deletions

View File

@@ -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:

View File

@@ -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."),

View File

@@ -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"),

View File

@@ -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_"),

View File

@@ -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.

View File

@@ -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;

View File

@@ -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.'
),

View File

@@ -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()

View File

@@ -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_"),

View File

@@ -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);
};

View File

@@ -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);
}

View File

@@ -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);

View File

@@ -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");

View File

@@ -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");

View File

@@ -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)

View File

@@ -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;
};

View File

@@ -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(
{

View File

@@ -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=

View File

@@ -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

View File

@@ -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
View 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,
|};

View File

@@ -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": {

View File

@@ -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",

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.8 KiB

BIN
newIDE/app/public/favicon-256.png Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.9 KiB

BIN
newIDE/app/public/favicon-512.png Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 361 KiB

After

Width:  |  Height:  |  Size: 3.4 KiB

File diff suppressed because one or more lines are too long

View File

@@ -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"
}
}

View File

Before

Width:  |  Height:  |  Size: 5.5 KiB

After

Width:  |  Height:  |  Size: 5.5 KiB

Binary file not shown.

Binary file not shown.

View 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
);
}
});
});

View 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());

View 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)
);

View File

@@ -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'
);
}

View File

@@ -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,

View File

@@ -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');

View File

@@ -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);
});

View File

@@ -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>

View File

@@ -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
*/

View File

@@ -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',

View File

@@ -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,
})}

View File

@@ -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,

View File

@@ -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}`;

View File

@@ -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

View File

@@ -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={

View File

@@ -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

View File

@@ -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

View File

@@ -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}
/>

View File

@@ -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}
/>
)}

View File

@@ -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}

View 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>
)}
/>
);
},
};

View 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>
)}
/>
);
},
};

View 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>
)}
/>
);
},
};

View File

@@ -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>
);
}
}

View File

@@ -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>
)}
/>
);
},
};

View 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]
);
};
}

View 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'
);
});
});
});

View 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>
)}
/>
);
},
};

View File

@@ -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
);
},
};

View File

@@ -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
);
},
};

View 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);
},
};

View File

@@ -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;
};
}

View File

@@ -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,
};
});
};

View File

@@ -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>
);
}
}

View File

@@ -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,

View File

@@ -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;
}
}

View File

@@ -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>
`;

View File

@@ -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 (&amp; 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 &amp; 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);
};

View File

@@ -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>
);
};

View File

@@ -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>
);

View File

@@ -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 = {

View File

@@ -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
);
}
}
});
}

View File

@@ -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() {

View File

@@ -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>
);

View 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>
);
}
}

View 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,
|};

View 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>
);

View 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>
);

View 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>
);

View File

@@ -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>
);
};

View 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>
);

View File

@@ -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>
);

View File

@@ -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>
);
};

View 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>
);

View File

@@ -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>
)}
/>
);
}
}
},
};

View File

@@ -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;
},
};

View File

@@ -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;
},
};

View File

@@ -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>
);
}
}

View File

@@ -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}
/>
)}
/>
);
},
};

View File

@@ -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>
);

View File

@@ -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;

View File

@@ -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);

View File

@@ -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);
});
});
});

View 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>}
/>
)}
/>
);
},
};

View 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
);
},
};

View File

@@ -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;

View File

@@ -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
);
},
};

View File

@@ -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;

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