mirror of
https://github.com/4ian/GDevelop.git
synced 2025-10-15 10:19:04 +00:00
Add a basic Asset Store for the web-app
This commit is contained in:

committed by
Florian Rival

parent
45d7c6188b
commit
8c6b9ef044
6
.vscode/settings.json
vendored
6
.vscode/settings.json
vendored
@@ -85,7 +85,11 @@
|
||||
"array": "cpp",
|
||||
"cinttypes": "cpp",
|
||||
"numeric": "cpp",
|
||||
"__memory": "cpp"
|
||||
"__memory": "cpp",
|
||||
"__errc": "cpp",
|
||||
"__node_handle": "cpp",
|
||||
"bit": "cpp",
|
||||
"optional": "cpp"
|
||||
},
|
||||
"files.exclude": {
|
||||
"Binaries/*build*": true,
|
||||
|
61
Core/GDCore/IDE/Project/ResourcesRenamer.h
Normal file
61
Core/GDCore/IDE/Project/ResourcesRenamer.h
Normal file
@@ -0,0 +1,61 @@
|
||||
/*
|
||||
* GDevelop Core
|
||||
* Copyright 2008-2016 Florian Rival (Florian.Rival@gmail.com). All rights
|
||||
* reserved. This project is released under the MIT License.
|
||||
*/
|
||||
|
||||
#if defined(GD_IDE_ONLY)
|
||||
#ifndef GDCORE_RESOURCESRENAMER_H
|
||||
#define GDCORE_RESOURCESRENAMER_H
|
||||
|
||||
#include <set>
|
||||
#include <vector>
|
||||
|
||||
#include "GDCore/IDE/Project/ArbitraryResourceWorker.h"
|
||||
#include "GDCore/String.h"
|
||||
|
||||
namespace gd {
|
||||
|
||||
/**
|
||||
* \brief Class used to rename resources (in an object, an entire project,
|
||||
* etc...)
|
||||
*
|
||||
* \ingroup IDE
|
||||
*/
|
||||
class ResourcesRenamer : public gd::ArbitraryResourceWorker {
|
||||
public:
|
||||
/**
|
||||
* @brief Constructor taking the map from old name to new name.
|
||||
* @param oldToNewNames_ A map associating to a resource name the new name to
|
||||
* use.
|
||||
*/
|
||||
ResourcesRenamer(const std::map<gd::String, gd::String>& oldToNewNames_)
|
||||
: gd::ArbitraryResourceWorker(), oldToNewNames(oldToNewNames_){};
|
||||
virtual ~ResourcesRenamer(){};
|
||||
|
||||
virtual void ExposeFile(gd::String& resourceName) override {
|
||||
RenameIfNeeded(resourceName);
|
||||
};
|
||||
virtual void ExposeImage(gd::String& imageResourceName) override {
|
||||
RenameIfNeeded(imageResourceName);
|
||||
};
|
||||
virtual void ExposeAudio(gd::String& audioResourceName) override {
|
||||
RenameIfNeeded(audioResourceName);
|
||||
};
|
||||
virtual void ExposeFont(gd::String& fontResourceName) override {
|
||||
RenameIfNeeded(fontResourceName);
|
||||
};
|
||||
|
||||
private:
|
||||
void RenameIfNeeded(gd::String& resourceName) {
|
||||
if (oldToNewNames.find(resourceName) != oldToNewNames.end())
|
||||
resourceName = oldToNewNames[resourceName];
|
||||
}
|
||||
|
||||
std::map<gd::String, gd::String> oldToNewNames;
|
||||
};
|
||||
|
||||
} // namespace gd
|
||||
|
||||
#endif // GDCORE_RESOURCESRENAMER_H
|
||||
#endif
|
33
Core/tests/ResourcesRenamer.cpp
Normal file
33
Core/tests/ResourcesRenamer.cpp
Normal file
@@ -0,0 +1,33 @@
|
||||
/*
|
||||
* GDevelop Core
|
||||
* Copyright 2008-2016 Florian Rival (Florian.Rival@gmail.com). All rights
|
||||
* reserved. This project is released under the MIT License.
|
||||
*/
|
||||
/**
|
||||
* @file Tests covering common features of GDevelop Core.
|
||||
*/
|
||||
#include "GDCore/IDE/Project/ResourcesRenamer.h"
|
||||
|
||||
#include "GDCore/CommonTools.h"
|
||||
#include "GDCore/Project/Project.h"
|
||||
#include "catch.hpp"
|
||||
|
||||
TEST_CASE("ResourcesRenamer", "[common]") {
|
||||
SECTION("It renames resources that are exposed") {
|
||||
std::map<gd::String, gd::String> renamings = {
|
||||
{"Resource1", "RenamedResource1"}};
|
||||
gd::ResourcesRenamer resourcesRenamer(renamings);
|
||||
|
||||
gd::Project project;
|
||||
project.GetPlatformSpecificAssets().Set(
|
||||
"android", "some-icon", "Resource1");
|
||||
project.GetPlatformSpecificAssets().Set(
|
||||
"android", "some-other-icon", "Resource2");
|
||||
|
||||
project.ExposeResources(resourcesRenamer);
|
||||
REQUIRE(project.GetPlatformSpecificAssets().Get("android", "some-icon") ==
|
||||
"RenamedResource1");
|
||||
REQUIRE(project.GetPlatformSpecificAssets().Get(
|
||||
"android", "some-other-icon") == "Resource2");
|
||||
}
|
||||
}
|
@@ -2002,6 +2002,12 @@ interface ResourcesMergingHelper {
|
||||
void SetBaseDirectory([Const] DOMString basePath);
|
||||
[Ref] MapStringString GetAllResourcesOldAndNewFilename();
|
||||
};
|
||||
ResourcesMergingHelper implements ArbitraryResourceWorker;
|
||||
|
||||
interface ResourcesRenamer {
|
||||
void ResourcesRenamer([Const, Ref] MapStringString oldToNewNames);
|
||||
};
|
||||
ResourcesRenamer implements ArbitraryResourceWorker;
|
||||
|
||||
interface ProjectResourcesCopier {
|
||||
boolean STATIC_CopyAllResourcesTo([Ref] Project project,
|
||||
@@ -2020,6 +2026,7 @@ interface ResourcesInUseHelper {
|
||||
[Ref] SetString GetAllFonts();
|
||||
[Ref] SetString GetAll([Const] DOMString resourceType);
|
||||
};
|
||||
ResourcesInUseHelper implements ArbitraryResourceWorker;
|
||||
|
||||
interface LayoutEditorCanvasOptions {
|
||||
void LayoutEditorCanvasOptions();
|
||||
|
@@ -37,6 +37,7 @@
|
||||
#include <GDCore/IDE/Project/ProjectResourcesCopier.h>
|
||||
#include <GDCore/IDE/Project/ResourcesInUseHelper.h>
|
||||
#include <GDCore/IDE/Project/ResourcesMergingHelper.h>
|
||||
#include <GDCore/IDE/Project/ResourcesRenamer.h>
|
||||
#include <GDCore/IDE/WholeProjectRefactorer.h>
|
||||
#include <GDCore/Project/Behavior.h>
|
||||
#include <GDCore/Project/Effect.h>
|
||||
|
@@ -248,12 +248,6 @@ type ParticleEmitterObject_RendererType = 0 | 1 | 2`
|
||||
'declare class gdHighestZOrderFinder extends gdInitialInstanceFunctor {',
|
||||
'types/gdhighestzorderfinder.js'
|
||||
);
|
||||
shell.sed(
|
||||
'-i',
|
||||
'declare class gdResourcesInUseHelper {',
|
||||
'declare class gdResourcesInUseHelper extends gdArbitraryResourceWorker {',
|
||||
'types/gdresourcesinusehelper.js'
|
||||
);
|
||||
|
||||
// Rename classes from GDJS:
|
||||
shell.sed(
|
||||
|
@@ -1,5 +1,5 @@
|
||||
// Automatically generated by GDevelop.js/scripts/generate-types.js
|
||||
declare class gdResourcesMergingHelper {
|
||||
declare class gdResourcesMergingHelper extends gdArbitraryResourceWorker {
|
||||
constructor(fs: gdAbstractFileSystem): void;
|
||||
setBaseDirectory(basePath: string): void;
|
||||
getAllResourcesOldAndNewFilename(): gdMapStringString;
|
||||
|
6
GDevelop.js/types/gdresourcesrenamer.js
Normal file
6
GDevelop.js/types/gdresourcesrenamer.js
Normal file
@@ -0,0 +1,6 @@
|
||||
// Automatically generated by GDevelop.js/scripts/generate-types.js
|
||||
declare class gdResourcesRenamer extends gdArbitraryResourceWorker {
|
||||
constructor(oldToNewNames: gdMapStringString): void;
|
||||
delete(): void;
|
||||
ptr: number;
|
||||
};
|
@@ -160,6 +160,7 @@ declare class libGDevelop {
|
||||
ArbitraryResourceWorker: Class<gdArbitraryResourceWorker>;
|
||||
ArbitraryResourceWorkerJS: Class<gdArbitraryResourceWorkerJS>;
|
||||
ResourcesMergingHelper: Class<gdResourcesMergingHelper>;
|
||||
ResourcesRenamer: Class<gdResourcesRenamer>;
|
||||
ProjectResourcesCopier: Class<gdProjectResourcesCopier>;
|
||||
ResourcesInUseHelper: Class<gdResourcesInUseHelper>;
|
||||
LayoutEditorCanvasOptions: Class<gdLayoutEditorCanvasOptions>;
|
||||
|
459
newIDE/app/flow-typed/npm/jest_v19.x.x.js
vendored
459
newIDE/app/flow-typed/npm/jest_v19.x.x.js
vendored
@@ -1,459 +0,0 @@
|
||||
// flow-typed signature: 2dd68ce1a3d8938f974e88d46cbf55d1
|
||||
// flow-typed version: c7c67b81c1/jest_v19.x.x/flow_>=v0.16.x
|
||||
|
||||
type JestMockFn = {
|
||||
(...args: Array<any>): any,
|
||||
/**
|
||||
* An object for introspecting mock calls
|
||||
*/
|
||||
mock: {
|
||||
/**
|
||||
* An array that represents all calls that have been made into this mock
|
||||
* function. Each call is represented by an array of arguments that were
|
||||
* passed during the call.
|
||||
*/
|
||||
calls: Array<Array<any>>,
|
||||
/**
|
||||
* An array that contains all the object instances that have been
|
||||
* instantiated from this mock function.
|
||||
*/
|
||||
instances: mixed,
|
||||
},
|
||||
/**
|
||||
* Resets all information stored in the mockFn.mock.calls and
|
||||
* mockFn.mock.instances arrays. Often this is useful when you want to clean
|
||||
* up a mock's usage data between two assertions.
|
||||
*/
|
||||
mockClear(): Function,
|
||||
/**
|
||||
* Resets all information stored in the mock. This is useful when you want to
|
||||
* completely restore a mock back to its initial state.
|
||||
*/
|
||||
mockReset(): Function,
|
||||
/**
|
||||
* Accepts a function that should be used as the implementation of the mock.
|
||||
* The mock itself will still record all calls that go into and instances
|
||||
* that come from itself -- the only difference is that the implementation
|
||||
* will also be executed when the mock is called.
|
||||
*/
|
||||
mockImplementation(fn: Function): JestMockFn,
|
||||
/**
|
||||
* Accepts a function that will be used as an implementation of the mock for
|
||||
* one call to the mocked function. Can be chained so that multiple function
|
||||
* calls produce different results.
|
||||
*/
|
||||
mockImplementationOnce(fn: Function): JestMockFn,
|
||||
/**
|
||||
* Just a simple sugar function for returning `this`
|
||||
*/
|
||||
mockReturnThis(): void,
|
||||
/**
|
||||
* Deprecated: use jest.fn(() => value) instead
|
||||
*/
|
||||
mockReturnValue(value: any): JestMockFn,
|
||||
/**
|
||||
* Sugar for only returning a value once inside your mock
|
||||
*/
|
||||
mockReturnValueOnce(value: any): JestMockFn,
|
||||
}
|
||||
|
||||
type JestAsymmetricEqualityType = {
|
||||
/**
|
||||
* A custom Jasmine equality tester
|
||||
*/
|
||||
asymmetricMatch(value: mixed): boolean,
|
||||
}
|
||||
|
||||
type JestCallsType = {
|
||||
allArgs(): mixed,
|
||||
all(): mixed,
|
||||
any(): boolean,
|
||||
count(): number,
|
||||
first(): mixed,
|
||||
mostRecent(): mixed,
|
||||
reset(): void,
|
||||
}
|
||||
|
||||
type JestClockType = {
|
||||
install(): void,
|
||||
mockDate(date: Date): void,
|
||||
tick(milliseconds?:number): void,
|
||||
uninstall(): void,
|
||||
}
|
||||
|
||||
type JestMatcherResult = {
|
||||
message?: string | ()=>string,
|
||||
pass: boolean,
|
||||
}
|
||||
|
||||
type JestMatcher = (actual: any, expected: any) => JestMatcherResult;
|
||||
|
||||
type JestExpectType = {
|
||||
not: JestExpectType,
|
||||
/**
|
||||
* If you have a mock function, you can use .lastCalledWith to test what
|
||||
* arguments it was last called with.
|
||||
*/
|
||||
lastCalledWith(...args: Array<any>): void,
|
||||
/**
|
||||
* toBe just checks that a value is what you expect. It uses === to check
|
||||
* strict equality.
|
||||
*/
|
||||
toBe(value: any): void,
|
||||
/**
|
||||
* Use .toHaveBeenCalled to ensure that a mock function got called.
|
||||
*/
|
||||
toBeCalled(): void,
|
||||
/**
|
||||
* Use .toBeCalledWith to ensure that a mock function was called with
|
||||
* specific arguments.
|
||||
*/
|
||||
toBeCalledWith(...args: Array<any>): void,
|
||||
/**
|
||||
* Using exact equality with floating point numbers is a bad idea. Rounding
|
||||
* means that intuitive things fail.
|
||||
*/
|
||||
toBeCloseTo(num: number, delta: any): void,
|
||||
/**
|
||||
* Use .toBeDefined to check that a variable is not undefined.
|
||||
*/
|
||||
toBeDefined(): void,
|
||||
/**
|
||||
* Use .toBeFalsy when you don't care what a value is, you just want to
|
||||
* ensure a value is false in a boolean context.
|
||||
*/
|
||||
toBeFalsy(): void,
|
||||
/**
|
||||
* To compare floating point numbers, you can use toBeGreaterThan.
|
||||
*/
|
||||
toBeGreaterThan(number: number): void,
|
||||
/**
|
||||
* To compare floating point numbers, you can use toBeGreaterThanOrEqual.
|
||||
*/
|
||||
toBeGreaterThanOrEqual(number: number): void,
|
||||
/**
|
||||
* To compare floating point numbers, you can use toBeLessThan.
|
||||
*/
|
||||
toBeLessThan(number: number): void,
|
||||
/**
|
||||
* To compare floating point numbers, you can use toBeLessThanOrEqual.
|
||||
*/
|
||||
toBeLessThanOrEqual(number: number): void,
|
||||
/**
|
||||
* Use .toBeInstanceOf(Class) to check that an object is an instance of a
|
||||
* class.
|
||||
*/
|
||||
toBeInstanceOf(cls: Class<*>): void,
|
||||
/**
|
||||
* .toBeNull() is the same as .toBe(null) but the error messages are a bit
|
||||
* nicer.
|
||||
*/
|
||||
toBeNull(): void,
|
||||
/**
|
||||
* Use .toBeTruthy when you don't care what a value is, you just want to
|
||||
* ensure a value is true in a boolean context.
|
||||
*/
|
||||
toBeTruthy(): void,
|
||||
/**
|
||||
* Use .toBeUndefined to check that a variable is undefined.
|
||||
*/
|
||||
toBeUndefined(): void,
|
||||
/**
|
||||
* Use .toContain when you want to check that an item is in a list. For
|
||||
* testing the items in the list, this uses ===, a strict equality check.
|
||||
*/
|
||||
toContain(item: any): void,
|
||||
/**
|
||||
* Use .toContainEqual when you want to check that an item is in a list. For
|
||||
* testing the items in the list, this matcher recursively checks the
|
||||
* equality of all fields, rather than checking for object identity.
|
||||
*/
|
||||
toContainEqual(item: any): void,
|
||||
/**
|
||||
* Use .toEqual when you want to check that two objects have the same value.
|
||||
* This matcher recursively checks the equality of all fields, rather than
|
||||
* checking for object identity.
|
||||
*/
|
||||
toEqual(value: any): void,
|
||||
/**
|
||||
* Use .toHaveBeenCalled to ensure that a mock function got called.
|
||||
*/
|
||||
toHaveBeenCalled(): void,
|
||||
/**
|
||||
* Use .toHaveBeenCalledTimes to ensure that a mock function got called exact
|
||||
* number of times.
|
||||
*/
|
||||
toHaveBeenCalledTimes(number: number): void,
|
||||
/**
|
||||
* Use .toHaveBeenCalledWith to ensure that a mock function was called with
|
||||
* specific arguments.
|
||||
*/
|
||||
toHaveBeenCalledWith(...args: Array<any>): void,
|
||||
/**
|
||||
* If you have a mock function, you can use .toHaveBeenLastCalledWith to test what
|
||||
* arguments it was last called with.
|
||||
*/
|
||||
toHaveBeenLastCalledWith(...args: Array<any>): void,
|
||||
/**
|
||||
* Check that an object has a .length property and it is set to a certain
|
||||
* numeric value.
|
||||
*/
|
||||
toHaveLength(number: number): void,
|
||||
/**
|
||||
*
|
||||
*/
|
||||
toHaveProperty(propPath: string, value?: any): void,
|
||||
/**
|
||||
* Use .toMatch to check that a string matches a regular expression.
|
||||
*/
|
||||
toMatch(regexp: RegExp): void,
|
||||
/**
|
||||
* Use .toMatchObject to check that a javascript object matches a subset of the properties of an object.
|
||||
*/
|
||||
toMatchObject(object: Object): void,
|
||||
/**
|
||||
* This ensures that a React component matches the most recent snapshot.
|
||||
*/
|
||||
toMatchSnapshot(name?: string): void,
|
||||
/**
|
||||
* Use .toThrow to test that a function throws when it is called.
|
||||
*/
|
||||
toThrow(message?: string | Error): void,
|
||||
/**
|
||||
* Use .toThrowError to test that a function throws a specific error when it
|
||||
* is called. The argument can be a string for the error message, a class for
|
||||
* the error, or a regex that should match the error.
|
||||
*/
|
||||
toThrowError(message?: string | Error | RegExp): void,
|
||||
/**
|
||||
* Use .toThrowErrorMatchingSnapshot to test that a function throws a error
|
||||
* matching the most recent snapshot when it is called.
|
||||
*/
|
||||
toThrowErrorMatchingSnapshot(): void,
|
||||
}
|
||||
|
||||
type JestObjectType = {
|
||||
/**
|
||||
* Disables automatic mocking in the module loader.
|
||||
*
|
||||
* After this method is called, all `require()`s will return the real
|
||||
* versions of each module (rather than a mocked version).
|
||||
*/
|
||||
disableAutomock(): JestObjectType,
|
||||
/**
|
||||
* An un-hoisted version of disableAutomock
|
||||
*/
|
||||
autoMockOff(): JestObjectType,
|
||||
/**
|
||||
* Enables automatic mocking in the module loader.
|
||||
*/
|
||||
enableAutomock(): JestObjectType,
|
||||
/**
|
||||
* An un-hoisted version of enableAutomock
|
||||
*/
|
||||
autoMockOn(): JestObjectType,
|
||||
/**
|
||||
* Clears the mock.calls and mock.instances properties of all mocks.
|
||||
* Equivalent to calling .mockClear() on every mocked function.
|
||||
*/
|
||||
clearAllMocks(): JestObjectType,
|
||||
/**
|
||||
* Resets the state of all mocks. Equivalent to calling .mockReset() on every
|
||||
* mocked function.
|
||||
*/
|
||||
resetAllMocks(): JestObjectType,
|
||||
/**
|
||||
* Removes any pending timers from the timer system.
|
||||
*/
|
||||
clearAllTimers(): void,
|
||||
/**
|
||||
* The same as `mock` but not moved to the top of the expectation by
|
||||
* babel-jest.
|
||||
*/
|
||||
doMock(moduleName: string, moduleFactory?: any): JestObjectType,
|
||||
/**
|
||||
* The same as `unmock` but not moved to the top of the expectation by
|
||||
* babel-jest.
|
||||
*/
|
||||
dontMock(moduleName: string): JestObjectType,
|
||||
/**
|
||||
* Returns a new, unused mock function. Optionally takes a mock
|
||||
* implementation.
|
||||
*/
|
||||
fn(implementation?: Function): JestMockFn,
|
||||
/**
|
||||
* Determines if the given function is a mocked function.
|
||||
*/
|
||||
isMockFunction(fn: Function): boolean,
|
||||
/**
|
||||
* Given the name of a module, use the automatic mocking system to generate a
|
||||
* mocked version of the module for you.
|
||||
*/
|
||||
genMockFromModule(moduleName: string): any,
|
||||
/**
|
||||
* Mocks a module with an auto-mocked version when it is being required.
|
||||
*
|
||||
* The second argument can be used to specify an explicit module factory that
|
||||
* is being run instead of using Jest's automocking feature.
|
||||
*
|
||||
* The third argument can be used to create virtual mocks -- mocks of modules
|
||||
* that don't exist anywhere in the system.
|
||||
*/
|
||||
mock(moduleName: string, moduleFactory?: any, options?: Object): JestObjectType,
|
||||
/**
|
||||
* Resets the module registry - the cache of all required modules. This is
|
||||
* useful to isolate modules where local state might conflict between tests.
|
||||
*/
|
||||
resetModules(): JestObjectType,
|
||||
/**
|
||||
* Exhausts the micro-task queue (usually interfaced in node via
|
||||
* process.nextTick).
|
||||
*/
|
||||
runAllTicks(): void,
|
||||
/**
|
||||
* Exhausts the macro-task queue (i.e., all tasks queued by setTimeout(),
|
||||
* setInterval(), and setImmediate()).
|
||||
*/
|
||||
runAllTimers(): void,
|
||||
/**
|
||||
* Exhausts all tasks queued by setImmediate().
|
||||
*/
|
||||
runAllImmediates(): void,
|
||||
/**
|
||||
* Executes only the macro task queue (i.e. all tasks queued by setTimeout()
|
||||
* or setInterval() and setImmediate()).
|
||||
*/
|
||||
runTimersToTime(msToRun: number): void,
|
||||
/**
|
||||
* Executes only the macro-tasks that are currently pending (i.e., only the
|
||||
* tasks that have been queued by setTimeout() or setInterval() up to this
|
||||
* point)
|
||||
*/
|
||||
runOnlyPendingTimers(): void,
|
||||
/**
|
||||
* Explicitly supplies the mock object that the module system should return
|
||||
* for the specified module. Note: It is recommended to use jest.mock()
|
||||
* instead.
|
||||
*/
|
||||
setMock(moduleName: string, moduleExports: any): JestObjectType,
|
||||
/**
|
||||
* Indicates that the module system should never return a mocked version of
|
||||
* the specified module from require() (e.g. that it should always return the
|
||||
* real module).
|
||||
*/
|
||||
unmock(moduleName: string): JestObjectType,
|
||||
/**
|
||||
* Instructs Jest to use fake versions of the standard timer functions
|
||||
* (setTimeout, setInterval, clearTimeout, clearInterval, nextTick,
|
||||
* setImmediate and clearImmediate).
|
||||
*/
|
||||
useFakeTimers(): JestObjectType,
|
||||
/**
|
||||
* Instructs Jest to use the real versions of the standard timer functions.
|
||||
*/
|
||||
useRealTimers(): JestObjectType,
|
||||
/**
|
||||
* Creates a mock function similar to jest.fn but also tracks calls to
|
||||
* object[methodName].
|
||||
*/
|
||||
spyOn(object: Object, methodName: string): JestMockFn,
|
||||
}
|
||||
|
||||
type JestSpyType = {
|
||||
calls: JestCallsType,
|
||||
}
|
||||
|
||||
/** Runs this function after every test inside this context */
|
||||
declare function afterEach(fn: Function): void;
|
||||
/** Runs this function before every test inside this context */
|
||||
declare function beforeEach(fn: Function): void;
|
||||
/** Runs this function after all tests have finished inside this context */
|
||||
declare function afterAll(fn: Function): void;
|
||||
/** Runs this function before any tests have started inside this context */
|
||||
declare function beforeAll(fn: Function): void;
|
||||
/** A context for grouping tests together */
|
||||
declare function describe(name: string, fn: Function): void;
|
||||
|
||||
/** An individual test unit */
|
||||
declare var it: {
|
||||
/**
|
||||
* An individual test unit
|
||||
*
|
||||
* @param {string} Name of Test
|
||||
* @param {Function} Test
|
||||
*/
|
||||
(name: string, fn?: Function): ?Promise<void>,
|
||||
/**
|
||||
* Only run this test
|
||||
*
|
||||
* @param {string} Name of Test
|
||||
* @param {Function} Test
|
||||
*/
|
||||
only(name: string, fn?: Function): ?Promise<void>,
|
||||
/**
|
||||
* Skip running this test
|
||||
*
|
||||
* @param {string} Name of Test
|
||||
* @param {Function} Test
|
||||
*/
|
||||
skip(name: string, fn?: Function): ?Promise<void>,
|
||||
/**
|
||||
* Run the test concurrently
|
||||
*
|
||||
* @param {string} Name of Test
|
||||
* @param {Function} Test
|
||||
*/
|
||||
concurrent(name: string, fn?: Function): ?Promise<void>,
|
||||
};
|
||||
declare function fit(name: string, fn: Function): ?Promise<void>;
|
||||
/** An individual test unit */
|
||||
declare var test: typeof it;
|
||||
/** A disabled group of tests */
|
||||
declare var xdescribe: typeof describe;
|
||||
/** A focused group of tests */
|
||||
declare var fdescribe: typeof describe;
|
||||
/** A disabled individual test */
|
||||
declare var xit: typeof it;
|
||||
/** A disabled individual test */
|
||||
declare var xtest: typeof it;
|
||||
|
||||
/** The expect function is used every time you want to test a value */
|
||||
declare var expect: {
|
||||
/** The object that you want to make assertions against */
|
||||
(value: any): JestExpectType,
|
||||
/** Add additional Jasmine matchers to Jest's roster */
|
||||
extend(matchers: {[name:string]: JestMatcher}): void,
|
||||
/** Add a module that formats application-specific data structures. */
|
||||
addSnapshotSerializer(serializer: (input: Object) => string): void,
|
||||
assertions(expectedAssertions: number): void,
|
||||
any(value: mixed): JestAsymmetricEqualityType,
|
||||
anything(): void,
|
||||
arrayContaining(value: Array<mixed>): void,
|
||||
objectContaining(value: Object): void,
|
||||
/** Matches any received string that contains the exact expected string. */
|
||||
stringContaining(value: string): void,
|
||||
stringMatching(value: string | RegExp): void,
|
||||
};
|
||||
|
||||
// TODO handle return type
|
||||
// http://jasmine.github.io/2.4/introduction.html#section-Spies
|
||||
declare function spyOn(value: mixed, method: string): Object;
|
||||
|
||||
/** Holds all functions related to manipulating test runner */
|
||||
declare var jest: JestObjectType
|
||||
|
||||
/**
|
||||
* The global Jasmine object, this is generally not exposed as the public API,
|
||||
* using features inside here could break in later versions of Jest.
|
||||
*/
|
||||
declare var jasmine: {
|
||||
DEFAULT_TIMEOUT_INTERVAL: number,
|
||||
any(value: mixed): JestAsymmetricEqualityType,
|
||||
anything(): void,
|
||||
arrayContaining(value: Array<mixed>): void,
|
||||
clock(): JestClockType,
|
||||
createSpy(name: string): JestSpyType,
|
||||
createSpyObj(baseName: string, methodNames: Array<string>): {[methodName: string]: JestSpyType},
|
||||
objectContaining(value: Object): void,
|
||||
stringMatching(value: string): void,
|
||||
}
|
1206
newIDE/app/flow-typed/npm/jest_v24.x.x.js
vendored
Normal file
1206
newIDE/app/flow-typed/npm/jest_v24.x.x.js
vendored
Normal file
File diff suppressed because it is too large
Load Diff
21
newIDE/app/package-lock.json
generated
21
newIDE/app/package-lock.json
generated
@@ -15054,6 +15054,27 @@
|
||||
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
|
||||
"integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="
|
||||
},
|
||||
"js-worker-search": {
|
||||
"version": "1.4.1",
|
||||
"resolved": "https://registry.npmjs.org/js-worker-search/-/js-worker-search-1.4.1.tgz",
|
||||
"integrity": "sha512-5KCio6angmP+AXX8X2pPtjw1h3BCrmEmwGv2MdkXhl3+7g/2NTHOMhUh8bZqacp8R9LjhTT2bGr+f7RdB8KFZw==",
|
||||
"requires": {
|
||||
"flow-bin": "^0.50.0",
|
||||
"uuid": "^2.0.1"
|
||||
},
|
||||
"dependencies": {
|
||||
"flow-bin": {
|
||||
"version": "0.50.0",
|
||||
"resolved": "https://registry.npmjs.org/flow-bin/-/flow-bin-0.50.0.tgz",
|
||||
"integrity": "sha1-1M2yQw3uGjWZ8Otv5VEUbjAnJWo="
|
||||
},
|
||||
"uuid": {
|
||||
"version": "2.0.3",
|
||||
"resolved": "https://registry.npmjs.org/uuid/-/uuid-2.0.3.tgz",
|
||||
"integrity": "sha1-Z+LoY3lyFVMN/zGOW/nc6/1Hsho="
|
||||
}
|
||||
}
|
||||
},
|
||||
"js-yaml": {
|
||||
"version": "3.14.0",
|
||||
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.0.tgz",
|
||||
|
@@ -43,6 +43,7 @@
|
||||
"element-closest": "2.0.2",
|
||||
"firebase": "^6.1.0",
|
||||
"fontfaceobserver": "2.0.13",
|
||||
"js-worker-search": "^1.4.1",
|
||||
"jss-rtl": "^0.3.0",
|
||||
"keen-tracking": "1.1.3",
|
||||
"lodash": "4.17.4",
|
||||
|
@@ -3,7 +3,7 @@
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no, viewport-fit=cover" />
|
||||
<meta http-equiv="Content-Security-Policy" content="script-src 'self' 'unsafe-eval' 'unsafe-inline' https://api.gdevelop-app.com https://cdn.jsdelivr.net/npm/docsearch.js@2/dist/cdn/docsearch.min.js https://api.keen.io https://apis.google.com">
|
||||
<meta http-equiv="Content-Security-Policy" content="script-src 'self' 'unsafe-eval' 'unsafe-inline' blob: https://api.gdevelop-app.com https://cdn.jsdelivr.net/npm/docsearch.js@2/dist/cdn/docsearch.min.js https://api.keen.io https://apis.google.com">
|
||||
<link rel="apple-touch-icon" href="%PUBLIC_URL%/apple-touch-icon.png">
|
||||
<link rel="shortcut icon" href="%PUBLIC_URL%/favicon-256.png">
|
||||
<title>GDevelop 5</title>
|
||||
|
79
newIDE/app/src/AssetStore/AssetCard.js
Normal file
79
newIDE/app/src/AssetStore/AssetCard.js
Normal file
@@ -0,0 +1,79 @@
|
||||
// @flow
|
||||
import * as React from 'react';
|
||||
import { type AssetShortHeader } from '../Utils/GDevelopServices/Asset';
|
||||
import ButtonBase from '@material-ui/core/ButtonBase';
|
||||
import Text from '../UI/Text';
|
||||
|
||||
const paddingSize = 10;
|
||||
const styles = {
|
||||
previewBackground: {
|
||||
background: 'url("res/transparentback.png") repeat',
|
||||
display: 'flex',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
},
|
||||
previewImage: {
|
||||
objectFit: 'contain',
|
||||
verticalAlign: 'middle',
|
||||
pointerEvents: 'none',
|
||||
},
|
||||
icon: {
|
||||
color: '#fff',
|
||||
},
|
||||
cardContainer: {
|
||||
overflow: 'hidden',
|
||||
position: 'relative',
|
||||
},
|
||||
titleContainer: {
|
||||
position: 'absolute',
|
||||
bottom: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
height: 48,
|
||||
color: '#fff',
|
||||
whiteSpace: 'nowrap',
|
||||
overflow: 'hidden',
|
||||
textOverflow: 'ellipsis',
|
||||
backgroundColor: 'rgb(0,0,0,0.5)',
|
||||
},
|
||||
title: {
|
||||
whiteSpace: 'nowrap',
|
||||
overflow: 'hidden',
|
||||
textOverflow: 'ellipsis',
|
||||
},
|
||||
};
|
||||
|
||||
type Props = {|
|
||||
size: number,
|
||||
assetShortHeader: AssetShortHeader,
|
||||
onOpenDetails: () => void,
|
||||
|};
|
||||
|
||||
export const AssetCard = ({ assetShortHeader, onOpenDetails, size }: Props) => {
|
||||
return (
|
||||
<ButtonBase onClick={onOpenDetails} focusRipple>
|
||||
<div style={{ ...styles.cardContainer, width: size, height: size }}>
|
||||
<div style={{ ...styles.previewBackground, width: size, height: size }}>
|
||||
<img
|
||||
key={assetShortHeader.previewImageUrls[0]}
|
||||
style={{
|
||||
...styles.previewImage,
|
||||
maxWidth: 128 - 2 * paddingSize,
|
||||
maxHeight: 128 - 2 * paddingSize,
|
||||
}}
|
||||
src={assetShortHeader.previewImageUrls[0]}
|
||||
alt={assetShortHeader.name}
|
||||
/>
|
||||
</div>
|
||||
<div style={styles.titleContainer}>
|
||||
<Text noMargin style={styles.title}>
|
||||
{assetShortHeader.name}
|
||||
</Text>
|
||||
<Text noMargin style={styles.title} size="body2">
|
||||
{assetShortHeader.shortDescription}
|
||||
</Text>
|
||||
</div>
|
||||
</div>
|
||||
</ButtonBase>
|
||||
);
|
||||
};
|
205
newIDE/app/src/AssetStore/AssetDetails.js
Normal file
205
newIDE/app/src/AssetStore/AssetDetails.js
Normal file
@@ -0,0 +1,205 @@
|
||||
// @flow
|
||||
import { Trans } from '@lingui/macro';
|
||||
import * as React from 'react';
|
||||
import { Column } from '../UI/Grid';
|
||||
import Text from '../UI/Text';
|
||||
import Chip from '@material-ui/core/Chip';
|
||||
import RaisedButton from '../UI/RaisedButton';
|
||||
import {
|
||||
type AssetShortHeader,
|
||||
type Asset,
|
||||
getAsset,
|
||||
} from '../Utils/GDevelopServices/Asset';
|
||||
import LeftLoader from '../UI/LeftLoader';
|
||||
import PlaceholderLoader from '../UI/PlaceholderLoader';
|
||||
import PlaceholderError from '../UI/PlaceholderError';
|
||||
import CustomizationFields from './CustomizationFields';
|
||||
import {
|
||||
type ResourceSource,
|
||||
type ChooseResourceFunction,
|
||||
} from '../ResourcesList/ResourceSource.flow';
|
||||
import { type ResourceExternalEditor } from '../ResourcesList/ResourceExternalEditor.flow';
|
||||
import Add from '@material-ui/icons/Add';
|
||||
import { ResponsiveLineStackLayout } from '../UI/Layout';
|
||||
import { ResponsiveWindowMeasurer } from '../UI/Reponsive/ResponsiveWindowMeasurer';
|
||||
import Dialog from '../UI/Dialog';
|
||||
import FlatButton from '../UI/FlatButton';
|
||||
|
||||
const styles = {
|
||||
previewBackground: {
|
||||
background: 'url("res/transparentback.png") repeat',
|
||||
display: 'flex',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
padding: 10,
|
||||
},
|
||||
verticalPreviewBackground: {
|
||||
width: 250,
|
||||
},
|
||||
horizontalPreviewBackground: {
|
||||
height: 170,
|
||||
},
|
||||
chip: {
|
||||
marginBottom: 2,
|
||||
marginRight: 2,
|
||||
},
|
||||
inlineText: {
|
||||
display: 'inline-block',
|
||||
},
|
||||
previewImage: {
|
||||
maxWidth: '100%',
|
||||
maxHeight: '100%',
|
||||
verticalAlign: 'middle',
|
||||
pointerEvents: 'none',
|
||||
objectFit: 'contain',
|
||||
},
|
||||
};
|
||||
|
||||
type Props = {|
|
||||
project: gdProject,
|
||||
layout: ?gdLayout,
|
||||
objectsContainer: gdObjectsContainer,
|
||||
resourceSources: Array<ResourceSource>,
|
||||
onChooseResource: ChooseResourceFunction,
|
||||
resourceExternalEditors: Array<ResourceExternalEditor>,
|
||||
|
||||
assetShortHeader: AssetShortHeader,
|
||||
onAdd: () => void,
|
||||
onClose: () => void,
|
||||
canInstall: boolean,
|
||||
isBeingInstalled: boolean,
|
||||
|};
|
||||
|
||||
export const AssetDetails = ({
|
||||
project,
|
||||
layout,
|
||||
objectsContainer,
|
||||
resourceSources,
|
||||
onChooseResource,
|
||||
resourceExternalEditors,
|
||||
assetShortHeader,
|
||||
onAdd,
|
||||
onClose,
|
||||
canInstall,
|
||||
isBeingInstalled,
|
||||
}: Props) => {
|
||||
const [asset, setAsset] = React.useState<?Asset>(null);
|
||||
const [error, setError] = React.useState<?Error>(null);
|
||||
const loadAsset = React.useCallback(
|
||||
() => {
|
||||
(async () => {
|
||||
try {
|
||||
const loadedAsset = await getAsset(assetShortHeader);
|
||||
setAsset(loadedAsset);
|
||||
} catch (error) {
|
||||
console.log('Error while loading asset:', error);
|
||||
setError(error);
|
||||
// TODO: handle error
|
||||
}
|
||||
})();
|
||||
},
|
||||
[assetShortHeader]
|
||||
);
|
||||
|
||||
React.useEffect(
|
||||
() => {
|
||||
loadAsset();
|
||||
},
|
||||
[loadAsset]
|
||||
);
|
||||
|
||||
return (
|
||||
<Dialog
|
||||
open
|
||||
title={<Trans>Add an object from the store</Trans>}
|
||||
onRequestClose={onClose}
|
||||
actions={[
|
||||
<FlatButton key="back" label={<Trans>Back</Trans>} onClick={onClose} />,
|
||||
<LeftLoader
|
||||
isLoading={isBeingInstalled || (!asset && !error)}
|
||||
key="install"
|
||||
>
|
||||
<RaisedButton
|
||||
primary
|
||||
icon={<Add />}
|
||||
label={<Trans>Add to the game</Trans>}
|
||||
onClick={onAdd}
|
||||
disabled={!canInstall || isBeingInstalled || !asset}
|
||||
/>
|
||||
</LeftLoader>,
|
||||
]}
|
||||
>
|
||||
<Column expand noMargin>
|
||||
<ResponsiveLineStackLayout noMargin>
|
||||
<ResponsiveWindowMeasurer>
|
||||
{windowWidth => (
|
||||
<div
|
||||
style={{
|
||||
...styles.previewBackground,
|
||||
...(windowWidth === 'small'
|
||||
? styles.horizontalPreviewBackground
|
||||
: styles.verticalPreviewBackground),
|
||||
}}
|
||||
>
|
||||
<img
|
||||
style={styles.previewImage}
|
||||
src={assetShortHeader.previewImageUrls[0]}
|
||||
alt={assetShortHeader.name}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</ResponsiveWindowMeasurer>
|
||||
<Column expand>
|
||||
<div>
|
||||
<Text size="title" style={styles.inlineText}>
|
||||
{assetShortHeader.name}
|
||||
</Text>{' '}
|
||||
-{' '}
|
||||
<Text size="body" style={styles.inlineText}>
|
||||
{assetShortHeader.shortDescription}
|
||||
</Text>
|
||||
</div>
|
||||
<span>
|
||||
{assetShortHeader.tags.map(tag => (
|
||||
<Chip size="small" style={styles.chip} label={tag} key={tag} />
|
||||
))}
|
||||
</span>
|
||||
{asset ? (
|
||||
<React.Fragment>
|
||||
<Text size="body">
|
||||
<Trans>By {asset.authors.join(', ')}</Trans>
|
||||
</Text>
|
||||
<Text size="body">
|
||||
<Trans>License: {asset.license}</Trans>
|
||||
</Text>
|
||||
<Text size="body">{asset.description}</Text>
|
||||
</React.Fragment>
|
||||
) : error ? (
|
||||
<PlaceholderError onRetry={loadAsset}>
|
||||
<Trans>
|
||||
Error while loading the asset. Verify your internet connection
|
||||
or try again later.
|
||||
</Trans>
|
||||
</PlaceholderError>
|
||||
) : (
|
||||
<PlaceholderLoader />
|
||||
)}
|
||||
</Column>
|
||||
</ResponsiveLineStackLayout>
|
||||
{asset ? (
|
||||
<Column>
|
||||
<CustomizationFields
|
||||
project={project}
|
||||
layout={layout}
|
||||
objectsContainer={objectsContainer}
|
||||
resourceSources={resourceSources}
|
||||
onChooseResource={onChooseResource}
|
||||
resourceExternalEditors={resourceExternalEditors}
|
||||
asset={asset}
|
||||
/>
|
||||
</Column>
|
||||
) : null}
|
||||
</Column>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
302
newIDE/app/src/AssetStore/AssetStoreContext.js
Normal file
302
newIDE/app/src/AssetStore/AssetStoreContext.js
Normal file
@@ -0,0 +1,302 @@
|
||||
// @flow
|
||||
import * as React from 'react';
|
||||
import {
|
||||
type FiltersState,
|
||||
type ChosenCategory,
|
||||
useFilters,
|
||||
} from './FiltersChooser';
|
||||
import {
|
||||
type AssetShortHeader,
|
||||
type Filters,
|
||||
listAllAssets,
|
||||
} from '../Utils/GDevelopServices/Asset';
|
||||
import shuffle from 'lodash/shuffle';
|
||||
import SearchApi from 'js-worker-search';
|
||||
|
||||
const defaultSearchText = '';
|
||||
|
||||
type AssetStoreState = {|
|
||||
filters: ?Filters,
|
||||
searchResults: ?Array<AssetShortHeader>,
|
||||
fetchAssetsAndFilters: () => void,
|
||||
error: ?Error,
|
||||
searchText: string,
|
||||
setSearchText: string => void,
|
||||
filtersState: FiltersState,
|
||||
|};
|
||||
|
||||
export const AssetStoreContext = React.createContext<AssetStoreState>({
|
||||
filters: null,
|
||||
searchResults: null,
|
||||
fetchAssetsAndFilters: () => {},
|
||||
error: null,
|
||||
searchText: '',
|
||||
setSearchText: () => {},
|
||||
filtersState: {
|
||||
chosenFilters: new Set(),
|
||||
addFilter: () => {},
|
||||
removeFilter: () => {},
|
||||
chosenCategory: null,
|
||||
setChosenCategory: () => {},
|
||||
},
|
||||
});
|
||||
|
||||
type AssetStoreStateProviderProps = {|
|
||||
children: React.Node,
|
||||
|};
|
||||
|
||||
const filterAssetShortHeaders = (
|
||||
assetShortHeaders: ?Array<AssetShortHeader>,
|
||||
chosenCategory: ?ChosenCategory,
|
||||
chosenFilters: Set<string>
|
||||
) => {
|
||||
if (!assetShortHeaders) return null;
|
||||
|
||||
const startTime = performance.now();
|
||||
const filteredAssetShortHeaders = assetShortHeaders
|
||||
.filter(({ tags }) => {
|
||||
if (!chosenCategory) return true;
|
||||
|
||||
const hasChosenCategoryTag = tags.some(
|
||||
tag => tag === chosenCategory.node.name
|
||||
);
|
||||
if (!hasChosenCategoryTag) return false; // Asset is not in the selected category
|
||||
for (const parentNode of chosenCategory.parentNodes) {
|
||||
const hasParentCategoryTag = tags.some(tag => tag === parentNode.name);
|
||||
if (!hasParentCategoryTag) return false; // Asset is not in the the parent(s) of the selected category
|
||||
}
|
||||
|
||||
return true;
|
||||
})
|
||||
.filter(({ tags }) => {
|
||||
return (
|
||||
chosenFilters.size === 0 || tags.some(tag => chosenFilters.has(tag))
|
||||
);
|
||||
});
|
||||
|
||||
const totalTime = performance.now() - startTime;
|
||||
console.info(
|
||||
`Filtered assets by category/filters in ${totalTime.toFixed(3)}ms.`
|
||||
);
|
||||
return filteredAssetShortHeaders;
|
||||
};
|
||||
|
||||
export const AssetStoreStateProvider = ({
|
||||
children,
|
||||
}: AssetStoreStateProviderProps) => {
|
||||
const searchApiRef = React.useRef<?any>(null);
|
||||
const [
|
||||
searchResults,
|
||||
setSearchResults,
|
||||
] = React.useState<?Array<AssetShortHeader>>(null);
|
||||
|
||||
const [assetShortHeadersById, setAssetShortHeadersById] = React.useState<?{
|
||||
[string]: AssetShortHeader,
|
||||
}>(null);
|
||||
const [filters, setFilters] = React.useState<?Filters>(null);
|
||||
const [error, setError] = React.useState<?Error>(null);
|
||||
const isLoading = React.useRef<boolean>(false);
|
||||
|
||||
const [searchText, setSearchText] = React.useState(defaultSearchText);
|
||||
const filtersState = useFilters();
|
||||
|
||||
const fetchAssetsAndFilters = React.useCallback(
|
||||
() => {
|
||||
// Don't attempt to load again assets and filters if they
|
||||
// were loaded already.
|
||||
if (assetShortHeadersById || isLoading.current) return;
|
||||
|
||||
(async () => {
|
||||
setError(null);
|
||||
isLoading.current = true;
|
||||
|
||||
try {
|
||||
const { assetShortHeaders, filters } = await listAllAssets();
|
||||
|
||||
const assetShortHeadersById = {};
|
||||
assetShortHeaders.forEach(assetShortHeader => {
|
||||
assetShortHeadersById[assetShortHeader.id] = assetShortHeader;
|
||||
});
|
||||
|
||||
console.info(
|
||||
`Loaded ${assetShortHeaders.length} assets from the asset store.`
|
||||
);
|
||||
setAssetShortHeadersById(assetShortHeadersById);
|
||||
setFilters(filters);
|
||||
} catch (error) {
|
||||
setError(error);
|
||||
}
|
||||
|
||||
isLoading.current = false;
|
||||
})();
|
||||
},
|
||||
[assetShortHeadersById, isLoading]
|
||||
);
|
||||
|
||||
React.useEffect(
|
||||
() => {
|
||||
// Don't attempt to load again assets and filters if they
|
||||
// were loaded already.
|
||||
if (assetShortHeadersById || isLoading.current) return;
|
||||
|
||||
const timeoutId = setTimeout(() => {
|
||||
console.info('Pre-fetching assets from asset store...');
|
||||
fetchAssetsAndFilters();
|
||||
}, 6000);
|
||||
return () => clearTimeout(timeoutId);
|
||||
},
|
||||
[fetchAssetsAndFilters, assetShortHeadersById, isLoading]
|
||||
);
|
||||
|
||||
// Keep in memory a list of all the assets, shuffled for
|
||||
// easing random discovery of assets when no search is done.
|
||||
const suffledAssetShortHeaders: ?Array<AssetShortHeader> = React.useMemo(
|
||||
() => {
|
||||
if (!assetShortHeadersById) return null;
|
||||
|
||||
return shuffle(
|
||||
Object.keys(assetShortHeadersById).map(id => assetShortHeadersById[id])
|
||||
);
|
||||
},
|
||||
[assetShortHeadersById]
|
||||
);
|
||||
|
||||
// Index assets that have been loaded.
|
||||
React.useEffect(
|
||||
() => {
|
||||
if (!assetShortHeadersById) {
|
||||
// Nothing to index - yet.
|
||||
return;
|
||||
}
|
||||
|
||||
const startTime = performance.now();
|
||||
if (searchApiRef.current) {
|
||||
searchApiRef.current.terminate();
|
||||
searchApiRef.current = null;
|
||||
}
|
||||
|
||||
try {
|
||||
const newSearchApi = new SearchApi();
|
||||
const allIds = Object.keys(assetShortHeadersById);
|
||||
|
||||
allIds.forEach(id => {
|
||||
const assetShortHeader = assetShortHeadersById[id];
|
||||
newSearchApi.indexDocument(
|
||||
assetShortHeader.id,
|
||||
assetShortHeader.name +
|
||||
'\n' +
|
||||
assetShortHeader.shortDescription +
|
||||
'\n' +
|
||||
assetShortHeader.tags.join(', ')
|
||||
);
|
||||
});
|
||||
|
||||
const totalTime = performance.now() - startTime;
|
||||
console.info(
|
||||
`Indexed ${allIds.length} assets in ${totalTime.toFixed(3)}ms.`
|
||||
);
|
||||
searchApiRef.current = newSearchApi;
|
||||
} catch (error) {
|
||||
console.error('Error while indexing assets: ', error);
|
||||
}
|
||||
},
|
||||
[assetShortHeadersById]
|
||||
);
|
||||
|
||||
// Update the search results according to the assets/search term/
|
||||
// chosen category and chosen filters.
|
||||
const searchApi = searchApiRef.current;
|
||||
const { chosenCategory, chosenFilters } = filtersState;
|
||||
React.useEffect(
|
||||
() => {
|
||||
let discardSearch = false;
|
||||
if (!searchText) {
|
||||
setSearchResults(
|
||||
filterAssetShortHeaders(
|
||||
suffledAssetShortHeaders,
|
||||
chosenCategory,
|
||||
chosenFilters
|
||||
)
|
||||
);
|
||||
} else {
|
||||
if (!assetShortHeadersById || !searchApi) {
|
||||
console.info(
|
||||
'Search for assets skipped because assets are not ready - will be retried when ready'
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
const startTime = performance.now();
|
||||
searchApi
|
||||
.search(searchText)
|
||||
.then((partialSearchResultIds: Array<string>) => {
|
||||
if (discardSearch) {
|
||||
console.info(
|
||||
'Discarding search results as a new search was launched.'
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
const partialSearchResults = partialSearchResultIds
|
||||
.map(id => assetShortHeadersById[id])
|
||||
.filter(Boolean);
|
||||
|
||||
const totalTime = performance.now() - startTime;
|
||||
console.info(
|
||||
`Found ${
|
||||
partialSearchResults.length
|
||||
} assets in ${totalTime.toFixed(3)}ms.`
|
||||
);
|
||||
|
||||
setSearchResults(
|
||||
filterAssetShortHeaders(
|
||||
partialSearchResults,
|
||||
chosenCategory,
|
||||
chosenFilters
|
||||
)
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
return () => {
|
||||
// Effect is being destroyed - meaning that a new search was launched.
|
||||
// Cancel this one.
|
||||
discardSearch = true;
|
||||
};
|
||||
},
|
||||
[
|
||||
suffledAssetShortHeaders,
|
||||
assetShortHeadersById,
|
||||
searchText,
|
||||
chosenCategory,
|
||||
chosenFilters,
|
||||
searchApi,
|
||||
]
|
||||
);
|
||||
|
||||
const assetStoreState = React.useMemo(
|
||||
() => ({
|
||||
searchResults,
|
||||
fetchAssetsAndFilters,
|
||||
filters,
|
||||
error,
|
||||
searchText,
|
||||
setSearchText,
|
||||
filtersState,
|
||||
}),
|
||||
[
|
||||
searchResults,
|
||||
error,
|
||||
filters,
|
||||
searchText,
|
||||
filtersState,
|
||||
fetchAssetsAndFilters,
|
||||
]
|
||||
);
|
||||
|
||||
return (
|
||||
<AssetStoreContext.Provider value={assetStoreState}>
|
||||
{children}
|
||||
</AssetStoreContext.Provider>
|
||||
);
|
||||
};
|
120
newIDE/app/src/AssetStore/CustomizationFields.js
Normal file
120
newIDE/app/src/AssetStore/CustomizationFields.js
Normal file
@@ -0,0 +1,120 @@
|
||||
// @flow
|
||||
import * as React from 'react';
|
||||
import { type Asset } from '../Utils/GDevelopServices/Asset';
|
||||
import { ColumnStackLayout } from '../UI/Layout';
|
||||
import { unserializeFromJSObject } from '../Utils/Serializer';
|
||||
import flatten from 'lodash/flatten';
|
||||
import ParameterRenderingService from '../EventsSheet/ParameterRenderingService';
|
||||
import {
|
||||
type ResourceSource,
|
||||
type ChooseResourceFunction,
|
||||
} from '../ResourcesList/ResourceSource.flow';
|
||||
import { type ResourceExternalEditor } from '../ResourcesList/ResourceExternalEditor.flow';
|
||||
const gd: libGDevelop = global.gd;
|
||||
|
||||
type Props = {|
|
||||
project: gdProject,
|
||||
layout: ?gdLayout,
|
||||
objectsContainer: gdObjectsContainer,
|
||||
resourceSources: Array<ResourceSource>,
|
||||
onChooseResource: ChooseResourceFunction,
|
||||
resourceExternalEditors: Array<ResourceExternalEditor>,
|
||||
asset: Asset,
|
||||
|};
|
||||
|
||||
export default function CustomizationFields({
|
||||
asset,
|
||||
project,
|
||||
layout,
|
||||
objectsContainer,
|
||||
resourceSources,
|
||||
onChooseResource,
|
||||
resourceExternalEditors,
|
||||
}: Props) {
|
||||
const [allParameterMetadata, setAllParameterMetadata] = React.useState<
|
||||
Array<gdParameterMetadata>
|
||||
>([]);
|
||||
|
||||
React.useEffect(
|
||||
() => {
|
||||
// TODO: refactor into InstallAsset?
|
||||
setAllParameterMetadata(
|
||||
flatten(
|
||||
asset.objectAssets.map(objectAsset => {
|
||||
return flatten(
|
||||
objectAsset.customization
|
||||
.map(customization => {
|
||||
if (customization.events) {
|
||||
return customization.parameters.map(
|
||||
serializedParameterMetadata => {
|
||||
const parameterMetadata = new gd.ParameterMetadata();
|
||||
unserializeFromJSObject(
|
||||
parameterMetadata,
|
||||
serializedParameterMetadata
|
||||
);
|
||||
return parameterMetadata;
|
||||
}
|
||||
);
|
||||
} else if (customization.behaviorName) {
|
||||
return customization.properties.map(
|
||||
serializedParameterMetadata => {
|
||||
const parameterMetadata = new gd.ParameterMetadata();
|
||||
unserializeFromJSObject(
|
||||
parameterMetadata,
|
||||
serializedParameterMetadata
|
||||
);
|
||||
return parameterMetadata;
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
})
|
||||
.filter(Boolean)
|
||||
);
|
||||
})
|
||||
).filter(Boolean)
|
||||
);
|
||||
|
||||
return () => {
|
||||
allParameterMetadata.forEach(parameterMetadata => {
|
||||
parameterMetadata.delete();
|
||||
});
|
||||
setAllParameterMetadata([]);
|
||||
};
|
||||
},
|
||||
[asset]
|
||||
);
|
||||
|
||||
return (
|
||||
<ColumnStackLayout>
|
||||
{allParameterMetadata.map((parameterMetadata, index) => {
|
||||
const parameterMetadataType = parameterMetadata.getType();
|
||||
const ParameterComponent = ParameterRenderingService.getParameterComponent(
|
||||
parameterMetadataType
|
||||
);
|
||||
|
||||
return (
|
||||
<ParameterComponent
|
||||
parameterMetadata={parameterMetadata}
|
||||
value={'TODO'}
|
||||
onChange={value => {
|
||||
/* TODO */
|
||||
}}
|
||||
project={project}
|
||||
scope={{
|
||||
layout,
|
||||
}}
|
||||
globalObjectsContainer={project}
|
||||
objectsContainer={objectsContainer}
|
||||
key={index}
|
||||
parameterRenderingService={ParameterRenderingService}
|
||||
resourceSources={resourceSources}
|
||||
onChooseResource={onChooseResource}
|
||||
resourceExternalEditors={resourceExternalEditors}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</ColumnStackLayout>
|
||||
);
|
||||
}
|
200
newIDE/app/src/AssetStore/FiltersChooser.js
Normal file
200
newIDE/app/src/AssetStore/FiltersChooser.js
Normal file
@@ -0,0 +1,200 @@
|
||||
// @flow
|
||||
import { Trans } from '@lingui/macro';
|
||||
import * as React from 'react';
|
||||
import {
|
||||
type Filters,
|
||||
type TagsTreeNode,
|
||||
} from '../Utils/GDevelopServices/Asset';
|
||||
import TreeView from '@material-ui/lab/TreeView';
|
||||
import TreeItem from '@material-ui/lab/TreeItem';
|
||||
import ExpandMoreIcon from '@material-ui/icons/ExpandMore';
|
||||
import ChevronRightIcon from '@material-ui/icons/ChevronRight';
|
||||
import Subheader from '../UI/Subheader';
|
||||
import InlineCheckbox from '../UI/InlineCheckbox';
|
||||
import { ColumnStackLayout } from '../UI/Layout';
|
||||
import PlaceholderLoader from '../UI/PlaceholderLoader';
|
||||
import EmptyMessage from '../UI/EmptyMessage';
|
||||
|
||||
export type ChosenCategory = {|
|
||||
node: TagsTreeNode,
|
||||
parentNodes: Array<TagsTreeNode>,
|
||||
|};
|
||||
|
||||
export type FiltersState = {|
|
||||
chosenFilters: Set<string>,
|
||||
addFilter: string => void,
|
||||
removeFilter: string => void,
|
||||
chosenCategory: ?ChosenCategory,
|
||||
setChosenCategory: (?ChosenCategory) => void,
|
||||
|};
|
||||
|
||||
export const useFilters = (): FiltersState => {
|
||||
const [chosenCategory, setChosenCategory] = React.useState<?ChosenCategory>(
|
||||
null
|
||||
);
|
||||
const [chosenFilters, setChosenFilters] = React.useState<Set<string>>(
|
||||
() => new Set()
|
||||
);
|
||||
|
||||
const setChosenCategoryAndUpdateFilters = React.useCallback(
|
||||
(newChosenCategory: ?ChosenCategory) => {
|
||||
if (!newChosenCategory) {
|
||||
// No more category is chosen. Keep the filters
|
||||
setChosenCategory(null);
|
||||
} else {
|
||||
// Remove the filters that are not included in the category,
|
||||
// as it would make no sense (not displayed, and everything would be filtered out).
|
||||
const newChosenFilters = new Set();
|
||||
for (const tag of chosenFilters.keys()) {
|
||||
if (newChosenCategory.node.allChildrenTags.includes(tag)) {
|
||||
newChosenFilters.add(tag);
|
||||
}
|
||||
}
|
||||
|
||||
setChosenFilters(newChosenFilters);
|
||||
setChosenCategory(newChosenCategory);
|
||||
}
|
||||
},
|
||||
[chosenFilters]
|
||||
);
|
||||
|
||||
return {
|
||||
chosenCategory,
|
||||
setChosenCategory: setChosenCategoryAndUpdateFilters,
|
||||
chosenFilters,
|
||||
addFilter: (tag: string) => {
|
||||
const newChosenFilters = new Set(chosenFilters);
|
||||
newChosenFilters.add(tag);
|
||||
setChosenFilters(newChosenFilters);
|
||||
},
|
||||
removeFilter: (tag: string) => {
|
||||
const newChosenFilters = new Set(chosenFilters);
|
||||
newChosenFilters.delete(tag);
|
||||
|
||||
setChosenFilters(newChosenFilters);
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
const nodeIdSeparator = ';-/>'; // A seemingly random string to ensure no clashes with tag names.
|
||||
const toNodeId = (nodes: Array<TagsTreeNode>) =>
|
||||
nodes.map(node => node.name).join(nodeIdSeparator);
|
||||
|
||||
const TagsTreeItems = ({
|
||||
tagsTreeNodes,
|
||||
onChoose,
|
||||
parentNodes,
|
||||
}: {|
|
||||
parentNodes: Array<TagsTreeNode>,
|
||||
tagsTreeNodes: Array<TagsTreeNode>,
|
||||
onChoose: ChosenCategory => void,
|
||||
|}) => {
|
||||
if (!tagsTreeNodes.length) return null;
|
||||
|
||||
return tagsTreeNodes.map(node => {
|
||||
const newParentNodes = [...parentNodes, node];
|
||||
return (
|
||||
<TreeItem
|
||||
nodeId={toNodeId(newParentNodes)}
|
||||
label={node.name}
|
||||
key={node.name}
|
||||
onLabelClick={() => onChoose({ node, parentNodes })}
|
||||
collapseIcon={node.children.length ? <ExpandMoreIcon /> : null}
|
||||
expandIcon={node.children.length ? <ChevronRightIcon /> : null}
|
||||
>
|
||||
<TagsTreeItems
|
||||
tagsTreeNodes={node.children}
|
||||
parentNodes={newParentNodes}
|
||||
onChoose={onChoose}
|
||||
/>
|
||||
</TreeItem>
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
const MemoizedTagsTree = React.memo(function TagsTree({
|
||||
chosenCategory,
|
||||
setChosenCategory,
|
||||
allFilters,
|
||||
}) {
|
||||
return (
|
||||
<TreeView
|
||||
selected={
|
||||
chosenCategory
|
||||
? toNodeId([...chosenCategory.parentNodes, chosenCategory.node])
|
||||
: ''
|
||||
}
|
||||
onNodeSelect={() => {}}
|
||||
>
|
||||
<TreeItem
|
||||
nodeId=""
|
||||
label={<Trans>All assets</Trans>}
|
||||
onLabelClick={() => setChosenCategory(null)}
|
||||
/>
|
||||
<TagsTreeItems
|
||||
tagsTreeNodes={allFilters.tagsTree}
|
||||
onChoose={setChosenCategory}
|
||||
parentNodes={[]}
|
||||
/>
|
||||
</TreeView>
|
||||
);
|
||||
});
|
||||
|
||||
type Props = {|
|
||||
filtersState: FiltersState,
|
||||
allFilters: ?Filters,
|
||||
error: ?Error,
|
||||
|};
|
||||
|
||||
export const FiltersChooser = ({ filtersState, allFilters, error }: Props) => {
|
||||
if (!allFilters) {
|
||||
return <PlaceholderLoader />;
|
||||
}
|
||||
if (error) {
|
||||
// Error and retry button shown somewhere else in the UI
|
||||
return null;
|
||||
}
|
||||
|
||||
// Only display the tags that are contained inside the selected category
|
||||
const selectedCategoryTags = filtersState.chosenCategory
|
||||
? filtersState.chosenCategory.node.allChildrenTags
|
||||
: allFilters.defaultTags;
|
||||
|
||||
return (
|
||||
<React.Fragment>
|
||||
<Subheader>Categories</Subheader>
|
||||
<MemoizedTagsTree
|
||||
chosenCategory={filtersState.chosenCategory}
|
||||
setChosenCategory={filtersState.setChosenCategory}
|
||||
allFilters={allFilters}
|
||||
/>
|
||||
<Subheader>Filters</Subheader>
|
||||
<ColumnStackLayout>
|
||||
{!selectedCategoryTags ? (
|
||||
<EmptyMessage>
|
||||
<Trans>Choose a category to display filters</Trans>
|
||||
</EmptyMessage>
|
||||
) : selectedCategoryTags.length ? (
|
||||
selectedCategoryTags.map(tag => (
|
||||
<InlineCheckbox
|
||||
key={tag}
|
||||
label={tag}
|
||||
checked={filtersState.chosenFilters.has(tag)}
|
||||
onCheck={() => {
|
||||
if (filtersState.chosenFilters.has(tag)) {
|
||||
filtersState.removeFilter(tag);
|
||||
} else {
|
||||
filtersState.addFilter(tag);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
))
|
||||
) : (
|
||||
<EmptyMessage>
|
||||
<Trans>No filters in this category.</Trans>
|
||||
</EmptyMessage>
|
||||
)}
|
||||
</ColumnStackLayout>
|
||||
</React.Fragment>
|
||||
);
|
||||
};
|
477
newIDE/app/src/AssetStore/InstallAsset.js
Normal file
477
newIDE/app/src/AssetStore/InstallAsset.js
Normal file
@@ -0,0 +1,477 @@
|
||||
// @flow
|
||||
import {
|
||||
type Asset,
|
||||
type AssetShortHeader,
|
||||
getAsset,
|
||||
} from '../Utils/GDevelopServices/Asset';
|
||||
import newNameGenerator from '../Utils/NewNameGenerator';
|
||||
import { isNullPtr } from '../Utils/IsNullPtr';
|
||||
import { unserializeFromJSObject } from '../Utils/Serializer';
|
||||
import flatten from 'lodash/flatten';
|
||||
import uniqBy from 'lodash/uniqBy';
|
||||
import uniq from 'lodash/uniq';
|
||||
import {
|
||||
getExtensionsRegistry,
|
||||
getExtension,
|
||||
type SerializedExtension,
|
||||
} from '../Utils/GDevelopServices/Extension';
|
||||
import { type EventsFunctionsExtensionsState } from '../EventsFunctionsExtensionsLoader/EventsFunctionsExtensionsContext';
|
||||
import { mapVector } from '../Utils/MapFor';
|
||||
const gd: libGDevelop = global.gd;
|
||||
|
||||
const toNewGdMapStringString = (object: {
|
||||
[string]: string,
|
||||
}): gdMapStringString => {
|
||||
const map = new gd.MapStringString();
|
||||
for (var key in object) {
|
||||
map.set(key, object[key]);
|
||||
}
|
||||
|
||||
return map;
|
||||
};
|
||||
|
||||
const toPascalCase = (str: string) => {
|
||||
if (!str) return '';
|
||||
return str
|
||||
.replace(/^[^A-Za-z0-9]*|[^A-Za-z0-9]*$/g, '$')
|
||||
.replace(/[^A-Za-z0-9]+/g, '$')
|
||||
.replace(/([a-z])([A-Z])/g, function(m, a, b) {
|
||||
return a + '$' + b;
|
||||
})
|
||||
.toLowerCase()
|
||||
.replace(/(\$)(\w?)/g, function(m, a, b) {
|
||||
return b.toUpperCase();
|
||||
});
|
||||
};
|
||||
|
||||
export const sanitizeObjectName = (objectName: string) => {
|
||||
const trimmedObjectName = objectName.trim();
|
||||
if (!trimmedObjectName) return 'UnnamedObject';
|
||||
|
||||
const pascalCaseName = toPascalCase(trimmedObjectName);
|
||||
|
||||
let prefixedObjectName = pascalCaseName;
|
||||
if (prefixedObjectName[0] >= '0' && prefixedObjectName[0] <= '9') {
|
||||
prefixedObjectName = '_' + prefixedObjectName;
|
||||
}
|
||||
|
||||
return prefixedObjectName;
|
||||
};
|
||||
|
||||
export const installResource = (
|
||||
resourcesManager: gdResourcesManager,
|
||||
serializedResource: any,
|
||||
resourceNewNames: { [string]: string }
|
||||
) => {
|
||||
const makeGdResource = () => {
|
||||
let resource;
|
||||
if (serializedResource.kind === 'image') {
|
||||
resource = new gd.ImageResource();
|
||||
} else {
|
||||
throw new Error(
|
||||
`Resource of kind "${serializedResource.kind}" is not supported.`
|
||||
);
|
||||
}
|
||||
|
||||
unserializeFromJSObject(resource, serializedResource);
|
||||
|
||||
return resource;
|
||||
};
|
||||
|
||||
const originalResourceName: string = serializedResource.name;
|
||||
const resourceFileUrl: string = serializedResource.file;
|
||||
|
||||
if (resourceNewNames[originalResourceName]) {
|
||||
// The resource was already added previously - don't
|
||||
// bother adding it again.
|
||||
return;
|
||||
}
|
||||
|
||||
if (!resourcesManager.hasResource(originalResourceName)) {
|
||||
// There is no existing resource with this name, just add the new resource.
|
||||
const newResource = makeGdResource();
|
||||
newResource.setName(originalResourceName);
|
||||
resourceNewNames[originalResourceName] = originalResourceName;
|
||||
|
||||
resourcesManager.addResource(newResource);
|
||||
newResource.delete();
|
||||
return;
|
||||
}
|
||||
|
||||
// There is an existing resource with the name - check if it's the
|
||||
// same resource or not.
|
||||
const existingResource = resourcesManager.getResource(originalResourceName);
|
||||
const file = existingResource.getFile();
|
||||
if (file === resourceFileUrl) {
|
||||
// Resource is already existing and is the same - nothing to do.
|
||||
return;
|
||||
}
|
||||
|
||||
// The existing resource is different - find a new name for the new resource.
|
||||
const newName = newNameGenerator(originalResourceName, name =>
|
||||
resourcesManager.hasResource(name)
|
||||
);
|
||||
const newResource = makeGdResource();
|
||||
newResource.setName(newName);
|
||||
resourceNewNames[originalResourceName] = newName;
|
||||
|
||||
resourcesManager.addResource(newResource);
|
||||
newResource.delete();
|
||||
};
|
||||
|
||||
export const addAssetToProject = async ({
|
||||
asset,
|
||||
project,
|
||||
events,
|
||||
objectsContainer,
|
||||
}: {|
|
||||
asset: Asset,
|
||||
project: gdProject,
|
||||
events: gdEventsList,
|
||||
objectsContainer: gdObjectsContainer,
|
||||
|}) => {
|
||||
const objectNewNames = {};
|
||||
const resourceNewNames = {};
|
||||
const createdObjects: Array<gdObject> = [];
|
||||
|
||||
asset.objectAssets.forEach(objectAsset => {
|
||||
const type: ?string = objectAsset.object.type;
|
||||
if (!type) throw new Error('An object has no type specified');
|
||||
|
||||
// Insert the object
|
||||
const originalName = sanitizeObjectName(objectAsset.object.name);
|
||||
const newName = newNameGenerator(originalName, name =>
|
||||
objectsContainer.hasObjectNamed(name)
|
||||
);
|
||||
const object = objectsContainer.insertNewObject(
|
||||
project,
|
||||
type,
|
||||
newName,
|
||||
objectsContainer.getObjectsCount()
|
||||
);
|
||||
objectNewNames[originalName] = newName;
|
||||
|
||||
unserializeFromJSObject(
|
||||
object,
|
||||
objectAsset.object,
|
||||
'unserializeFrom',
|
||||
project
|
||||
);
|
||||
|
||||
// The name was overwritten after unserialization.
|
||||
object.setName(newName);
|
||||
|
||||
// Add resources used by the object
|
||||
objectAsset.resources.forEach(serializedResource => {
|
||||
installResource(
|
||||
project.getResourcesManager(),
|
||||
serializedResource,
|
||||
resourceNewNames
|
||||
);
|
||||
});
|
||||
|
||||
// TODO: Check how multiple objects are handled
|
||||
|
||||
// Resources may have been renamed to be added to the project.
|
||||
// In this case, rename them in the object.
|
||||
const renamedResourcesMap = toNewGdMapStringString(resourceNewNames);
|
||||
const resourcesRenamer = new gd.ResourcesRenamer(renamedResourcesMap);
|
||||
renamedResourcesMap.delete();
|
||||
object.exposeResources(resourcesRenamer);
|
||||
resourcesRenamer.delete();
|
||||
|
||||
objectAsset.customization.forEach(customization => {
|
||||
if (customization.behaviorName) {
|
||||
const { behaviorName, behaviorType } = customization;
|
||||
const behavior = gd.JsPlatform.get().getBehavior(behaviorType);
|
||||
if (isNullPtr(gd, behavior)) {
|
||||
throw new Error(
|
||||
'Behavior with type ' + behaviorType + ' could not be found.'
|
||||
);
|
||||
}
|
||||
|
||||
const behaviorContent = object.addNewBehavior(
|
||||
project,
|
||||
behaviorType,
|
||||
behaviorName
|
||||
);
|
||||
customization.properties.forEach(property => {
|
||||
behavior.updateProperty(
|
||||
behaviorContent.getContent(),
|
||||
property.name,
|
||||
property.defaultValue
|
||||
);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
createdObjects.push(object);
|
||||
});
|
||||
|
||||
// Add the events after adding all objects, as we need to potentially
|
||||
// rename the objects in the inserted events.
|
||||
asset.objectAssets.forEach(objectAsset => {
|
||||
const originalName = objectAsset.object.name;
|
||||
const newName = objectNewNames[originalName];
|
||||
|
||||
objectAsset.customization.forEach(customization => {
|
||||
if (customization.events) {
|
||||
const groupEvent = new gd.GroupEvent();
|
||||
groupEvent.setName(newName);
|
||||
|
||||
unserializeFromJSObject(
|
||||
groupEvent.getSubEvents(),
|
||||
customization.events,
|
||||
'unserializeFrom',
|
||||
project
|
||||
);
|
||||
|
||||
// Find/replace the customization parameters in the events.
|
||||
customization.parameters.forEach(parameter => {
|
||||
gd.EventsRefactorer.replaceStringInEvents(
|
||||
project,
|
||||
objectsContainer,
|
||||
groupEvent.getSubEvents(),
|
||||
parameter.name,
|
||||
parameter.defaultValue,
|
||||
true,
|
||||
true,
|
||||
true
|
||||
);
|
||||
});
|
||||
|
||||
// Rename any object that was renamed when inserted.
|
||||
// Do this **after** replacing the customization parameters,
|
||||
// as some expressions can be invalid before customization
|
||||
// parameters replacements.
|
||||
for (const originalName in objectNewNames) {
|
||||
const newName = objectNewNames[originalName];
|
||||
if (originalName !== newName) {
|
||||
gd.EventsRefactorer.renameObjectInEvents(
|
||||
project.getCurrentPlatform(),
|
||||
project,
|
||||
objectsContainer,
|
||||
groupEvent.getSubEvents(),
|
||||
originalName,
|
||||
newName
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
events.insertEvent(groupEvent, events.getEventsCount());
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
return {
|
||||
createdObjects,
|
||||
}
|
||||
};
|
||||
|
||||
type RequiredBehavior = {|
|
||||
extensionName: string,
|
||||
extensionVersion: string,
|
||||
behaviorType: string,
|
||||
|};
|
||||
|
||||
export const getRequiredBehaviorsFromAsset = (
|
||||
asset: Asset
|
||||
): Array<RequiredBehavior> => {
|
||||
return uniqBy(
|
||||
flatten(
|
||||
asset.objectAssets.map(objectAsset => {
|
||||
return objectAsset.customization
|
||||
.map(customization => {
|
||||
if (customization.behaviorName) {
|
||||
const {
|
||||
behaviorType,
|
||||
extensionName,
|
||||
extensionVersion,
|
||||
} = customization;
|
||||
return { behaviorType, extensionName, extensionVersion };
|
||||
}
|
||||
|
||||
return null;
|
||||
})
|
||||
.filter(Boolean);
|
||||
})
|
||||
),
|
||||
({ behaviorType }) => behaviorType // TODO: extension name??
|
||||
);
|
||||
};
|
||||
|
||||
type RequiredExtension = {|
|
||||
extensionName: string,
|
||||
extensionVersion: string,
|
||||
|};
|
||||
|
||||
export const filterMissingExtensions = (
|
||||
gd: libGDevelop,
|
||||
requiredExtensions: Array<RequiredExtension>
|
||||
): Array<RequiredExtension> => {
|
||||
const loadedExtensionNames = mapVector(
|
||||
gd.asPlatform(gd.JsPlatform.get()).getAllPlatformExtensions(),
|
||||
extension => {
|
||||
return extension.getName();
|
||||
}
|
||||
);
|
||||
|
||||
return requiredExtensions.filter(({ extensionName }) => {
|
||||
return !loadedExtensionNames.includes(extensionName);
|
||||
});
|
||||
};
|
||||
|
||||
export const getRequiredExtensionsForEventsFromAsset = (
|
||||
asset: Asset
|
||||
): Array<RequiredExtension> => {
|
||||
return uniqBy(
|
||||
flatten(
|
||||
asset.objectAssets.map(objectAsset => {
|
||||
return flatten(
|
||||
objectAsset.customization
|
||||
.map(customization => {
|
||||
if (customization.events) {
|
||||
return customization.extensions;
|
||||
}
|
||||
|
||||
return null;
|
||||
})
|
||||
.filter(Boolean)
|
||||
);
|
||||
})
|
||||
),
|
||||
({ extensionName }) => extensionName
|
||||
);
|
||||
};
|
||||
|
||||
export const filterMissingBehaviors = (
|
||||
gd: libGDevelop,
|
||||
requiredBehaviors: Array<RequiredBehavior>
|
||||
): Array<RequiredBehavior> => {
|
||||
return requiredBehaviors.filter(({ behaviorType }) => {
|
||||
const behavior = gd.JsPlatform.get().getBehavior(behaviorType);
|
||||
return isNullPtr(gd, behavior);
|
||||
});
|
||||
};
|
||||
|
||||
export const downloadExtensions = async (
|
||||
extensionNames: Array<string>
|
||||
): Promise<Array<SerializedExtension>> => {
|
||||
if (!extensionNames.length) return Promise.resolve([]);
|
||||
|
||||
const extensionsRegistry = await getExtensionsRegistry();
|
||||
|
||||
const serializedExtensions = await Promise.all(
|
||||
uniq(extensionNames).map(extensionName => {
|
||||
const extensionShortHeader = extensionsRegistry.extensionShortHeaders.find(
|
||||
extensionShortHeader => {
|
||||
return extensionShortHeader.name === extensionName;
|
||||
}
|
||||
);
|
||||
if (!extensionShortHeader) {
|
||||
throw new Error(
|
||||
'Unable to find extension ' + extensionName + ' in the registry.'
|
||||
);
|
||||
}
|
||||
|
||||
return getExtension(extensionShortHeader);
|
||||
})
|
||||
);
|
||||
|
||||
return serializedExtensions;
|
||||
};
|
||||
|
||||
/**
|
||||
* Add a serialized (JS object) events function extension to the project,
|
||||
* triggering reload of extensions.
|
||||
*/
|
||||
export const addSerializedExtensionsToProject = (
|
||||
eventsFunctionsExtensionsState: EventsFunctionsExtensionsState,
|
||||
project: gdProject,
|
||||
serializedExtensions: Array<SerializedExtension>
|
||||
): Promise<void> => {
|
||||
serializedExtensions.forEach(serializedExtension => {
|
||||
const { name } = serializedExtension;
|
||||
if (!name)
|
||||
return Promise.reject(new Error('Malformed extension (missing name).'));
|
||||
|
||||
const newEventsFunctionsExtension = project.hasEventsFunctionsExtensionNamed(
|
||||
name
|
||||
)
|
||||
? project.getEventsFunctionsExtension(name)
|
||||
: project.insertNewEventsFunctionsExtension(name, 0);
|
||||
|
||||
unserializeFromJSObject(
|
||||
newEventsFunctionsExtension,
|
||||
serializedExtension,
|
||||
'unserializeFrom',
|
||||
project
|
||||
);
|
||||
});
|
||||
|
||||
return eventsFunctionsExtensionsState.loadProjectEventsFunctionsExtensions(
|
||||
project
|
||||
);
|
||||
};
|
||||
|
||||
type InstallAssetArgs = {|
|
||||
assetShortHeader: AssetShortHeader,
|
||||
eventsFunctionsExtensionsState: EventsFunctionsExtensionsState,
|
||||
project: gdProject,
|
||||
events: gdEventsList,
|
||||
objectsContainer: gdObjectsContainer,
|
||||
|};
|
||||
|
||||
type InstallAssetOutput = {|
|
||||
createdObjects: Array<gdObject>,
|
||||
|};
|
||||
|
||||
export const installAsset = async ({
|
||||
assetShortHeader,
|
||||
eventsFunctionsExtensionsState,
|
||||
project,
|
||||
events,
|
||||
objectsContainer,
|
||||
}: InstallAssetArgs): Promise<InstallAssetOutput> => {
|
||||
const asset = await getAsset(assetShortHeader);
|
||||
const requiredBehaviors = getRequiredBehaviorsFromAsset(asset);
|
||||
const requiredExtensions = getRequiredExtensionsForEventsFromAsset(asset);
|
||||
const missingBehaviors = filterMissingBehaviors(gd, requiredBehaviors);
|
||||
const missingExtensions = filterMissingExtensions(gd, requiredExtensions);
|
||||
const serializedExtensions = await downloadExtensions([
|
||||
...missingBehaviors.map(({ extensionName }) => extensionName),
|
||||
...missingExtensions.map(({ extensionName }) => extensionName),
|
||||
]);
|
||||
await addSerializedExtensionsToProject(
|
||||
eventsFunctionsExtensionsState,
|
||||
project,
|
||||
serializedExtensions
|
||||
);
|
||||
|
||||
const stillMissingBehaviors = filterMissingBehaviors(gd, requiredBehaviors);
|
||||
if (stillMissingBehaviors.length) {
|
||||
throw new Error(
|
||||
'These behaviors could not be installed: ' +
|
||||
missingBehaviors
|
||||
.map(
|
||||
({ extensionName, behaviorType }) =>
|
||||
`${behaviorType} (${extensionName})`
|
||||
)
|
||||
.join(', ')
|
||||
);
|
||||
}
|
||||
|
||||
const stillMissingExtensions = filterMissingExtensions(
|
||||
gd,
|
||||
requiredExtensions
|
||||
);
|
||||
if (stillMissingExtensions.length) {
|
||||
throw new Error(
|
||||
'These extensions could not be installed: ' +
|
||||
missingExtensions.map(({ extensionName }) => extensionName).join(', ')
|
||||
);
|
||||
}
|
||||
|
||||
const output = await addAssetToProject({ project, asset, events, objectsContainer });
|
||||
return output;
|
||||
};
|
683
newIDE/app/src/AssetStore/InstallAsset.spec.js
Normal file
683
newIDE/app/src/AssetStore/InstallAsset.spec.js
Normal file
@@ -0,0 +1,683 @@
|
||||
// @flow
|
||||
import {
|
||||
addAssetToProject,
|
||||
getRequiredBehaviorsFromAsset,
|
||||
filterMissingBehaviors,
|
||||
downloadExtensions,
|
||||
installAsset,
|
||||
getRequiredExtensionsForEventsFromAsset,
|
||||
filterMissingExtensions,
|
||||
sanitizeObjectName,
|
||||
} from './InstallAsset';
|
||||
import { makeTestProject } from '../fixtures/TestProject';
|
||||
import { type EventsFunctionsExtensionsState } from '../EventsFunctionsExtensionsLoader/EventsFunctionsExtensionsContext';
|
||||
import {
|
||||
fakeAssetShortHeader1,
|
||||
fakeAsset1,
|
||||
fakeAssetWithBehaviorCustomizations1,
|
||||
fakeAssetWithUnknownBehaviorCustomizations1,
|
||||
fakeAssetWithFlashBehaviorCustomizations1,
|
||||
fakeAssetWithEventCustomizationsAndFlashExtension1,
|
||||
flashExtensionShortHeader,
|
||||
fireBulletExtensionShortHeader,
|
||||
fakeAssetWithEventCustomizationsAndUnknownExtension1,
|
||||
} from '../fixtures/GDevelopServicesTestData';
|
||||
import { makeTestExtensions } from '../fixtures/TestExtensions';
|
||||
import {
|
||||
getExtensionsRegistry,
|
||||
getExtension,
|
||||
} from '../Utils/GDevelopServices/Extension';
|
||||
import { jssPreset } from '@material-ui/core';
|
||||
import { getAsset } from '../Utils/GDevelopServices/Asset';
|
||||
const gd: libGDevelop = global.gd;
|
||||
|
||||
jest.mock('../Utils/GDevelopServices/Extension');
|
||||
jest.mock('../Utils/GDevelopServices/Asset');
|
||||
|
||||
const mockFn = (fn: Function): JestMockFn<any, any> => fn;
|
||||
|
||||
describe('InstallAsset', () => {
|
||||
describe('sanitizeObjectName', () => {
|
||||
expect(sanitizeObjectName('')).toBe('UnnamedObject');
|
||||
expect(sanitizeObjectName('HelloWorld')).toBe('HelloWorld');
|
||||
expect(sanitizeObjectName('Hello World')).toBe('HelloWorld');
|
||||
expect(sanitizeObjectName('hello world')).toBe('HelloWorld');
|
||||
expect(sanitizeObjectName('hello world12')).toBe('HelloWorld12');
|
||||
expect(sanitizeObjectName('12 hello world')).toBe('_12HelloWorld');
|
||||
expect(sanitizeObjectName('/-=hello/-=world/-=')).toBe('HelloWorld');
|
||||
expect(sanitizeObjectName(' hello/-=world/-=')).toBe('HelloWorld');
|
||||
expect(sanitizeObjectName('9hello/-=world/-=')).toBe('_9helloWorld');
|
||||
expect(sanitizeObjectName(' 9hello/-=world/-=')).toBe('_9helloWorld');
|
||||
});
|
||||
|
||||
describe('addAssetToProject', () => {
|
||||
it('installs an object asset in the project, without renaming it if not needed', async () => {
|
||||
const { project } = makeTestProject(gd);
|
||||
const layout = project.insertNewLayout('MyTestLayout', 0);
|
||||
|
||||
const output = await addAssetToProject({
|
||||
project,
|
||||
objectsContainer: layout,
|
||||
events: layout.getEvents(),
|
||||
asset: fakeAsset1,
|
||||
});
|
||||
|
||||
expect(output.createdObjects).toHaveLength(1);
|
||||
expect(layout.hasObjectNamed('PlayerSpaceship')).toBe(true);
|
||||
expect(output.createdObjects).toEqual([
|
||||
layout.getObject('PlayerSpaceship'),
|
||||
]);
|
||||
expect(
|
||||
project.getResourcesManager().hasResource('player-ship1.png')
|
||||
).toBe(true);
|
||||
expect(
|
||||
project.getResourcesManager().hasResource('player-ship2.png')
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it('renames the object if name is already used', async () => {
|
||||
const { project } = makeTestProject(gd);
|
||||
const layout = project.insertNewLayout('MyTestLayout', 0);
|
||||
layout.insertNewObject(project, 'Sprite', 'PlayerSpaceship', 0);
|
||||
|
||||
const output = await addAssetToProject({
|
||||
project,
|
||||
objectsContainer: layout,
|
||||
events: layout.getEvents(),
|
||||
asset: fakeAsset1,
|
||||
});
|
||||
|
||||
expect(output.createdObjects).toHaveLength(1);
|
||||
expect(layout.hasObjectNamed('PlayerSpaceship')).toBe(true);
|
||||
expect(layout.hasObjectNamed('PlayerSpaceship2')).toBe(true);
|
||||
expect(
|
||||
project.getResourcesManager().hasResource('player-ship1.png')
|
||||
).toBe(true);
|
||||
expect(
|
||||
project.getResourcesManager().hasResource('player-ship2.png')
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it('does not add a resource if it is already existing', async () => {
|
||||
const { project } = makeTestProject(gd);
|
||||
const layout = project.insertNewLayout('MyTestLayout', 0);
|
||||
|
||||
const originalResourceNames = project
|
||||
.getResourcesManager()
|
||||
.getAllResourceNames()
|
||||
.toJSArray();
|
||||
|
||||
// Create a resource that is the same as the one added for the spaceship
|
||||
const resource = new gd.ImageResource();
|
||||
resource.setName('player-ship1.png');
|
||||
resource.setFile('https://example.com/player-ship1.png');
|
||||
project.getResourcesManager().addResource(resource);
|
||||
resource.delete();
|
||||
|
||||
// Install the spaceship
|
||||
await addAssetToProject({
|
||||
project,
|
||||
objectsContainer: layout,
|
||||
events: layout.getEvents(),
|
||||
asset: fakeAsset1,
|
||||
});
|
||||
|
||||
// Verify there was not extra resource added.
|
||||
expect(
|
||||
project
|
||||
.getResourcesManager()
|
||||
.getAllResourceNames()
|
||||
.toJSArray()
|
||||
).toEqual([
|
||||
...originalResourceNames,
|
||||
'player-ship1.png',
|
||||
'player-ship2.png',
|
||||
]);
|
||||
});
|
||||
|
||||
it('add a resource with a new name, if this name is already taken by another', async () => {
|
||||
const { project } = makeTestProject(gd);
|
||||
const layout = project.insertNewLayout('MyTestLayout', 0);
|
||||
|
||||
const originalResourceNames = project
|
||||
.getResourcesManager()
|
||||
.getAllResourceNames()
|
||||
.toJSArray();
|
||||
|
||||
// Create a resource that is NOT the same as the one added for the spaceship
|
||||
// but has the same name.
|
||||
const resource = new gd.ImageResource();
|
||||
resource.setName('player-ship1.png');
|
||||
resource.setFile('https://example.com/some-unrelated-file.png');
|
||||
project.getResourcesManager().addResource(resource);
|
||||
resource.delete();
|
||||
|
||||
// Install the spaceship
|
||||
await addAssetToProject({
|
||||
project,
|
||||
objectsContainer: layout,
|
||||
events: layout.getEvents(),
|
||||
asset: fakeAsset1,
|
||||
});
|
||||
|
||||
// Verify there was not extra resource added
|
||||
expect(
|
||||
project
|
||||
.getResourcesManager()
|
||||
.getAllResourceNames()
|
||||
.toJSArray()
|
||||
).toEqual([
|
||||
...originalResourceNames,
|
||||
'player-ship1.png',
|
||||
'player-ship1.png2',
|
||||
'player-ship2.png',
|
||||
]);
|
||||
expect(
|
||||
project
|
||||
.getResourcesManager()
|
||||
.getResource('player-ship1.png2')
|
||||
.getFile()
|
||||
).toBe('https://example.com/player-ship1.png');
|
||||
|
||||
// Verify the resource names used by the object
|
||||
expect(layout.hasObjectNamed('PlayerSpaceship')).toBe(true);
|
||||
const object = layout.getObject('PlayerSpaceship');
|
||||
|
||||
const resourcesInUse = new gd.ResourcesInUseHelper();
|
||||
object.exposeResources(resourcesInUse);
|
||||
const objectResourceNames = resourcesInUse
|
||||
.getAllImages()
|
||||
.toNewVectorString()
|
||||
.toJSArray();
|
||||
resourcesInUse.delete();
|
||||
|
||||
expect(objectResourceNames).toEqual([
|
||||
'player-ship1.png2',
|
||||
'player-ship2.png',
|
||||
]);
|
||||
});
|
||||
|
||||
it('installs an object asset in the project, adding the required behaviors', async () => {
|
||||
makeTestExtensions(gd);
|
||||
const { project } = makeTestProject(gd);
|
||||
const layout = project.insertNewLayout('MyTestLayout', 0);
|
||||
|
||||
await addAssetToProject({
|
||||
project,
|
||||
objectsContainer: layout,
|
||||
events: layout.getEvents(),
|
||||
asset: fakeAssetWithBehaviorCustomizations1,
|
||||
});
|
||||
|
||||
expect(layout.hasObjectNamed('PlayerSpaceship')).toBe(true);
|
||||
expect(
|
||||
layout
|
||||
.getObject('PlayerSpaceship')
|
||||
.getAllBehaviorNames()
|
||||
.toJSArray()
|
||||
).toEqual(['MyBehavior']);
|
||||
expect(
|
||||
layout
|
||||
.getObject('PlayerSpaceship')
|
||||
.getBehavior('MyBehavior')
|
||||
.getTypeName()
|
||||
).toBe('FakeBehavior::FakeBehavior');
|
||||
|
||||
// Check that the properties from customization were set.
|
||||
expect(
|
||||
gd.Serializer.toJSON(
|
||||
layout
|
||||
.getObject('PlayerSpaceship')
|
||||
.getBehavior('MyBehavior')
|
||||
.getContent()
|
||||
)
|
||||
).toBe('{"property1": "Overriden value","property2": true}');
|
||||
});
|
||||
|
||||
it('installs an object asset in the project, adding the required events', async () => {
|
||||
makeTestExtensions(gd);
|
||||
const { project } = makeTestProject(gd);
|
||||
const layout = project.insertNewLayout('MyTestLayout', 0);
|
||||
const eventsList = new gd.EventsList();
|
||||
|
||||
await addAssetToProject({
|
||||
project,
|
||||
objectsContainer: layout,
|
||||
events: eventsList,
|
||||
asset: fakeAssetWithEventCustomizationsAndFlashExtension1,
|
||||
});
|
||||
|
||||
expect(layout.hasObjectNamed('PlayerSpaceship')).toBe(true);
|
||||
expect(eventsList.getEventsCount()).toBe(1);
|
||||
|
||||
// Check that the events had their customization parameters properly applied
|
||||
const serializedEvents = new gd.SerializerElement();
|
||||
eventsList.serializeTo(serializedEvents);
|
||||
const serializedEventsJson = gd.Serializer.toJSON(serializedEvents);
|
||||
expect(serializedEventsJson).toMatch(
|
||||
'3 + PlayerSpaceship.Variable(test)'
|
||||
);
|
||||
expect(serializedEventsJson).toMatch(
|
||||
'3 + PlayerSpaceship.Variable(test2)'
|
||||
);
|
||||
});
|
||||
|
||||
it('renames the object if name is already used, including in events', async () => {
|
||||
makeTestExtensions(gd);
|
||||
const { project } = makeTestProject(gd);
|
||||
const layout = project.insertNewLayout('MyTestLayout', 0);
|
||||
const eventsList = new gd.EventsList();
|
||||
|
||||
// Add an object with the same name as the object asset.
|
||||
layout.insertNewObject(project, 'Sprite', 'PlayerSpaceship', 0);
|
||||
expect(layout.hasObjectNamed('PlayerSpaceship')).toBe(true);
|
||||
|
||||
await addAssetToProject({
|
||||
project,
|
||||
objectsContainer: layout,
|
||||
events: eventsList,
|
||||
asset: fakeAssetWithEventCustomizationsAndFlashExtension1,
|
||||
});
|
||||
|
||||
expect(layout.hasObjectNamed('PlayerSpaceship2')).toBe(true);
|
||||
expect(eventsList.getEventsCount()).toBe(1);
|
||||
|
||||
// Check that the events had their customization parameters properly applied
|
||||
// and the object renamed.
|
||||
const serializedEvents = new gd.SerializerElement();
|
||||
eventsList.serializeTo(serializedEvents);
|
||||
const serializedEventsJson = gd.Serializer.toJSON(serializedEvents);
|
||||
expect(serializedEventsJson).toMatch(
|
||||
'3 + PlayerSpaceship2.Variable(test)'
|
||||
);
|
||||
expect(serializedEventsJson).toMatch(
|
||||
'3 + PlayerSpaceship2.Variable(test2)'
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getRequiredBehaviorsFromAsset', () => {
|
||||
it('get the behaviors required for an asset', () => {
|
||||
expect(
|
||||
getRequiredBehaviorsFromAsset(
|
||||
fakeAssetWithEventCustomizationsAndFlashExtension1
|
||||
)
|
||||
).toEqual([]);
|
||||
expect(
|
||||
getRequiredBehaviorsFromAsset(fakeAssetWithBehaviorCustomizations1)
|
||||
).toEqual([
|
||||
{
|
||||
behaviorType: 'FakeBehavior::FakeBehavior',
|
||||
extensionName: 'FakeBehavior',
|
||||
extensionVersion: '1.0.0',
|
||||
},
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getRequiredExtensionsForEventsFromAsset', () => {
|
||||
it('get the extensions required for an asset', () => {
|
||||
expect(
|
||||
getRequiredExtensionsForEventsFromAsset(
|
||||
fakeAssetWithBehaviorCustomizations1
|
||||
)
|
||||
).toEqual([]);
|
||||
expect(
|
||||
getRequiredExtensionsForEventsFromAsset(
|
||||
fakeAssetWithEventCustomizationsAndFlashExtension1
|
||||
)
|
||||
).toEqual([
|
||||
{
|
||||
extensionName: 'Flash',
|
||||
extensionVersion: '1.0.0',
|
||||
},
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('filterMissingBehaviors', () => {
|
||||
it('filters behaviors that are not loaded ', () => {
|
||||
makeTestExtensions(gd);
|
||||
|
||||
expect(
|
||||
filterMissingBehaviors(gd, [
|
||||
// An unknown behavior not loaded:
|
||||
{
|
||||
extensionName: 'NotExistingExtension',
|
||||
extensionVersion: '1.0.0',
|
||||
behaviorType: 'NotExistingExtension::MissingBehavior',
|
||||
},
|
||||
// A fake behavior loaded in makeTestExtensions:
|
||||
{
|
||||
behaviorType: 'FakeBehavior::FakeBehavior',
|
||||
extensionName: 'FakeBehavior',
|
||||
extensionVersion: '1.0.0',
|
||||
},
|
||||
])
|
||||
).toEqual([
|
||||
{
|
||||
extensionName: 'NotExistingExtension',
|
||||
extensionVersion: '1.0.0',
|
||||
behaviorType: 'NotExistingExtension::MissingBehavior',
|
||||
},
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('filterMissingExtensions', () => {
|
||||
it('filters extensions that are not loaded ', () => {
|
||||
makeTestExtensions(gd);
|
||||
|
||||
expect(
|
||||
filterMissingExtensions(gd, [
|
||||
// An unknown behavior not loaded:
|
||||
{
|
||||
extensionName: 'NotExistingExtension',
|
||||
extensionVersion: '1.0.0',
|
||||
},
|
||||
// A fake extension loaded in makeTestExtensions:
|
||||
{
|
||||
extensionName: 'FakeBehavior',
|
||||
extensionVersion: '1.0.0',
|
||||
},
|
||||
])
|
||||
).toEqual([
|
||||
{
|
||||
extensionName: 'NotExistingExtension',
|
||||
extensionVersion: '1.0.0',
|
||||
},
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('downloadExtensions', () => {
|
||||
it('loads the required extensions ', async () => {
|
||||
mockFn(getExtensionsRegistry).mockImplementationOnce(() => ({
|
||||
version: '1.0.0',
|
||||
allTags: [''],
|
||||
extensionShortHeaders: [
|
||||
flashExtensionShortHeader,
|
||||
fireBulletExtensionShortHeader,
|
||||
],
|
||||
}));
|
||||
|
||||
mockFn(getExtension).mockImplementationOnce(
|
||||
() => fireBulletExtensionShortHeader
|
||||
);
|
||||
|
||||
await expect(downloadExtensions(['FireBullet'])).resolves.toEqual([
|
||||
fireBulletExtensionShortHeader,
|
||||
]);
|
||||
});
|
||||
|
||||
it('errors if an extension is not found ', async () => {
|
||||
mockFn(getExtensionsRegistry).mockImplementationOnce(() => ({
|
||||
version: '1.0.0',
|
||||
allTags: [''],
|
||||
extensionShortHeaders: [
|
||||
flashExtensionShortHeader,
|
||||
fireBulletExtensionShortHeader,
|
||||
],
|
||||
}));
|
||||
|
||||
await expect(
|
||||
downloadExtensions(['NotFoundExtension'])
|
||||
).rejects.toMatchObject({
|
||||
message: 'Unable to find extension NotFoundExtension in the registry.',
|
||||
});
|
||||
});
|
||||
|
||||
it("errors if the registry can't be loaded ", async () => {
|
||||
mockFn(getExtensionsRegistry).mockImplementationOnce(() => {
|
||||
throw new Error('Fake error');
|
||||
});
|
||||
|
||||
await expect(downloadExtensions(['FakeExtension'])).rejects.toMatchObject(
|
||||
{ message: 'Fake error' }
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('installAsset', () => {
|
||||
beforeEach(() => {
|
||||
mockFn(getAsset).mockReset();
|
||||
mockFn(getExtensionsRegistry).mockReset();
|
||||
mockFn(getExtension).mockReset();
|
||||
});
|
||||
|
||||
const mockEventsFunctionsExtensionsState: EventsFunctionsExtensionsState = {
|
||||
eventsFunctionsExtensionsError: null,
|
||||
loadProjectEventsFunctionsExtensions: () => Promise.resolve(),
|
||||
unloadProjectEventsFunctionsExtensions: () => {},
|
||||
reloadProjectEventsFunctionsExtensions: () => Promise.resolve(),
|
||||
unloadProjectEventsFunctionsExtension: () => {},
|
||||
getEventsFunctionsExtensionWriter: () => null,
|
||||
getEventsFunctionsExtensionOpener: () => null,
|
||||
ensureLoadFinished: () => Promise.resolve(),
|
||||
getIncludeFileHashs: () => ({}),
|
||||
};
|
||||
|
||||
it("throws if asset can't be downloaded", async () => {
|
||||
makeTestExtensions(gd);
|
||||
const { project } = makeTestProject(gd);
|
||||
const layout = project.insertNewLayout('MyTestLayout', 0);
|
||||
const eventsList = new gd.EventsList();
|
||||
mockFn(getAsset).mockImplementationOnce(() => {
|
||||
throw new Error('Fake error - unable to download');
|
||||
});
|
||||
|
||||
await expect(
|
||||
installAsset({
|
||||
assetShortHeader: fakeAssetShortHeader1,
|
||||
events: eventsList,
|
||||
project,
|
||||
objectsContainer: layout,
|
||||
eventsFunctionsExtensionsState: mockEventsFunctionsExtensionsState,
|
||||
})
|
||||
).rejects.toMatchObject({
|
||||
message: 'Fake error - unable to download',
|
||||
});
|
||||
|
||||
expect(getExtensionsRegistry).not.toHaveBeenCalled();
|
||||
expect(getExtension).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("throws if an extension for a behavior can't be found in the registry", async () => {
|
||||
makeTestExtensions(gd);
|
||||
const { project } = makeTestProject(gd);
|
||||
const layout = project.insertNewLayout('MyTestLayout', 0);
|
||||
const eventsList = new gd.EventsList();
|
||||
|
||||
// Get an asset that uses a behavior...
|
||||
mockFn(getAsset).mockImplementationOnce(
|
||||
() => fakeAssetWithUnknownBehaviorCustomizations1
|
||||
);
|
||||
|
||||
// ...but this behavior extension does not exist in the registry
|
||||
mockFn(getExtensionsRegistry).mockImplementationOnce(() => ({
|
||||
version: '1.0.0',
|
||||
allTags: [''],
|
||||
extensionShortHeaders: [
|
||||
flashExtensionShortHeader,
|
||||
fireBulletExtensionShortHeader,
|
||||
],
|
||||
}));
|
||||
|
||||
// Check that the extension is stated as not found in the registry
|
||||
await expect(
|
||||
installAsset({
|
||||
assetShortHeader: fakeAssetShortHeader1,
|
||||
events: eventsList,
|
||||
project,
|
||||
objectsContainer: layout,
|
||||
eventsFunctionsExtensionsState: mockEventsFunctionsExtensionsState,
|
||||
})
|
||||
).rejects.toMatchObject({
|
||||
message: 'Unable to find extension UnknownBehavior in the registry.',
|
||||
});
|
||||
|
||||
expect(getExtensionsRegistry).toHaveBeenCalledTimes(1);
|
||||
expect(getExtension).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("throws if an extension can't be found in the registry", async () => {
|
||||
makeTestExtensions(gd);
|
||||
const { project } = makeTestProject(gd);
|
||||
const layout = project.insertNewLayout('MyTestLayout', 0);
|
||||
const eventsList = new gd.EventsList();
|
||||
|
||||
// Get an asset that uses an extension...
|
||||
mockFn(getAsset).mockImplementationOnce(
|
||||
() => fakeAssetWithEventCustomizationsAndUnknownExtension1
|
||||
);
|
||||
|
||||
// ...but this extension does not exist in the registry
|
||||
mockFn(getExtensionsRegistry).mockImplementationOnce(() => ({
|
||||
version: '1.0.0',
|
||||
allTags: [''],
|
||||
extensionShortHeaders: [
|
||||
flashExtensionShortHeader,
|
||||
fireBulletExtensionShortHeader,
|
||||
],
|
||||
}));
|
||||
|
||||
// Check that the extension is stated as not found in the registry
|
||||
await expect(
|
||||
installAsset({
|
||||
assetShortHeader: fakeAssetShortHeader1,
|
||||
events: eventsList,
|
||||
project,
|
||||
objectsContainer: layout,
|
||||
eventsFunctionsExtensionsState: mockEventsFunctionsExtensionsState,
|
||||
})
|
||||
).rejects.toMatchObject({
|
||||
message: 'Unable to find extension UnknownExtension in the registry.',
|
||||
});
|
||||
|
||||
expect(getExtensionsRegistry).toHaveBeenCalledTimes(1);
|
||||
expect(getExtension).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("throws if a behavior can't be installed, even if its extension was properly found in the registry", async () => {
|
||||
makeTestExtensions(gd);
|
||||
const { project } = makeTestProject(gd);
|
||||
const layout = project.insertNewLayout('MyTestLayout', 0);
|
||||
const eventsList = new gd.EventsList();
|
||||
|
||||
// Get an asset that uses a behavior...
|
||||
mockFn(getAsset).mockImplementationOnce(
|
||||
() => fakeAssetWithFlashBehaviorCustomizations1
|
||||
);
|
||||
|
||||
// ...and this behavior extension is in the registry
|
||||
mockFn(getExtensionsRegistry).mockImplementationOnce(() => ({
|
||||
version: '1.0.0',
|
||||
allTags: [''],
|
||||
extensionShortHeaders: [
|
||||
flashExtensionShortHeader,
|
||||
fireBulletExtensionShortHeader,
|
||||
],
|
||||
}));
|
||||
|
||||
mockFn(getExtension).mockImplementationOnce(
|
||||
() => flashExtensionShortHeader
|
||||
);
|
||||
|
||||
// Verify that, because we use `mockEventsFunctionsExtensionsState`, the
|
||||
// extension won't be loaded, so the behavior won't be installed.
|
||||
await expect(
|
||||
installAsset({
|
||||
assetShortHeader: fakeAssetShortHeader1,
|
||||
events: eventsList,
|
||||
project,
|
||||
objectsContainer: layout,
|
||||
eventsFunctionsExtensionsState: mockEventsFunctionsExtensionsState,
|
||||
})
|
||||
).rejects.toMatchObject({
|
||||
message: 'These behaviors could not be installed: Flash::Flash (Flash)',
|
||||
});
|
||||
|
||||
expect(getExtensionsRegistry).toHaveBeenCalledTimes(1);
|
||||
expect(getExtension).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("throws if an extension can't be installed, even if it was properly found in the registry", async () => {
|
||||
makeTestExtensions(gd);
|
||||
const { project } = makeTestProject(gd);
|
||||
const layout = project.insertNewLayout('MyTestLayout', 0);
|
||||
const eventsList = new gd.EventsList();
|
||||
|
||||
// Get an asset that uses an extension...
|
||||
mockFn(getAsset).mockImplementationOnce(
|
||||
() => fakeAssetWithEventCustomizationsAndFlashExtension1
|
||||
);
|
||||
|
||||
// ...and this extension is in the registry
|
||||
mockFn(getExtensionsRegistry).mockImplementationOnce(() => ({
|
||||
version: '1.0.0',
|
||||
allTags: [''],
|
||||
extensionShortHeaders: [
|
||||
flashExtensionShortHeader,
|
||||
fireBulletExtensionShortHeader,
|
||||
],
|
||||
}));
|
||||
|
||||
mockFn(getExtension).mockImplementationOnce(
|
||||
() => flashExtensionShortHeader
|
||||
);
|
||||
|
||||
// Verify that, because we use `mockEventsFunctionsExtensionsState`, the
|
||||
// extension won't be loaded, so the extension won't be installed.
|
||||
await expect(
|
||||
installAsset({
|
||||
assetShortHeader: fakeAssetShortHeader1,
|
||||
events: eventsList,
|
||||
project,
|
||||
objectsContainer: layout,
|
||||
eventsFunctionsExtensionsState: mockEventsFunctionsExtensionsState,
|
||||
})
|
||||
).rejects.toMatchObject({
|
||||
message: 'These extensions could not be installed: Flash',
|
||||
});
|
||||
|
||||
expect(getExtensionsRegistry).toHaveBeenCalledTimes(1);
|
||||
expect(getExtension).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('install an asset, with a behavior that is already installed', async () => {
|
||||
makeTestExtensions(gd);
|
||||
const { project } = makeTestProject(gd);
|
||||
const layout = project.insertNewLayout('MyTestLayout', 0);
|
||||
const eventsList = new gd.EventsList();
|
||||
|
||||
// Fake an asset with a behavior of type "FakeBehavior::FakeBehavior",
|
||||
// that is installed already.
|
||||
mockFn(getAsset).mockImplementationOnce(
|
||||
() => fakeAssetWithBehaviorCustomizations1
|
||||
);
|
||||
|
||||
// Install the asset
|
||||
await installAsset({
|
||||
assetShortHeader: fakeAssetShortHeader1,
|
||||
events: eventsList,
|
||||
project,
|
||||
objectsContainer: layout,
|
||||
eventsFunctionsExtensionsState: mockEventsFunctionsExtensionsState,
|
||||
});
|
||||
|
||||
// No extensions fetched because the behavior is already installed.
|
||||
expect(getExtension).not.toHaveBeenCalled();
|
||||
expect(getExtensionsRegistry).not.toHaveBeenCalled();
|
||||
|
||||
// Check that the object was created, with the proper behavior:
|
||||
expect(layout.getObjectsCount()).toBe(1);
|
||||
expect(layout.getObjectAt(0).getName()).toBe('PlayerSpaceship');
|
||||
expect(
|
||||
layout
|
||||
.getObjectAt(0)
|
||||
.getAllBehaviorNames()
|
||||
.toJSArray()
|
||||
).toEqual(['MyBehavior']);
|
||||
});
|
||||
});
|
||||
});
|
260
newIDE/app/src/AssetStore/NewObjectDialog.js
Normal file
260
newIDE/app/src/AssetStore/NewObjectDialog.js
Normal file
@@ -0,0 +1,260 @@
|
||||
// @flow
|
||||
import { Trans } from '@lingui/macro';
|
||||
import * as React from 'react';
|
||||
import Dialog from '../UI/Dialog';
|
||||
import FlatButton from '../UI/FlatButton';
|
||||
import ListIcon from '../UI/ListIcon';
|
||||
import Subheader from '../UI/Subheader';
|
||||
import { List, ListItem } from '../UI/List';
|
||||
import Visibility from '@material-ui/icons/Visibility';
|
||||
import VisibilityOff from '@material-ui/icons/VisibilityOff';
|
||||
import {
|
||||
enumerateObjectTypes,
|
||||
type EnumeratedObjectMetadata,
|
||||
} from '../ObjectsList/EnumerateObjects';
|
||||
import HelpButton from '../UI/HelpButton';
|
||||
import { getExperimentalObjects } from '../Hints';
|
||||
import { Line, Column } from '../UI/Grid';
|
||||
import { Tabs, Tab } from '../UI/Tabs';
|
||||
import { AssetStore } from '.';
|
||||
import { type AssetShortHeader } from '../Utils/GDevelopServices/Asset';
|
||||
import EventsFunctionsExtensionsContext from '../EventsFunctionsExtensionsLoader/EventsFunctionsExtensionsContext';
|
||||
import { installAsset } from './InstallAsset';
|
||||
import { AssetDetails } from './AssetDetails';
|
||||
import {
|
||||
type ResourceSource,
|
||||
type ChooseResourceFunction,
|
||||
} from '../ResourcesList/ResourceSource.flow';
|
||||
import { type ResourceExternalEditor } from '../ResourcesList/ResourceExternalEditor.flow';
|
||||
import {
|
||||
sendAssetAddedToProject,
|
||||
sendAssetOpened,
|
||||
} from '../Utils/Analytics/EventSender';
|
||||
import optionalRequire from '../Utils/OptionalRequire';
|
||||
const electron = optionalRequire('electron');
|
||||
|
||||
const ObjectListItem = ({
|
||||
objectMetadata,
|
||||
onClick,
|
||||
}: {|
|
||||
objectMetadata: EnumeratedObjectMetadata,
|
||||
onClick: () => void,
|
||||
|}) => {
|
||||
if (objectMetadata.name === '') {
|
||||
// Base object is an "abstract" object
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<ListItem
|
||||
leftIcon={
|
||||
<ListIcon
|
||||
src={objectMetadata.iconFilename}
|
||||
iconSize={40}
|
||||
isGDevelopIcon
|
||||
/>
|
||||
}
|
||||
key={objectMetadata.name}
|
||||
primaryText={objectMetadata.fullName}
|
||||
secondaryText={objectMetadata.description}
|
||||
secondaryTextLines={2}
|
||||
onClick={onClick}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
type Props = {|
|
||||
project: gdProject,
|
||||
layout: ?gdLayout,
|
||||
objectsContainer: gdObjectsContainer,
|
||||
resourceSources: Array<ResourceSource>,
|
||||
onChooseResource: ChooseResourceFunction,
|
||||
resourceExternalEditors: Array<ResourceExternalEditor>,
|
||||
events: gdEventsList,
|
||||
onClose: () => void,
|
||||
onCreateNewObject: (type: string) => void,
|
||||
onObjectAddedFromAsset: gdObject => void,
|
||||
|};
|
||||
|
||||
export default function NewObjectDialog({
|
||||
project,
|
||||
layout,
|
||||
objectsContainer,
|
||||
resourceSources,
|
||||
onChooseResource,
|
||||
resourceExternalEditors,
|
||||
events,
|
||||
onClose,
|
||||
onCreateNewObject,
|
||||
onObjectAddedFromAsset,
|
||||
}: Props) {
|
||||
const [openedAsset, setOpenedAsset] = React.useState<null | AssetShortHeader>(
|
||||
null
|
||||
);
|
||||
const showAssetStore = !electron;
|
||||
const [currentTab, setCurrentTab] = React.useState(
|
||||
showAssetStore ? 'asset-store' : 'new-object'
|
||||
);
|
||||
const [showExperimental, setShowExperimental] = React.useState(false);
|
||||
const objectMetadata = React.useMemo(() => enumerateObjectTypes(project), [
|
||||
project,
|
||||
]);
|
||||
const experimentalObjectsInformation = getExperimentalObjects();
|
||||
|
||||
const objects = objectMetadata.filter(
|
||||
({ name }) => !experimentalObjectsInformation[name]
|
||||
);
|
||||
const experimentalObjects = objectMetadata.filter(
|
||||
({ name }) => !!experimentalObjectsInformation[name]
|
||||
);
|
||||
|
||||
const [
|
||||
assetBeingInstalled,
|
||||
setAssetBeingInstalled,
|
||||
] = React.useState<?AssetShortHeader>(null);
|
||||
const eventsFunctionsExtensionsState = React.useContext(
|
||||
EventsFunctionsExtensionsContext
|
||||
);
|
||||
const onInstallAsset = React.useCallback(
|
||||
(assetShortHeader: AssetShortHeader) => {
|
||||
setAssetBeingInstalled(assetShortHeader);
|
||||
(async () => {
|
||||
try {
|
||||
const installOutput = await installAsset({
|
||||
assetShortHeader,
|
||||
eventsFunctionsExtensionsState,
|
||||
project,
|
||||
objectsContainer,
|
||||
events,
|
||||
});
|
||||
sendAssetAddedToProject({
|
||||
id: assetShortHeader.id,
|
||||
name: assetShortHeader.name,
|
||||
});
|
||||
console.log('Asset successfully installed.');
|
||||
|
||||
installOutput.createdObjects.forEach(object => {
|
||||
onObjectAddedFromAsset(object);
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error while installing asset:', error);
|
||||
//TODO: Handle error
|
||||
}
|
||||
|
||||
setAssetBeingInstalled(null);
|
||||
})();
|
||||
},
|
||||
[eventsFunctionsExtensionsState, project, objectsContainer, events]
|
||||
);
|
||||
|
||||
return (
|
||||
<Dialog
|
||||
title={<Trans>Add a new object</Trans>}
|
||||
secondaryActions={<HelpButton helpPagePath="/objects" />}
|
||||
actions={[
|
||||
<FlatButton
|
||||
key="close"
|
||||
label={<Trans>Close</Trans>}
|
||||
primary={false}
|
||||
onClick={onClose}
|
||||
/>,
|
||||
]}
|
||||
onRequestClose={onClose}
|
||||
cannotBeDismissed={false}
|
||||
open
|
||||
flexBody
|
||||
noMargin
|
||||
>
|
||||
<Column noMargin expand>
|
||||
{showAssetStore && (
|
||||
<Tabs value={currentTab} onChange={setCurrentTab}>
|
||||
<Tab
|
||||
label={<Trans>Search pre-made objects</Trans>}
|
||||
value="asset-store"
|
||||
/>
|
||||
<Tab
|
||||
label={<Trans>New object from scratch</Trans>}
|
||||
value="new-object"
|
||||
/>
|
||||
</Tabs>
|
||||
)}
|
||||
{currentTab === 'asset-store' && (
|
||||
<AssetStore
|
||||
project={project}
|
||||
objectsContainer={objectsContainer}
|
||||
events={events}
|
||||
onOpenDetails={assetShortHeader => {
|
||||
setOpenedAsset(assetShortHeader);
|
||||
sendAssetOpened({
|
||||
id: assetShortHeader.id,
|
||||
name: assetShortHeader.name,
|
||||
});
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
{currentTab === 'new-object' && (
|
||||
<React.Fragment>
|
||||
<List>
|
||||
{objects.map(objectMetadata => (
|
||||
<ObjectListItem
|
||||
key={objectMetadata.name}
|
||||
objectMetadata={objectMetadata}
|
||||
onClick={() => onCreateNewObject(objectMetadata.name)}
|
||||
/>
|
||||
))}
|
||||
{showExperimental && (
|
||||
<Subheader>
|
||||
Experimental (make sure to read the documentation page)
|
||||
</Subheader>
|
||||
)}
|
||||
{showExperimental &&
|
||||
experimentalObjects.map(objectMetadata => (
|
||||
<ObjectListItem
|
||||
key={objectMetadata.name}
|
||||
objectMetadata={objectMetadata}
|
||||
onClick={() => onCreateNewObject(objectMetadata.name)}
|
||||
/>
|
||||
))}
|
||||
</List>
|
||||
<Line justifyContent="center" alignItems="center">
|
||||
{!showExperimental ? (
|
||||
<FlatButton
|
||||
key="toggle-experimental"
|
||||
icon={<Visibility />}
|
||||
primary={false}
|
||||
onClick={() => setShowExperimental(true)}
|
||||
label={<Trans>Show experimental objects</Trans>}
|
||||
/>
|
||||
) : (
|
||||
<FlatButton
|
||||
key="toggle-experimental"
|
||||
icon={<VisibilityOff />}
|
||||
primary={false}
|
||||
onClick={() => setShowExperimental(false)}
|
||||
label={<Trans>Hide experimental objects</Trans>}
|
||||
/>
|
||||
)}
|
||||
</Line>
|
||||
</React.Fragment>
|
||||
)}
|
||||
</Column>
|
||||
{openedAsset !== null ? (
|
||||
<AssetDetails
|
||||
project={project}
|
||||
layout={layout}
|
||||
objectsContainer={objectsContainer}
|
||||
resourceSources={resourceSources}
|
||||
onChooseResource={onChooseResource}
|
||||
resourceExternalEditors={resourceExternalEditors}
|
||||
assetShortHeader={openedAsset}
|
||||
onAdd={() => onInstallAsset(openedAsset)}
|
||||
onClose={() => setOpenedAsset(null)}
|
||||
canInstall={!assetBeingInstalled}
|
||||
isBeingInstalled={
|
||||
!!assetBeingInstalled && assetBeingInstalled.id === openedAsset.id
|
||||
}
|
||||
/>
|
||||
) : null}
|
||||
</Dialog>
|
||||
);
|
||||
}
|
102
newIDE/app/src/AssetStore/SearchResults.js
Normal file
102
newIDE/app/src/AssetStore/SearchResults.js
Normal file
@@ -0,0 +1,102 @@
|
||||
// @flow
|
||||
import { Trans } from '@lingui/macro';
|
||||
import * as React from 'react';
|
||||
import { AssetCard } from './AssetCard';
|
||||
import { type AssetShortHeader } from '../Utils/GDevelopServices/Asset';
|
||||
import PlaceholderLoader from '../UI/PlaceholderLoader';
|
||||
import PlaceholderError from '../UI/PlaceholderError';
|
||||
import { AutoSizer, Grid } from 'react-virtualized';
|
||||
import EmptyMessage from '../UI/EmptyMessage';
|
||||
|
||||
type Props = {|
|
||||
assetShortHeaders: ?Array<AssetShortHeader>,
|
||||
error: ?Error,
|
||||
onRetry: () => void,
|
||||
onOpenDetails: AssetShortHeader => void,
|
||||
|};
|
||||
|
||||
const styles = {
|
||||
container: { flex: 1 },
|
||||
grid: { overflowX: 'hidden' },
|
||||
};
|
||||
|
||||
export const SearchResults = ({
|
||||
assetShortHeaders,
|
||||
error,
|
||||
onRetry,
|
||||
onOpenDetails,
|
||||
}: Props) => {
|
||||
if (!assetShortHeaders) {
|
||||
if (!error) return <PlaceholderLoader />;
|
||||
else {
|
||||
return (
|
||||
<PlaceholderError onRetry={onRetry}>
|
||||
<Trans>
|
||||
Can't load the results. Verify your internet connection or retry
|
||||
later.
|
||||
</Trans>
|
||||
</PlaceholderError>
|
||||
);
|
||||
}
|
||||
} else if (assetShortHeaders.length === 0) {
|
||||
return (
|
||||
<EmptyMessage>
|
||||
<Trans>
|
||||
No results returned for your search. Try something else, browse the
|
||||
categories or create your object from scratch!
|
||||
</Trans>
|
||||
</EmptyMessage>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div style={styles.container}>
|
||||
<AutoSizer>
|
||||
{({ width, height }) => {
|
||||
if (!width || !height) return null;
|
||||
|
||||
const baseSize = 128;
|
||||
const columnCount = Math.floor((width - 5) / baseSize);
|
||||
const columnWidth = Math.floor(width / columnCount);
|
||||
const rowCount = Math.max(
|
||||
1,
|
||||
Math.ceil(assetShortHeaders.length / columnCount)
|
||||
);
|
||||
|
||||
function cellRenderer({ columnIndex, key, rowIndex, style }) {
|
||||
const indexInList = rowIndex * columnCount + columnIndex;
|
||||
const assetShortHeader =
|
||||
indexInList < assetShortHeaders.length
|
||||
? assetShortHeaders[indexInList]
|
||||
: null;
|
||||
|
||||
return (
|
||||
<div key={key} style={style}>
|
||||
{assetShortHeader ? (
|
||||
<AssetCard
|
||||
size={columnWidth}
|
||||
onOpenDetails={() => onOpenDetails(assetShortHeader)}
|
||||
assetShortHeader={assetShortHeader}
|
||||
/>
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Grid
|
||||
width={width}
|
||||
height={height}
|
||||
columnCount={columnCount}
|
||||
columnWidth={columnWidth}
|
||||
rowHeight={columnWidth}
|
||||
rowCount={rowCount}
|
||||
cellRenderer={cellRenderer}
|
||||
style={styles.grid}
|
||||
/>
|
||||
);
|
||||
}}
|
||||
</AutoSizer>
|
||||
</div>
|
||||
);
|
||||
};
|
99
newIDE/app/src/AssetStore/index.js
Normal file
99
newIDE/app/src/AssetStore/index.js
Normal file
@@ -0,0 +1,99 @@
|
||||
// @flow
|
||||
import * as React from 'react';
|
||||
import SearchBar from '../UI/SearchBar';
|
||||
import { Column, Line } from '../UI/Grid';
|
||||
import Background from '../UI/Background';
|
||||
import ScrollView from '../UI/ScrollView';
|
||||
import { type AssetShortHeader } from '../Utils/GDevelopServices/Asset';
|
||||
import { SearchResults } from './SearchResults';
|
||||
import { FiltersChooser } from './FiltersChooser';
|
||||
import { AssetStoreContext } from './AssetStoreContext';
|
||||
|
||||
const styles = {
|
||||
previewBackground: {
|
||||
background: 'url("res/transparentback.png") repeat',
|
||||
width: 150,
|
||||
display: 'flex',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
},
|
||||
chip: {
|
||||
marginBottom: 2,
|
||||
marginRight: 2,
|
||||
},
|
||||
previewImage: {
|
||||
maxWidth: 150,
|
||||
verticalAlign: 'middle',
|
||||
pointerEvents: 'none',
|
||||
},
|
||||
content: {
|
||||
flex: '1',
|
||||
},
|
||||
searchBar: {
|
||||
// TODO: Can we put this in the search bar by default?
|
||||
flexShrink: 0,
|
||||
},
|
||||
};
|
||||
|
||||
type Props = {
|
||||
project: gdProject,
|
||||
objectsContainer: gdObjectsContainer,
|
||||
events: gdEventsList,
|
||||
onOpenDetails: AssetShortHeader => void,
|
||||
};
|
||||
|
||||
export const AssetStore = ({
|
||||
project,
|
||||
objectsContainer,
|
||||
events,
|
||||
onOpenDetails,
|
||||
}: Props) => {
|
||||
const {
|
||||
filters,
|
||||
searchResults,
|
||||
error,
|
||||
fetchAssetsAndFilters,
|
||||
filtersState,
|
||||
searchText,
|
||||
setSearchText,
|
||||
} = React.useContext(AssetStoreContext);
|
||||
|
||||
React.useEffect(
|
||||
() => {
|
||||
fetchAssetsAndFilters();
|
||||
},
|
||||
[fetchAssetsAndFilters]
|
||||
);
|
||||
|
||||
return (
|
||||
<Column expand noMargin useFullHeight>
|
||||
<SearchBar
|
||||
value={searchText}
|
||||
onChange={setSearchText}
|
||||
onRequestSearch={() => {}}
|
||||
style={styles.searchBar}
|
||||
/>
|
||||
<Line
|
||||
overflow={
|
||||
'hidden' /* Somehow required on Chrome/Firefox to avoid children growing (but not on Safari) */
|
||||
}
|
||||
>
|
||||
<Background noFullHeight noExpand width={250}>
|
||||
<ScrollView>
|
||||
<FiltersChooser
|
||||
allFilters={filters}
|
||||
filtersState={filtersState}
|
||||
error={error}
|
||||
/>
|
||||
</ScrollView>
|
||||
</Background>
|
||||
<SearchResults
|
||||
onOpenDetails={onOpenDetails}
|
||||
onRetry={fetchAssetsAndFilters}
|
||||
error={error}
|
||||
assetShortHeaders={searchResults}
|
||||
/>
|
||||
</Line>
|
||||
</Column>
|
||||
);
|
||||
};
|
@@ -99,7 +99,7 @@ export default class DebuggerContent extends React.Component<Props, State> {
|
||||
toolbarControls: [],
|
||||
renderEditor: () => (
|
||||
<Background>
|
||||
<Column expand noMargin useMaxHeight>
|
||||
<Column expand noMargin useFullHeight>
|
||||
<Line justifyContent="center">
|
||||
<RaisedButton
|
||||
label={<Trans>Refresh</Trans>}
|
||||
|
@@ -147,7 +147,7 @@ export default class EventsFunctionConfigurationEditor extends React.Component<
|
||||
} = this.props;
|
||||
|
||||
return (
|
||||
<Column expand noMargin useMaxHeight>
|
||||
<Column expand noMargin useFullHeight>
|
||||
<Tabs value={this.state.currentTab} onChange={this._chooseTab}>
|
||||
<Tab
|
||||
label={<Trans>Configuration</Trans>}
|
||||
|
@@ -7,13 +7,14 @@ import React, { Component } from 'react';
|
||||
import Dialog from '../UI/Dialog';
|
||||
import FlatButton from '../UI/FlatButton';
|
||||
import CloudDownload from '@material-ui/icons/CloudDownload';
|
||||
import ExtensionsSearch, { addSerializedExtensionToProject } from '.';
|
||||
import ExtensionsSearch from '.';
|
||||
import EventsFunctionsExtensionsContext, {
|
||||
type EventsFunctionsExtensionsState,
|
||||
} from '../EventsFunctionsExtensionsLoader/EventsFunctionsExtensionsContext';
|
||||
import HelpButton from '../UI/HelpButton';
|
||||
import { showErrorBox } from '../UI/Messages/MessageBox';
|
||||
import Window from '../Utils/Window';
|
||||
import { addSerializedExtensionsToProject } from '../AssetStore/InstallAsset';
|
||||
|
||||
type Props = {|
|
||||
project: gdProject,
|
||||
@@ -47,10 +48,10 @@ const importExtension = (
|
||||
if (!answer) return;
|
||||
}
|
||||
|
||||
return addSerializedExtensionToProject(
|
||||
return addSerializedExtensionsToProject(
|
||||
eventsFunctionsExtensionsState,
|
||||
project,
|
||||
serializedExtension
|
||||
[serializedExtension]
|
||||
);
|
||||
});
|
||||
})
|
||||
|
@@ -9,13 +9,11 @@ import {
|
||||
getExtensionsRegistry,
|
||||
type ExtensionsRegistry,
|
||||
type ExtensionShortHeader,
|
||||
type SerializedExtension,
|
||||
getExtension,
|
||||
} from '../Utils/GDevelopServices/Extension';
|
||||
import { List, ListItem } from '../UI/List';
|
||||
import PlaceholderLoader from '../UI/PlaceholderLoader';
|
||||
import ExtensionInstallDialog from './ExtensionInstallDialog';
|
||||
import { unserializeFromJSObject } from '../Utils/Serializer';
|
||||
import { showErrorBox } from '../UI/Messages/MessageBox';
|
||||
import EventsFunctionsExtensionsContext, {
|
||||
type EventsFunctionsExtensionsState,
|
||||
@@ -23,37 +21,7 @@ import EventsFunctionsExtensionsContext, {
|
||||
import PlaceholderError from '../UI/PlaceholderError';
|
||||
import EmptyMessage from '../UI/EmptyMessage';
|
||||
import SearchbarWithChips from '../UI/SearchbarWithChips';
|
||||
|
||||
/**
|
||||
* Add a serialized (JS object) events function extension to the project,
|
||||
* triggering reload of extensions.
|
||||
*/
|
||||
export const addSerializedExtensionToProject = (
|
||||
eventsFunctionsExtensionsState: EventsFunctionsExtensionsState,
|
||||
project: gdProject,
|
||||
serializedExtension: SerializedExtension
|
||||
): Promise<void> => {
|
||||
const { name } = serializedExtension;
|
||||
if (!name)
|
||||
return Promise.reject(new Error('Malformed extension (missing name).'));
|
||||
|
||||
const newEventsFunctionsExtension = project.hasEventsFunctionsExtensionNamed(
|
||||
name
|
||||
)
|
||||
? project.getEventsFunctionsExtension(name)
|
||||
: project.insertNewEventsFunctionsExtension(name, 0);
|
||||
|
||||
unserializeFromJSObject(
|
||||
newEventsFunctionsExtension,
|
||||
serializedExtension,
|
||||
'unserializeFrom',
|
||||
project
|
||||
);
|
||||
|
||||
return eventsFunctionsExtensionsState.loadProjectEventsFunctionsExtensions(
|
||||
project
|
||||
);
|
||||
};
|
||||
import { addSerializedExtensionsToProject } from '../AssetStore/InstallAsset';
|
||||
|
||||
type Props = {|
|
||||
project: gdProject,
|
||||
@@ -157,10 +125,10 @@ export default class ExtensionsSearch extends Component<Props, State> {
|
||||
getExtension(extensionShortHeader)
|
||||
.then(
|
||||
serializedExtension => {
|
||||
return addSerializedExtensionToProject(
|
||||
return addSerializedExtensionsToProject(
|
||||
eventsFunctionsExtensionsState,
|
||||
project,
|
||||
serializedExtension
|
||||
[serializedExtension]
|
||||
).then(() => {
|
||||
this.setState(
|
||||
{
|
||||
|
@@ -28,6 +28,7 @@ import { UnsavedChangesContextProvider } from './UnsavedChangesContext';
|
||||
import { CommandsContextProvider } from '../CommandPalette/CommandsContext';
|
||||
import { create } from 'jss';
|
||||
import rtl from 'jss-rtl';
|
||||
import { AssetStoreStateProvider } from '../AssetStore/AssetStoreContext';
|
||||
|
||||
// Add the rtl plugin to the JSS instance to support RTL languages in material-ui components.
|
||||
const jss = create({
|
||||
@@ -93,14 +94,16 @@ export default class Providers extends React.Component<Props, {||}> {
|
||||
}
|
||||
>
|
||||
<CommandsContextProvider>
|
||||
<EventsFunctionsExtensionsContext.Consumer>
|
||||
{eventsFunctionsExtensionsState =>
|
||||
children({
|
||||
i18n,
|
||||
eventsFunctionsExtensionsState,
|
||||
})
|
||||
}
|
||||
</EventsFunctionsExtensionsContext.Consumer>
|
||||
<AssetStoreStateProvider>
|
||||
<EventsFunctionsExtensionsContext.Consumer>
|
||||
{eventsFunctionsExtensionsState =>
|
||||
children({
|
||||
i18n,
|
||||
eventsFunctionsExtensionsState,
|
||||
})
|
||||
}
|
||||
</EventsFunctionsExtensionsContext.Consumer>
|
||||
</AssetStoreStateProvider>
|
||||
</CommandsContextProvider>
|
||||
</EventsFunctionsExtensionsProvider>
|
||||
)}
|
||||
|
@@ -1,163 +0,0 @@
|
||||
// @flow
|
||||
import { Trans } from '@lingui/macro';
|
||||
import React, { Component } from 'react';
|
||||
import Dialog from '../UI/Dialog';
|
||||
import FlatButton from '../UI/FlatButton';
|
||||
import ListIcon from '../UI/ListIcon';
|
||||
import Subheader from '../UI/Subheader';
|
||||
import { List, ListItem } from '../UI/List';
|
||||
import Visibility from '@material-ui/icons/Visibility';
|
||||
import VisibilityOff from '@material-ui/icons/VisibilityOff';
|
||||
import {
|
||||
enumerateObjectTypes,
|
||||
type EnumeratedObjectMetadata,
|
||||
} from './EnumerateObjects';
|
||||
import HelpButton from '../UI/HelpButton';
|
||||
import { getExperimentalObjects } from '../Hints';
|
||||
import { Line } from '../UI/Grid';
|
||||
|
||||
const ObjectListItem = ({
|
||||
objectMetadata,
|
||||
onClick,
|
||||
}: {|
|
||||
objectMetadata: EnumeratedObjectMetadata,
|
||||
onClick: () => void,
|
||||
|}) => {
|
||||
if (objectMetadata.name === '') {
|
||||
// Base object is an "abstract" object
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<ListItem
|
||||
leftIcon={
|
||||
<ListIcon
|
||||
src={objectMetadata.iconFilename}
|
||||
iconSize={40}
|
||||
isGDevelopIcon
|
||||
/>
|
||||
}
|
||||
key={objectMetadata.name}
|
||||
primaryText={objectMetadata.fullName}
|
||||
secondaryText={objectMetadata.description}
|
||||
secondaryTextLines={2}
|
||||
onClick={onClick}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
type Props = {|
|
||||
project: gdProject,
|
||||
open: boolean,
|
||||
onClose: () => void,
|
||||
onChoose: string => void,
|
||||
|};
|
||||
|
||||
type State = {|
|
||||
objectMetadata: Array<EnumeratedObjectMetadata>,
|
||||
showExperimental: boolean,
|
||||
|};
|
||||
|
||||
export default class NewObjectDialog extends Component<Props, State> {
|
||||
state = { ...this._loadFrom(this.props.project), showExperimental: false };
|
||||
|
||||
_loadFrom(project: gdProject) {
|
||||
if (!project || !project.getCurrentPlatform()) {
|
||||
return { objectMetadata: [] };
|
||||
}
|
||||
|
||||
return {
|
||||
objectMetadata: enumerateObjectTypes(project),
|
||||
};
|
||||
}
|
||||
|
||||
componentWillReceiveProps(newProps: Props) {
|
||||
if (
|
||||
(!this.props.open && newProps.open) ||
|
||||
(newProps.open && this.props.project !== newProps.project)
|
||||
) {
|
||||
this.setState(this._loadFrom(newProps.project));
|
||||
}
|
||||
}
|
||||
|
||||
_showExperimental = (showExperimental: boolean = true) => {
|
||||
this.setState({
|
||||
showExperimental,
|
||||
});
|
||||
};
|
||||
|
||||
render() {
|
||||
const { project, open, onClose, onChoose } = this.props;
|
||||
const { objectMetadata, showExperimental } = this.state;
|
||||
if (!open || !project) return null;
|
||||
|
||||
const experimentalObjectsInformation = getExperimentalObjects();
|
||||
|
||||
const objects = objectMetadata.filter(
|
||||
({ name }) => !experimentalObjectsInformation[name]
|
||||
);
|
||||
const experimentalObjects = objectMetadata.filter(
|
||||
({ name }) => !!experimentalObjectsInformation[name]
|
||||
);
|
||||
|
||||
return (
|
||||
<Dialog
|
||||
title={<Trans>Add a new object</Trans>}
|
||||
secondaryActions={<HelpButton helpPagePath="/objects" />}
|
||||
actions={
|
||||
<FlatButton
|
||||
label={<Trans>Close</Trans>}
|
||||
primary={false}
|
||||
onClick={onClose}
|
||||
/>
|
||||
}
|
||||
onRequestClose={onClose}
|
||||
cannotBeDismissed={false}
|
||||
open={open}
|
||||
noMargin
|
||||
>
|
||||
<List>
|
||||
{objects.map(objectMetadata => (
|
||||
<ObjectListItem
|
||||
key={objectMetadata.name}
|
||||
objectMetadata={objectMetadata}
|
||||
onClick={() => onChoose(objectMetadata.name)}
|
||||
/>
|
||||
))}
|
||||
{showExperimental && (
|
||||
<Subheader>
|
||||
Experimental (make sure to read the documentation page)
|
||||
</Subheader>
|
||||
)}
|
||||
{showExperimental &&
|
||||
experimentalObjects.map(objectMetadata => (
|
||||
<ObjectListItem
|
||||
key={objectMetadata.name}
|
||||
objectMetadata={objectMetadata}
|
||||
onClick={() => onChoose(objectMetadata.name)}
|
||||
/>
|
||||
))}
|
||||
</List>
|
||||
<Line justifyContent="center" alignItems="center">
|
||||
{!showExperimental ? (
|
||||
<FlatButton
|
||||
key="toggle-experimental"
|
||||
icon={<Visibility />}
|
||||
primary={false}
|
||||
onClick={() => this._showExperimental(true)}
|
||||
label={<Trans>Show experimental objects</Trans>}
|
||||
/>
|
||||
) : (
|
||||
<FlatButton
|
||||
key="toggle-experimental"
|
||||
icon={<VisibilityOff />}
|
||||
primary={false}
|
||||
onClick={() => this._showExperimental(false)}
|
||||
label={<Trans>Hide experimental objects</Trans>}
|
||||
/>
|
||||
)}
|
||||
</Line>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
}
|
@@ -9,7 +9,7 @@ import { AutoSizer } from 'react-virtualized';
|
||||
import SortableVirtualizedItemList from '../UI/SortableVirtualizedItemList';
|
||||
import Background from '../UI/Background';
|
||||
import SearchBar from '../UI/SearchBar';
|
||||
import NewObjectDialog from './NewObjectDialog';
|
||||
import NewObjectDialog from '../AssetStore/NewObjectDialog';
|
||||
import VariablesEditorDialog from '../VariablesList/VariablesEditorDialog';
|
||||
import newNameGenerator from '../Utils/NewNameGenerator';
|
||||
import Clipboard from '../Utils/Clipboard';
|
||||
@@ -41,6 +41,11 @@ import {
|
||||
import { type UnsavedChanges } from '../MainFrame/UnsavedChangesContext';
|
||||
import { type HotReloadPreviewButtonProps } from '../HotReload/HotReloadPreviewButton';
|
||||
import { useScreenType } from '../UI/Reponsive/ScreenTypeMeasurer';
|
||||
import {
|
||||
type ResourceSource,
|
||||
type ChooseResourceFunction,
|
||||
} from '../ResourcesList/ResourceSource.flow';
|
||||
import { type ResourceExternalEditor } from '../ResourcesList/ResourceExternalEditor.flow';
|
||||
|
||||
const styles = {
|
||||
listContainer: {
|
||||
@@ -80,7 +85,12 @@ type State = {|
|
||||
|
||||
type Props = {|
|
||||
project: gdProject,
|
||||
layout: ?gdLayout,
|
||||
objectsContainer: gdObjectsContainer,
|
||||
resourceSources: Array<ResourceSource>,
|
||||
onChooseResource: ChooseResourceFunction,
|
||||
resourceExternalEditors: Array<ResourceExternalEditor>,
|
||||
events: gdEventsList,
|
||||
onDeleteObject: (
|
||||
objectWithContext: ObjectWithContext,
|
||||
cb: (boolean) => void
|
||||
@@ -188,6 +198,13 @@ export default class ObjectsList extends React.Component<Props, State> {
|
||||
);
|
||||
};
|
||||
|
||||
_onObjectAddedFromAsset = (object: gdObject) => {
|
||||
const { onObjectCreated } = this.props;
|
||||
|
||||
object.setTags(getStringFromTags(this.props.selectedObjectTags));
|
||||
onObjectCreated(object);
|
||||
};
|
||||
|
||||
onAddNewObject = () => {
|
||||
this.setState({ newObjectDialogOpen: true });
|
||||
};
|
||||
@@ -501,7 +518,16 @@ export default class ObjectsList extends React.Component<Props, State> {
|
||||
};
|
||||
|
||||
render() {
|
||||
const { project, objectsContainer, selectedObjectTags } = this.props;
|
||||
const {
|
||||
project,
|
||||
layout,
|
||||
objectsContainer,
|
||||
resourceSources,
|
||||
onChooseResource,
|
||||
resourceExternalEditors,
|
||||
selectedObjectTags,
|
||||
events,
|
||||
} = this.props;
|
||||
const { searchText, tagEditedObject } = this.state;
|
||||
|
||||
const lists = enumerateObjects(project, objectsContainer);
|
||||
@@ -578,14 +604,20 @@ export default class ObjectsList extends React.Component<Props, State> {
|
||||
/>
|
||||
{this.state.newObjectDialogOpen && (
|
||||
<NewObjectDialog
|
||||
open={this.state.newObjectDialogOpen}
|
||||
onClose={() =>
|
||||
this.setState({
|
||||
newObjectDialogOpen: false,
|
||||
})
|
||||
}
|
||||
onChoose={this.addObject}
|
||||
onCreateNewObject={this.addObject}
|
||||
onObjectAddedFromAsset={this._onObjectAddedFromAsset}
|
||||
project={project}
|
||||
layout={layout}
|
||||
objectsContainer={objectsContainer}
|
||||
events={events}
|
||||
resourceSources={resourceSources}
|
||||
onChooseResource={onChooseResource}
|
||||
resourceExternalEditors={resourceExternalEditors}
|
||||
/>
|
||||
)}
|
||||
{this.state.variablesEditedObject && (
|
||||
|
@@ -1062,6 +1062,11 @@ export default class SceneEditor extends React.Component<Props, State> {
|
||||
)}
|
||||
project={project}
|
||||
objectsContainer={layout}
|
||||
layout={layout}
|
||||
events={layout.getEvents()}
|
||||
resourceSources={resourceSources}
|
||||
resourceExternalEditors={resourceExternalEditors}
|
||||
onChooseResource={onChooseResource}
|
||||
selectedObjectNames={this.state.selectedObjectNames}
|
||||
onEditObject={this.props.onEditObject || this.editObject}
|
||||
onDeleteObject={this._onDeleteObject}
|
||||
|
@@ -16,6 +16,7 @@ type Props = {|
|
||||
children: ?React.Node,
|
||||
maxWidth?: boolean,
|
||||
width?: number | string,
|
||||
/** Sometimes required on Safari */
|
||||
noFullHeight?: boolean,
|
||||
noExpand?: boolean,
|
||||
|};
|
||||
|
@@ -15,6 +15,7 @@ export const Line = props => (
|
||||
alignItems: props.alignItems,
|
||||
justifyContent: props.justifyContent,
|
||||
flex: props.expand ? 1 : undefined,
|
||||
overflow: props.overflow,
|
||||
}}
|
||||
>
|
||||
{props.children}
|
||||
@@ -36,7 +37,10 @@ export const Column = props => (
|
||||
alignItems: props.alignItems || 'stretch',
|
||||
justifyContent: props.justifyContent,
|
||||
flex: props.expand ? 1 : undefined,
|
||||
maxHeight: props.useMaxHeight ? '100%' : undefined,
|
||||
// Setting the min-height to 0 forces the flex to use
|
||||
// all the height (if set to flex: 1) and to *not* grow
|
||||
// larger than the parent.
|
||||
minHeight: props.useFullHeight ? '0' : undefined,
|
||||
}}
|
||||
>
|
||||
{props.children}
|
||||
|
@@ -18,6 +18,8 @@ type Props = {|
|
||||
// Allow to specify that the text should break words
|
||||
overflow?: 'hidden',
|
||||
overflowWrap?: 'break-word',
|
||||
whiteSpace?: 'nowrap',
|
||||
textOverflow?: 'ellipsis',
|
||||
|
||||
// Allow to expand the text
|
||||
flex?: 1,
|
||||
|
@@ -214,6 +214,30 @@ export const sendSubscriptionDialogShown = () => {
|
||||
client.recordEvent('subscription-dialog-shown', {});
|
||||
};
|
||||
|
||||
export const sendAssetOpened = ({
|
||||
id,
|
||||
name,
|
||||
}: {|
|
||||
id: string,
|
||||
name: string,
|
||||
|}) => {
|
||||
if (isDev || !client) return;
|
||||
|
||||
client.recordEvent('asset-opened', { id, name });
|
||||
};
|
||||
|
||||
export const sendAssetAddedToProject = ({
|
||||
id,
|
||||
name,
|
||||
}: {|
|
||||
id: string,
|
||||
name: string,
|
||||
|}) => {
|
||||
if (isDev || !client) return;
|
||||
|
||||
client.recordEvent('asset-added-to-project', { id, name });
|
||||
};
|
||||
|
||||
export const sendChoosePlanClicked = (planId: string | null) => {
|
||||
if (isDev || !client) return;
|
||||
|
||||
|
@@ -37,3 +37,9 @@ export const GDevelopExtensionApi = {
|
||||
? 'https://raw.githubusercontent.com/4ian/GDevelop-extensions/master'
|
||||
: 'https://raw.githubusercontent.com/4ian/GDevelop-extensions/master',
|
||||
};
|
||||
|
||||
export const GDevelopAssetApi = {
|
||||
baseUrl: isDev
|
||||
? 'https://57l4cj31aj.execute-api.us-east-1.amazonaws.com/dev'
|
||||
: 'TODO',
|
||||
};
|
||||
|
142
newIDE/app/src/Utils/GDevelopServices/Asset.js
Normal file
142
newIDE/app/src/Utils/GDevelopServices/Asset.js
Normal file
@@ -0,0 +1,142 @@
|
||||
// @flow
|
||||
import axios from 'axios';
|
||||
import { GDevelopAssetApi } from './ApiConfigs';
|
||||
import semverSatisfies from 'semver/functions/satisfies';
|
||||
|
||||
export type SerializedParameterMetadata = {|
|
||||
codeOnly: boolean,
|
||||
defaultValue: string,
|
||||
description: string,
|
||||
longDescription: string,
|
||||
name: string,
|
||||
optional: boolean,
|
||||
supplementaryInformation: string,
|
||||
type: string, // See ParameterRenderingService for valid types.
|
||||
|};
|
||||
|
||||
export type AssetCustomization =
|
||||
| {|
|
||||
required: boolean,
|
||||
extensionName: string,
|
||||
extensionVersion: string,
|
||||
behaviorName: string,
|
||||
behaviorType: string,
|
||||
properties: Array<SerializedParameterMetadata>,
|
||||
|}
|
||||
| {|
|
||||
required: boolean,
|
||||
events: any /*(serialized gdEventsList)*/,
|
||||
parameters: Array<SerializedParameterMetadata>,
|
||||
extensions: Array<{|
|
||||
extensionName: string,
|
||||
extensionVersion: string,
|
||||
|}>,
|
||||
|};
|
||||
|
||||
export type ObjectAsset = {|
|
||||
object: any /*(serialized gdObject)*/,
|
||||
resources: Array<any /*(serialized gdResource)*/>,
|
||||
customization: Array<AssetCustomization>,
|
||||
|};
|
||||
|
||||
export type AssetShortHeader = {|
|
||||
id: string,
|
||||
name: string,
|
||||
shortDescription: string,
|
||||
previewImageUrls: Array<string>,
|
||||
tags: Array<string>,
|
||||
|};
|
||||
|
||||
export type AssetHeader = {|
|
||||
...AssetShortHeader,
|
||||
gdevelopVersion: string,
|
||||
version: string,
|
||||
description: string,
|
||||
|};
|
||||
|
||||
export type Asset = {|
|
||||
...AssetHeader,
|
||||
authors: Array<string>,
|
||||
license: string,
|
||||
objectAssets: Array<ObjectAsset>,
|
||||
|};
|
||||
|
||||
export type TagsTreeNode = {|
|
||||
name: string,
|
||||
children: Array<TagsTreeNode>,
|
||||
allChildrenTags: Array<string>,
|
||||
|};
|
||||
|
||||
export type Filters = {|
|
||||
allTags: Array<string>,
|
||||
defaultTags: Array<string>,
|
||||
tagsTree: Array<TagsTreeNode>,
|
||||
|};
|
||||
|
||||
export type AllAssets = {|
|
||||
assetShortHeaders: Array<AssetShortHeader>,
|
||||
filters: Filters,
|
||||
|};
|
||||
|
||||
export type Resource = {|
|
||||
url: string,
|
||||
type: string,
|
||||
tags: Array<string>,
|
||||
|};
|
||||
|
||||
export type AllResources = {|
|
||||
resources: Array<Resource>,
|
||||
filters: Filters,
|
||||
|};
|
||||
|
||||
|
||||
/** Check if the IDE version, passed as argument, satisfy the version required by the asset. */
|
||||
export const isCompatibleWithAsset = (
|
||||
ideVersion: string,
|
||||
assetHeader: AssetHeader
|
||||
) =>
|
||||
assetHeader.gdevelopVersion
|
||||
? semverSatisfies(ideVersion, assetHeader.gdevelopVersion, {
|
||||
includePrerelease: true,
|
||||
})
|
||||
: true;
|
||||
|
||||
export const listAllAssets = (): Promise<AllAssets> => {
|
||||
return axios
|
||||
.get(`${GDevelopAssetApi.baseUrl}/asset`)
|
||||
.then(response => response.data)
|
||||
.then(({ assetShortHeadersUrl, filtersUrl }) => {
|
||||
return Promise.all([
|
||||
axios.get(assetShortHeadersUrl).then(response => response.data),
|
||||
axios.get(filtersUrl).then(response => response.data),
|
||||
]).then(([assetShortHeaders, filters]) => ({
|
||||
assetShortHeaders,
|
||||
filters,
|
||||
}));
|
||||
});
|
||||
};
|
||||
|
||||
export const getAsset = (
|
||||
assetShortHeader: AssetShortHeader
|
||||
): Promise<Asset> => {
|
||||
return axios
|
||||
.get(`${GDevelopAssetApi.baseUrl}/asset/${assetShortHeader.id}`)
|
||||
.then(response => response.data)
|
||||
.then(({ assetUrl }) => axios.get(assetUrl))
|
||||
.then(response => response.data);
|
||||
};
|
||||
|
||||
export const listAllResources = (): Promise<AllResources> => {
|
||||
return axios
|
||||
.get(`${GDevelopAssetApi.baseUrl}/resource`)
|
||||
.then(response => response.data)
|
||||
.then(({ resourcesUrl, filtersUrl }) => {
|
||||
return Promise.all([
|
||||
axios.get(resourcesUrl).then(response => response.data),
|
||||
axios.get(filtersUrl).then(response => response.data),
|
||||
]).then(([resources, filters]) => ({
|
||||
resources,
|
||||
filters,
|
||||
}));
|
||||
});
|
||||
};
|
@@ -7,7 +7,12 @@ import {
|
||||
import { type Profile } from '../Utils/GDevelopServices/Authentification';
|
||||
import { type Release } from '../Utils/GDevelopServices/Release';
|
||||
import { type Build } from '../Utils/GDevelopServices/Build';
|
||||
import { type ExtensionShortHeader } from '../Utils/GDevelopServices/Extension';
|
||||
import { type UserProfile } from '../Profile/UserProfileContext';
|
||||
import {
|
||||
type AssetShortHeader,
|
||||
type Asset,
|
||||
} from '../Utils/GDevelopServices/Asset';
|
||||
|
||||
export const profileForIndieUser: Profile = {
|
||||
uid: 'indie-user',
|
||||
@@ -217,3 +222,387 @@ export const completeWebBuild: Build = {
|
||||
s3Key: 'game-12345',
|
||||
updatedAt: Date.now(),
|
||||
};
|
||||
|
||||
const spaceshipSerializedObject = {
|
||||
name: 'PlayerSpaceship',
|
||||
tags: '',
|
||||
type: 'Sprite',
|
||||
updateIfNotVisible: false,
|
||||
variables: [
|
||||
{
|
||||
name: 'State',
|
||||
value: 'Spaceship',
|
||||
},
|
||||
],
|
||||
behaviors: [],
|
||||
animations: [
|
||||
{
|
||||
name: 'SpaceshipIdle',
|
||||
useMultipleDirections: false,
|
||||
directions: [
|
||||
{
|
||||
looping: true,
|
||||
timeBetweenFrames: 0.08,
|
||||
sprites: [
|
||||
{
|
||||
hasCustomCollisionMask: false,
|
||||
image: 'player-ship1.png',
|
||||
points: [],
|
||||
originPoint: {
|
||||
name: 'origine',
|
||||
x: 0,
|
||||
y: 0,
|
||||
},
|
||||
centerPoint: {
|
||||
automatic: true,
|
||||
name: 'centre',
|
||||
x: 0,
|
||||
y: 0,
|
||||
},
|
||||
customCollisionMask: [],
|
||||
},
|
||||
{
|
||||
hasCustomCollisionMask: false,
|
||||
image: 'player-ship2.png',
|
||||
points: [],
|
||||
originPoint: {
|
||||
name: 'origine',
|
||||
x: 0,
|
||||
y: 0,
|
||||
},
|
||||
centerPoint: {
|
||||
automatic: true,
|
||||
name: 'centre',
|
||||
x: 0,
|
||||
y: 0,
|
||||
},
|
||||
customCollisionMask: [],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const spaceshipSerializedResources = [
|
||||
{
|
||||
alwaysLoaded: false,
|
||||
file: 'https://example.com/player-ship1.png',
|
||||
kind: 'image',
|
||||
metadata: '',
|
||||
name: 'player-ship1.png',
|
||||
smoothed: false,
|
||||
userAdded: false,
|
||||
},
|
||||
{
|
||||
alwaysLoaded: false,
|
||||
file: 'https://example.com/player-ship2.png',
|
||||
kind: 'image',
|
||||
metadata: '',
|
||||
name: 'player-ship2.png',
|
||||
smoothed: false,
|
||||
userAdded: false,
|
||||
},
|
||||
];
|
||||
|
||||
export const fakeAsset1: Asset = {
|
||||
id: '123',
|
||||
name: 'My spaceship',
|
||||
shortDescription:
|
||||
'A spaceship that can be moved with the keyboard or by touching the screen',
|
||||
description: "A very nice way to start a shoot'em up.",
|
||||
previewImageUrls: ['res/GD-logo.png'],
|
||||
gdevelopVersion: '5.0.0-beta100',
|
||||
version: '1.0.0',
|
||||
authors: ['test author'],
|
||||
license: 'MIT',
|
||||
tags: ['space shooter', 'tag2'],
|
||||
objectAssets: [
|
||||
{
|
||||
object: spaceshipSerializedObject,
|
||||
resources: spaceshipSerializedResources,
|
||||
customization: [],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
export const fakeAssetWithBehaviorCustomizations1: Asset = {
|
||||
id: '123',
|
||||
name: 'My spaceship',
|
||||
shortDescription:
|
||||
'A spaceship that can be moved with the keyboard or by touching the screen',
|
||||
description: "A very nice way to start a shoot'em up.",
|
||||
previewImageUrls: ['res/GD-logo.png'],
|
||||
gdevelopVersion: '5.0.0-beta100',
|
||||
version: '1.0.0',
|
||||
authors: ['test author'],
|
||||
license: 'MIT',
|
||||
tags: ['space shooter', 'tag2'],
|
||||
objectAssets: [
|
||||
{
|
||||
object: spaceshipSerializedObject,
|
||||
resources: spaceshipSerializedResources,
|
||||
customization: [
|
||||
{
|
||||
behaviorName: 'MyBehavior',
|
||||
behaviorType: 'FakeBehavior::FakeBehavior',
|
||||
required: true,
|
||||
extensionName: 'FakeBehavior',
|
||||
extensionVersion: '1.0.0',
|
||||
properties: [
|
||||
{
|
||||
codeOnly: false,
|
||||
description: 'Example of a parameter',
|
||||
longDescription: '',
|
||||
supplementaryInformation: '',
|
||||
optional: false,
|
||||
type: 'string',
|
||||
name: 'property1',
|
||||
defaultValue: 'Overriden value',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
export const fakeAssetWithUnknownBehaviorCustomizations1: Asset = {
|
||||
id: '123',
|
||||
name: 'My spaceship',
|
||||
shortDescription:
|
||||
'A spaceship that can be moved with the keyboard or by touching the screen',
|
||||
description: "A very nice way to start a shoot'em up.",
|
||||
previewImageUrls: ['res/GD-logo.png'],
|
||||
gdevelopVersion: '5.0.0-beta100',
|
||||
version: '1.0.0',
|
||||
authors: ['test author'],
|
||||
license: 'MIT',
|
||||
tags: ['space shooter', 'tag2'],
|
||||
objectAssets: [
|
||||
{
|
||||
object: spaceshipSerializedObject,
|
||||
resources: spaceshipSerializedResources,
|
||||
customization: [
|
||||
{
|
||||
behaviorName: 'MyUnknownBehavior',
|
||||
behaviorType: 'UnknownBehavior::UnknownBehavior',
|
||||
required: true,
|
||||
extensionName: 'UnknownBehavior',
|
||||
extensionVersion: '1.0.0',
|
||||
properties: [],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
export const fakeAssetWithFlashBehaviorCustomizations1: Asset = {
|
||||
id: '123',
|
||||
name: 'My spaceship',
|
||||
shortDescription:
|
||||
'A spaceship that can be moved with the keyboard or by touching the screen',
|
||||
description: "A very nice way to start a shoot'em up.",
|
||||
previewImageUrls: ['res/GD-logo.png'],
|
||||
gdevelopVersion: '5.0.0-beta100',
|
||||
version: '1.0.0',
|
||||
authors: ['test author'],
|
||||
license: 'MIT',
|
||||
tags: ['space shooter', 'tag2'],
|
||||
objectAssets: [
|
||||
{
|
||||
object: spaceshipSerializedObject,
|
||||
resources: spaceshipSerializedResources,
|
||||
customization: [
|
||||
{
|
||||
behaviorName: 'MyFlashBehavior',
|
||||
behaviorType: 'Flash::Flash',
|
||||
required: true,
|
||||
extensionName: 'Flash',
|
||||
extensionVersion: '1.0.0',
|
||||
properties: [],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
export const fakeAssetWithEventCustomizationsAndFlashExtension1: Asset = {
|
||||
id: '123',
|
||||
name: 'My spaceship',
|
||||
shortDescription:
|
||||
'A spaceship that can be moved with the keyboard or by touching the screen',
|
||||
description: "A very nice way to start a shoot'em up.",
|
||||
previewImageUrls: ['res/GD-logo.png'],
|
||||
gdevelopVersion: '5.0.0-beta100',
|
||||
authors: ['test author'],
|
||||
license: 'MIT',
|
||||
version: '1.0.0',
|
||||
tags: ['space shooter', 'tag2'],
|
||||
objectAssets: [
|
||||
{
|
||||
object: spaceshipSerializedObject,
|
||||
resources: spaceshipSerializedResources,
|
||||
customization: [
|
||||
{
|
||||
required: true,
|
||||
extensions: [
|
||||
{
|
||||
extensionName: 'Flash', // Not really used in events, just for tests.
|
||||
extensionVersion: '1.0.0',
|
||||
},
|
||||
],
|
||||
events: [
|
||||
{
|
||||
disabled: false,
|
||||
folded: false,
|
||||
type: 'BuiltinCommonInstructions::Standard',
|
||||
conditions: [
|
||||
{
|
||||
type: { inverted: false, value: 'VarScene' },
|
||||
parameters: [
|
||||
'Counter',
|
||||
'<',
|
||||
'TEXT_TO_REPLACE + PlayerSpaceship.Variable(test)',
|
||||
],
|
||||
subInstructions: [],
|
||||
},
|
||||
],
|
||||
actions: [
|
||||
{
|
||||
type: { inverted: false, value: 'ModVarScene' },
|
||||
parameters: [
|
||||
'Counter',
|
||||
'=',
|
||||
'TEXT_TO_REPLACE + PlayerSpaceship.Variable(test2)',
|
||||
],
|
||||
subInstructions: [],
|
||||
},
|
||||
],
|
||||
events: [],
|
||||
},
|
||||
],
|
||||
parameters: [
|
||||
{
|
||||
codeOnly: false,
|
||||
description: 'Example of a parameter',
|
||||
longDescription: '',
|
||||
supplementaryInformation: '',
|
||||
optional: false,
|
||||
type: 'string',
|
||||
name: 'TEXT_TO_REPLACE',
|
||||
defaultValue: '3',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
export const fakeAssetWithEventCustomizationsAndUnknownExtension1: Asset = {
|
||||
id: '123',
|
||||
name: 'My spaceship',
|
||||
shortDescription:
|
||||
'A spaceship that can be moved with the keyboard or by touching the screen',
|
||||
description: "A very nice way to start a shoot'em up.",
|
||||
previewImageUrls: ['res/GD-logo.png'],
|
||||
gdevelopVersion: '5.0.0-beta100',
|
||||
authors: ['test author'],
|
||||
license: 'MIT',
|
||||
version: '1.0.0',
|
||||
tags: ['space shooter', 'tag2'],
|
||||
objectAssets: [
|
||||
{
|
||||
object: spaceshipSerializedObject,
|
||||
resources: spaceshipSerializedResources,
|
||||
customization: [
|
||||
{
|
||||
required: true,
|
||||
extensions: [
|
||||
{
|
||||
extensionName: 'UnknownExtension', // Not really used in events, just for tests.
|
||||
extensionVersion: '1.0.0',
|
||||
},
|
||||
],
|
||||
events: [
|
||||
{
|
||||
disabled: false,
|
||||
folded: false,
|
||||
type: 'BuiltinCommonInstructions::Standard',
|
||||
conditions: [],
|
||||
actions: [],
|
||||
events: [],
|
||||
},
|
||||
],
|
||||
parameters: [
|
||||
{
|
||||
codeOnly: false,
|
||||
description: 'Example of a parameter',
|
||||
longDescription: '',
|
||||
supplementaryInformation: '',
|
||||
optional: false,
|
||||
type: 'string',
|
||||
name: 'EXAMPLE_PARAMETER',
|
||||
defaultValue: 'Hello World',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
export const fakeAssetShortHeader1: AssetShortHeader = {
|
||||
id: '123',
|
||||
name: 'My spaceship',
|
||||
shortDescription:
|
||||
'A spaceship that can be moved with the keyboard or by touching the screen',
|
||||
previewImageUrls: ['res/GD-logo.png'],
|
||||
tags: ['space shooter', 'tag2'],
|
||||
};
|
||||
|
||||
export const fakeAssetShortHeader2: AssetShortHeader = {
|
||||
id: '456',
|
||||
name: 'Zombie',
|
||||
shortDescription: 'A zombie attacking the player and wandering around.',
|
||||
previewImageUrls: ['res/GD-logo.png'],
|
||||
tags: ['survival', 'tag2'],
|
||||
};
|
||||
|
||||
export const fakeAssetShortHeader3: AssetShortHeader = {
|
||||
id: '789',
|
||||
name: 'Sword',
|
||||
shortDescription: 'A small sword.',
|
||||
previewImageUrls: ['res/GD-logo.png'],
|
||||
tags: ['medieval', 'tag2'],
|
||||
};
|
||||
|
||||
export const fireBulletExtensionShortHeader: ExtensionShortHeader = {
|
||||
shortDescription:
|
||||
'Allow the object to fire bullets, with customizable speed, angle and fire rate.',
|
||||
extensionNamespace: '',
|
||||
fullName: 'Fire bullets',
|
||||
name: 'FireBullet',
|
||||
version: '0.0.2',
|
||||
url: 'Extensions/FireBullet.json',
|
||||
headerUrl: 'Extensions/FireBullet-header.json',
|
||||
tags: 'fire, bullets, spawn, firerate',
|
||||
eventsBasedBehaviorsCount: 1,
|
||||
eventsFunctionsCount: 0,
|
||||
};
|
||||
|
||||
export const flashExtensionShortHeader: ExtensionShortHeader = {
|
||||
shortDescription:
|
||||
'Make the object flash (blink) for a period of time, so that it is alternately visible and invisible.\nTrigger the effect by using the Flash action.',
|
||||
extensionNamespace: '',
|
||||
fullName: 'Flash (blink)',
|
||||
name: 'Flash',
|
||||
version: '1.0.0',
|
||||
url: 'Extensions/Flash.json',
|
||||
headerUrl: 'Extensions/Flash-header.json',
|
||||
tags: 'flash, blink, visible, invisible, hit, damage',
|
||||
eventsBasedBehaviorsCount: 1,
|
||||
eventsFunctionsCount: 0,
|
||||
};
|
||||
|
@@ -95,6 +95,70 @@ export const makeTestExtensions = (gd: libGDevelop) => {
|
||||
platform.addNewExtension(extension);
|
||||
extension.delete(); // Release the extension as it was copied inside gd.JsPlatform
|
||||
}
|
||||
{
|
||||
const extension = new gd.PlatformExtension();
|
||||
extension.setExtensionInformation(
|
||||
'FakeBehavior',
|
||||
'Fake extension with a fake behavior',
|
||||
'A fake extension with a fake behavior containing 2 properties.',
|
||||
'',
|
||||
'MIT'
|
||||
);
|
||||
const fakeBehavior = new gd.BehaviorJsImplementation();
|
||||
// $FlowExpectedError - ignore Flow warning as we're creating a behavior
|
||||
fakeBehavior.updateProperty = function(
|
||||
behaviorContent,
|
||||
propertyName,
|
||||
newValue
|
||||
) {
|
||||
if (propertyName === 'property1') {
|
||||
behaviorContent.setStringAttribute('property1', newValue);
|
||||
return true;
|
||||
}
|
||||
if (propertyName === 'property2') {
|
||||
behaviorContent.setBoolAttribute('property2', newValue === '1');
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
};
|
||||
// $FlowExpectedError - ignore Flow warning as we're creating a behavior
|
||||
fakeBehavior.getProperties = function(behaviorContent) {
|
||||
var behaviorProperties = new gd.MapStringPropertyDescriptor();
|
||||
|
||||
behaviorProperties
|
||||
.getOrCreate('property1')
|
||||
.setValue(behaviorContent.getStringAttribute('property1'));
|
||||
behaviorProperties
|
||||
.getOrCreate('property2')
|
||||
.setValue(
|
||||
behaviorContent.getBoolAttribute('property2') ? 'true' : 'false'
|
||||
)
|
||||
.setType('Boolean');
|
||||
|
||||
return behaviorProperties;
|
||||
};
|
||||
// $FlowExpectedError - ignore Flow warning as we're creating a behavior
|
||||
fakeBehavior.initializeContent = function(behaviorContent) {
|
||||
behaviorContent.setStringAttribute('property1', 'Initial value 1');
|
||||
behaviorContent.setBoolAttribute('property2', true);
|
||||
};
|
||||
|
||||
extension.addBehavior(
|
||||
'FakeBehavior',
|
||||
'Fake behavior with two properties',
|
||||
'FakeBehavior', // Default name is the name
|
||||
'A fake behavior with two properties.',
|
||||
'',
|
||||
'res/function24.png',
|
||||
'FakeBehavior', // Class name is the name, actually unused
|
||||
fakeBehavior,
|
||||
new gd.BehaviorsSharedData()
|
||||
);
|
||||
|
||||
platform.addNewExtension(extension);
|
||||
extension.delete(); // Release the extension as it was copied inside gd.JsPlatform
|
||||
}
|
||||
{
|
||||
const extension = new gd.PlatformExtension();
|
||||
extension.setExtensionInformation(
|
||||
@@ -105,6 +169,21 @@ export const makeTestExtensions = (gd: libGDevelop) => {
|
||||
'MIT'
|
||||
);
|
||||
const fakeBehavior = new gd.BehaviorJsImplementation();
|
||||
// $FlowExpectedError - ignore Flow warning as we're creating a behavior
|
||||
fakeBehavior.updateProperty = function(
|
||||
behaviorContent,
|
||||
propertyName,
|
||||
newValue
|
||||
) {
|
||||
return false;
|
||||
};
|
||||
// $FlowExpectedError - ignore Flow warning as we're creating a behavior
|
||||
fakeBehavior.getProperties = function(behaviorContent) {
|
||||
var behaviorProperties = new gd.MapStringPropertyDescriptor();
|
||||
return behaviorProperties;
|
||||
};
|
||||
// $FlowExpectedError - ignore Flow warning as we're creating a behavior
|
||||
fakeBehavior.initializeContent = function(behaviorContent) {};
|
||||
|
||||
extension
|
||||
.addBehavior(
|
||||
|
@@ -99,6 +99,11 @@ import {
|
||||
completeCordovaBuild,
|
||||
completeElectronBuild,
|
||||
completeWebBuild,
|
||||
fakeAssetShortHeader1,
|
||||
fakeAssetShortHeader2,
|
||||
fakeAssetShortHeader3,
|
||||
fakeAssetWithBehaviorCustomizations1,
|
||||
fakeAssetWithEventCustomizationsAndFlashExtension1,
|
||||
} from '../fixtures/GDevelopServicesTestData';
|
||||
import debuggerGameDataDump from '../fixtures/DebuggerGameDataDump.json';
|
||||
import profilerOutput from '../fixtures/ProfilerOutputsTestData.json';
|
||||
@@ -152,7 +157,7 @@ import EffectsList from '../EffectsList';
|
||||
import SubscriptionPendingDialog from '../Profile/SubscriptionPendingDialog';
|
||||
import Dialog from '../UI/Dialog';
|
||||
import MiniToolbar, { MiniToolbarText } from '../UI/MiniToolbar';
|
||||
import NewObjectDialog from '../ObjectsList/NewObjectDialog';
|
||||
import NewObjectDialog from '../AssetStore/NewObjectDialog';
|
||||
import { Column, Line } from '../UI/Grid';
|
||||
import DragAndDropTestBed from './DragAndDropTestBed';
|
||||
import EditorMosaic from '../UI/EditorMosaic';
|
||||
@@ -200,6 +205,16 @@ import HotReloadPreviewButton, {
|
||||
type HotReloadPreviewButtonProps,
|
||||
} from '../HotReload/HotReloadPreviewButton';
|
||||
import HotReloadLogsDialog from '../HotReload/HotReloadLogsDialog';
|
||||
import { AssetStore } from '../AssetStore';
|
||||
import { AssetStoreStateProvider } from '../AssetStore/AssetStoreContext';
|
||||
import ScrollView from '../UI/ScrollView';
|
||||
|
||||
import '../UI/Theme/Global.css';
|
||||
import { AssetCard } from '../AssetStore/AssetCard';
|
||||
import { useFilters } from '../AssetStore/FiltersChooser';
|
||||
import { SearchResults } from '../AssetStore/SearchResults';
|
||||
import { AssetDetails } from '../AssetStore/AssetDetails';
|
||||
import CustomizationFields from '../AssetStore/CustomizationFields';
|
||||
|
||||
configureActions({
|
||||
depth: 2,
|
||||
@@ -735,6 +750,33 @@ storiesOf('UI Building Blocks/SearchBar', module)
|
||||
/>
|
||||
));
|
||||
|
||||
storiesOf('UI Building Blocks/Layout/Grid', module)
|
||||
.addDecorator(muiDecorator)
|
||||
.add('Line and ScrollView in a fixed height container', () => (
|
||||
<FixedHeightFlexContainer height={100}>
|
||||
<Column expand>
|
||||
<Line overflow="hidden">
|
||||
<ScrollView>
|
||||
<Text>123</Text>
|
||||
<Text>456</Text>
|
||||
<Text>789</Text>
|
||||
<Text>123</Text>
|
||||
<Text>456</Text>
|
||||
<Text>789</Text>
|
||||
</ScrollView>
|
||||
<ScrollView>
|
||||
<Text>123</Text>
|
||||
<Text>456</Text>
|
||||
<Text>789</Text>
|
||||
<Text>123</Text>
|
||||
<Text>456</Text>
|
||||
<Text>789</Text>
|
||||
</ScrollView>
|
||||
</Line>
|
||||
</Column>
|
||||
</FixedHeightFlexContainer>
|
||||
));
|
||||
|
||||
storiesOf('UI Building Blocks/Layout/ResponsiveLineStackLayout', module)
|
||||
.addDecorator(muiDecorator)
|
||||
.add('Default', () => (
|
||||
@@ -1585,6 +1627,11 @@ storiesOf('UI Building Blocks/ClosableTabs', module)
|
||||
getThumbnail={() => 'res/unknown32.png'}
|
||||
project={testProject.project}
|
||||
objectsContainer={testProject.testLayout}
|
||||
layout={testProject.testLayout}
|
||||
events={testProject.testLayout.getEvents()}
|
||||
resourceSources={[]}
|
||||
onChooseResource={() => Promise.reject('unimplemented')}
|
||||
resourceExternalEditors={fakeResourceExternalEditors}
|
||||
onEditObject={action('On edit object')}
|
||||
selectedObjectNames={[]}
|
||||
selectedObjectTags={[]}
|
||||
@@ -3238,6 +3285,11 @@ storiesOf('ObjectsList', module)
|
||||
getThumbnail={() => 'res/unknown32.png'}
|
||||
project={testProject.project}
|
||||
objectsContainer={testProject.testLayout}
|
||||
layout={testProject.testLayout}
|
||||
events={testProject.testLayout.getEvents()}
|
||||
resourceSources={[]}
|
||||
onChooseResource={() => Promise.reject('unimplemented')}
|
||||
resourceExternalEditors={fakeResourceExternalEditors}
|
||||
onEditObject={action('On edit object')}
|
||||
onObjectCreated={action('On object created')}
|
||||
selectedObjectNames={[]}
|
||||
@@ -3262,6 +3314,11 @@ storiesOf('ObjectsList', module)
|
||||
getThumbnail={() => 'res/unknown32.png'}
|
||||
project={testProject.project}
|
||||
objectsContainer={testProject.testLayout}
|
||||
layout={testProject.testLayout}
|
||||
events={testProject.testLayout.getEvents()}
|
||||
resourceSources={[]}
|
||||
onChooseResource={() => Promise.reject('unimplemented')}
|
||||
resourceExternalEditors={fakeResourceExternalEditors}
|
||||
onEditObject={action('On edit object')}
|
||||
onObjectCreated={action('On object created')}
|
||||
selectedObjectNames={[]}
|
||||
@@ -4322,10 +4379,18 @@ storiesOf('NewObjectDialog', module)
|
||||
.addDecorator(muiDecorator)
|
||||
.add('default', () => (
|
||||
<NewObjectDialog
|
||||
open
|
||||
project={testProject.project}
|
||||
onClose={action('close')}
|
||||
onChoose={action('choose')}
|
||||
layout={testProject.testLayout}
|
||||
onClose={action('onClose')}
|
||||
onCreateNewObject={action('onCreateNewObject')}
|
||||
events={testProject.testLayout.getEvents()}
|
||||
objectsContainer={testProject.testLayout}
|
||||
resourceExternalEditors={fakeResourceExternalEditors}
|
||||
onChooseResource={() => {
|
||||
action('onChooseResource');
|
||||
return Promise.reject();
|
||||
}}
|
||||
resourceSources={[]}
|
||||
/>
|
||||
));
|
||||
|
||||
@@ -4432,3 +4497,139 @@ storiesOf('HotReloadLogsDialog', module)
|
||||
onLaunchNewPreview={() => {}}
|
||||
/>
|
||||
));
|
||||
|
||||
storiesOf('AssetStore', module)
|
||||
.addDecorator(muiDecorator)
|
||||
.add('default', () => (
|
||||
<FixedHeightFlexContainer height={400}>
|
||||
<AssetStoreStateProvider>
|
||||
<AssetStore
|
||||
onOpenDetails={action('onOpenDetails')}
|
||||
events={testProject.testLayout.getEvents()}
|
||||
project={testProject.project}
|
||||
objectsContainer={testProject.testLayout}
|
||||
/>
|
||||
</AssetStoreStateProvider>
|
||||
</FixedHeightFlexContainer>
|
||||
));
|
||||
|
||||
storiesOf('AssetStore/SearchResults', module)
|
||||
.addDecorator(muiDecorator)
|
||||
.add('loading', () => (
|
||||
<SearchResults
|
||||
error={null}
|
||||
assetShortHeaders={null}
|
||||
onRetry={action('onRetry')}
|
||||
onOpenDetails={action('onOpenDetails')}
|
||||
/>
|
||||
))
|
||||
.add('errored', () => (
|
||||
<SearchResults
|
||||
error={new Error('Fake error')}
|
||||
assetShortHeaders={null}
|
||||
onRetry={action('onRetry')}
|
||||
onOpenDetails={action('onOpenDetails')}
|
||||
/>
|
||||
))
|
||||
.add('no results', () => (
|
||||
<SearchResults
|
||||
error={null}
|
||||
assetShortHeaders={[]}
|
||||
onRetry={action('onRetry')}
|
||||
onOpenDetails={action('onOpenDetails')}
|
||||
/>
|
||||
))
|
||||
.add('some results', () => (
|
||||
<SearchResults
|
||||
error={null}
|
||||
assetShortHeaders={[
|
||||
fakeAssetShortHeader1,
|
||||
fakeAssetShortHeader2,
|
||||
fakeAssetShortHeader3,
|
||||
]}
|
||||
onRetry={action('onRetry')}
|
||||
onOpenDetails={action('onOpenDetails')}
|
||||
/>
|
||||
));
|
||||
|
||||
storiesOf('AssetStore/AssetCard', module)
|
||||
.addDecorator(muiDecorator)
|
||||
.add('default', () => (
|
||||
<AssetCard
|
||||
size={128}
|
||||
onOpenDetails={action('onOpenDetails')}
|
||||
assetShortHeader={fakeAssetShortHeader1}
|
||||
/>
|
||||
));
|
||||
|
||||
storiesOf('AssetStore/AssetDetails', module)
|
||||
.addDecorator(paperDecorator)
|
||||
.addDecorator(muiDecorator)
|
||||
.add('default', () => (
|
||||
<AssetDetails
|
||||
canInstall={true}
|
||||
isBeingInstalled={false}
|
||||
onAdd={action('onAdd')}
|
||||
onClose={action('onClose')}
|
||||
assetShortHeader={fakeAssetShortHeader1}
|
||||
project={testProject.project}
|
||||
objectsContainer={testProject.testLayout}
|
||||
layout={testProject.testLayout}
|
||||
resourceExternalEditors={fakeResourceExternalEditors}
|
||||
onChooseResource={() => {
|
||||
action('onChooseResource');
|
||||
return Promise.reject();
|
||||
}}
|
||||
resourceSources={[]}
|
||||
/>
|
||||
))
|
||||
.add('being installed', () => (
|
||||
<AssetDetails
|
||||
canInstall={false}
|
||||
isBeingInstalled={true}
|
||||
onAdd={action('onAdd')}
|
||||
onClose={action('onClose')}
|
||||
assetShortHeader={fakeAssetShortHeader1}
|
||||
project={testProject.project}
|
||||
objectsContainer={testProject.testLayout}
|
||||
layout={testProject.testLayout}
|
||||
resourceExternalEditors={fakeResourceExternalEditors}
|
||||
onChooseResource={() => {
|
||||
action('onChooseResource');
|
||||
return Promise.reject();
|
||||
}}
|
||||
resourceSources={[]}
|
||||
/>
|
||||
));
|
||||
|
||||
storiesOf('AssetStore/CustomizationFields', module)
|
||||
.addDecorator(paperDecorator)
|
||||
.addDecorator(muiDecorator)
|
||||
.add('with behavior customizations', () => (
|
||||
<CustomizationFields
|
||||
asset={fakeAssetWithBehaviorCustomizations1}
|
||||
project={testProject.project}
|
||||
objectsContainer={testProject.testLayout}
|
||||
layout={testProject.testLayout}
|
||||
resourceExternalEditors={fakeResourceExternalEditors}
|
||||
onChooseResource={() => {
|
||||
action('onChooseResource');
|
||||
return Promise.reject();
|
||||
}}
|
||||
resourceSources={[]}
|
||||
/>
|
||||
))
|
||||
.add('with event customizations', () => (
|
||||
<CustomizationFields
|
||||
asset={fakeAssetWithEventCustomizationsAndFlashExtension1}
|
||||
project={testProject.project}
|
||||
objectsContainer={testProject.testLayout}
|
||||
layout={testProject.testLayout}
|
||||
resourceExternalEditors={fakeResourceExternalEditors}
|
||||
onChooseResource={() => {
|
||||
action('onChooseResource');
|
||||
return Promise.reject();
|
||||
}}
|
||||
resourceSources={[]}
|
||||
/>
|
||||
));
|
||||
|
@@ -7287,6 +7287,11 @@ flow-bin@0.120.1:
|
||||
resolved "https://registry.yarnpkg.com/flow-bin/-/flow-bin-0.120.1.tgz#ab051d6df71829b70a26a2c90bb81f9d43797cae"
|
||||
integrity sha512-KgE+d+rKzdXzhweYVJty1QIOOZTTbtnXZf+4SLnmArLvmdfeLreQOZpeLbtq5h79m7HhDzX/HkUkoyu/fmSC2A==
|
||||
|
||||
flow-bin@^0.50.0:
|
||||
version "0.50.0"
|
||||
resolved "https://registry.yarnpkg.com/flow-bin/-/flow-bin-0.50.0.tgz#d4cdb2430dee1a3599f0eb6fe551146e3027256a"
|
||||
integrity sha1-1M2yQw3uGjWZ8Otv5VEUbjAnJWo=
|
||||
|
||||
flow-coverage-report@^0.4.0:
|
||||
version "0.4.1"
|
||||
resolved "https://registry.yarnpkg.com/flow-coverage-report/-/flow-coverage-report-0.4.1.tgz#f757954024a4d11de86d8a05040042f7b78e41ab"
|
||||
@@ -9457,6 +9462,14 @@ js-tokens@^3.0.2:
|
||||
resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-3.0.2.tgz#9866df395102130e38f7f996bceb65443209c25b"
|
||||
integrity sha1-mGbfOVECEw449/mWvOtlRDIJwls=
|
||||
|
||||
js-worker-search@^1.4.1:
|
||||
version "1.4.1"
|
||||
resolved "https://registry.yarnpkg.com/js-worker-search/-/js-worker-search-1.4.1.tgz#beb5ac0df3f37900ff2ad676951833d3913b3192"
|
||||
integrity sha512-5KCio6angmP+AXX8X2pPtjw1h3BCrmEmwGv2MdkXhl3+7g/2NTHOMhUh8bZqacp8R9LjhTT2bGr+f7RdB8KFZw==
|
||||
dependencies:
|
||||
flow-bin "^0.50.0"
|
||||
uuid "^2.0.1"
|
||||
|
||||
js-yaml@^3.13.1:
|
||||
version "3.14.0"
|
||||
resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-3.14.0.tgz#a7a34170f26a21bb162424d8adacb4113a69e482"
|
||||
@@ -15452,6 +15465,11 @@ utils-merge@1.0.1:
|
||||
resolved "https://registry.yarnpkg.com/utils-merge/-/utils-merge-1.0.1.tgz#9f95710f50a267947b2ccc124741c1028427e713"
|
||||
integrity sha1-n5VxD1CiZ5R7LMwSR0HBAoQn5xM=
|
||||
|
||||
uuid@^2.0.1:
|
||||
version "2.0.3"
|
||||
resolved "https://registry.yarnpkg.com/uuid/-/uuid-2.0.3.tgz#67e2e863797215530dff318e5bf9dcebfd47b21a"
|
||||
integrity sha1-Z+LoY3lyFVMN/zGOW/nc6/1Hsho=
|
||||
|
||||
uuid@^3.0.1, uuid@^3.3.2:
|
||||
version "3.4.0"
|
||||
resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.4.0.tgz#b23e4358afa8a202fe7a100af1f5f883f02007ee"
|
||||
|
Reference in New Issue
Block a user