Compare commits

..

15 Commits

Author SHA1 Message Date
Florian Rival
0e3f70627b Bump newIDE version 2018-02-03 19:32:52 +01:00
Florian Rival
a814a07105 Fix ResourcesEditor selection 2018-02-03 19:24:47 +01:00
Florian Rival
c49af90a9c Add program opening count to Keen.io analytics 2018-02-03 19:09:33 +01:00
Florian Rival
24afa155c8 Improve Keen.io analytics events by adding user profile information 2018-02-03 18:30:09 +01:00
Florian Rival
9e5a431516 Only run Fullstory on newIDE web-app 2018-02-03 17:53:57 +01:00
Florian Rival
182a94285c Unregister service worker (it is not ready yet) 2018-02-03 13:05:22 +01:00
Florian Rival
442c2c8dd9 Merge pull request #439 from 4ian/feature/resources-editor
Resources editor for newIDE
2018-02-03 12:57:22 +01:00
Florian Rival
f8fd0dd353 Add a preview in ResourcesEditor 2018-02-03 12:55:30 +01:00
Florian Rival
faad9e23ac Add ResourcesEditor with ResourcePropertiesEditor and Toolbar 2018-01-30 23:12:31 +01:00
Florian Rival
d2af0da1b1 Added tools method to ResourcesManager and finish working version of ResourcesList 2018-01-30 21:56:40 +01:00
Florian Rival
c65e5c3e49 [WIP] Add ResourcesEditor based on a generic SortableVirtualizedItemList
TODO:
* Use SortableVirtualizedItemList for ObjectsList and ObjectsGroupsList
* Add missing features (sorting, delete, thumbnail for images, properties edition) to ResourcesEditor
2018-01-30 00:58:21 +01:00
Florian Rival
24a8dfc5f0 Add SemiControlledTextField to be able to type freely in PropertiesEditor 2018-01-28 18:45:44 +01:00
Florian Rival
9c6790ac37 Update default gravity in PhysicsBehavior 2018-01-27 23:50:49 +01:00
Florian Rival
18ef7460ba Bump newIDE version 2018-01-27 19:34:36 +01:00
Florian Rival
63cd0e76c3 Fix warning 2018-01-27 19:34:27 +01:00
39 changed files with 1130 additions and 182 deletions

View File

@@ -945,7 +945,7 @@ void ResourcesEditor::Refresh()
gd::ResourceFolder & folder = project.GetResourcesManager().GetFolder(folders[i]);
wxTreeItemId folderItem = resourcesTree->AppendItem( resourcesTree->GetRootItem(), folders[i], -1, -1, new gd::TreeItemStringData("Folder", folders[i] ));
std::vector<gd::String> resources = folder.GetAllResourcesList();
std::vector<gd::String> resources = folder.GetAllResourceNames();
for (std::size_t j=0;j<resources.size();++j)
{
gd::Resource & resource = folder.GetResource(resources[j]);
@@ -959,7 +959,7 @@ void ResourcesEditor::Refresh()
//All images
allImagesItem = resourcesTree->AppendItem( resourcesTree->GetRootItem(), _("All images"), -1,-1, new gd::TreeItemStringData("BaseFolder", "" ));
std::vector<gd::String> resources = project.GetResourcesManager().GetAllResourcesList();
std::vector<gd::String> resources = project.GetResourcesManager().GetAllResourceNames();
for ( std::size_t i = 0;i <resources.size();i++ )
{
gd::Resource & resource = project.GetResourcesManager().GetResource(resources[i]);

View File

@@ -51,7 +51,7 @@ void ArbitraryResourceWorker::ExposeResources(gd::ResourcesManager * resourcesMa
resourcesManagers.push_back(resourcesManager);
std::vector<gd::String> resources = resourcesManager->GetAllResourcesList();
std::vector<gd::String> resources = resourcesManager->GetAllResourceNames();
for ( std::size_t i = 0;i < resources.size() ;i++ )
{
if ( resourcesManager->GetResource(resources[i]).UseFile() )

View File

@@ -45,7 +45,7 @@ std::vector<gd::String> ProjectResourcesAdder::GetAllUselessImages(gd::Project &
std::set<gd::String> & usedImages = inventorizer.GetAllUsedImages();
//Search all images resources not used
std::vector<gd::String> resources = project.GetResourcesManager().GetAllResourcesList();
std::vector<gd::String> resources = project.GetResourcesManager().GetAllResourceNames();
for (std::size_t i = 0;i < resources.size();i++)
{
if (project.GetResourcesManager().GetResource(resources[i]).GetKind() != "image")

View File

@@ -141,7 +141,7 @@ void ImageManager::LoadPermanentImages()
//so as not to unload images that could be still present.
std::map < gd::String, std::shared_ptr<SFMLTextureWrapper> > newPermanentlyLoadedImages;
std::vector<gd::String> resources = resourcesManager->GetAllResourcesList();
std::vector<gd::String> resources = resourcesManager->GetAllResourceNames();
for ( std::size_t i = 0;i <resources.size();i++ )
{
try

View File

@@ -131,6 +131,7 @@ gd::Layer & Layout::GetLayer(const gd::String & name)
return badLayer;
}
const gd::Layer & Layout::GetLayer(const gd::String & name) const
{
std::vector<gd::Layer>::const_iterator layer = find_if(initialLayers.begin(), initialLayers.end(), bind2nd(gd::LayerHasName(), name));
@@ -140,14 +141,17 @@ const gd::Layer & Layout::GetLayer(const gd::String & name) const
return badLayer;
}
gd::Layer & Layout::GetLayer(std::size_t index)
{
return initialLayers[index];
}
const gd::Layer & Layout::GetLayer (std::size_t index) const
{
return initialLayers[index];
}
std::size_t Layout::GetLayersCount() const
{
return initialLayers.size();

View File

@@ -172,37 +172,37 @@ public:
/** \name Layout layers management
* Members functions related to layout layers management.
* TODO: This should be moved to a separate class
* TODO: This could be moved to a separate class
*/
///@{
/**
* Must return true if the layer called "name" exists.
* \brief Return true if the layer called "name" exists.
*/
bool HasLayerNamed(const gd::String & name) const;
/**
* Must return a reference to the layer called "name".
* \brief Return a reference to the layer called "name".
*/
Layer & GetLayer(const gd::String & name);
/**
* Must return a reference to the layer called "name".
* \brief Return a reference to the layer called "name".
*/
const Layer & GetLayer(const gd::String & name) const;
/**
* Must return a reference to the layer at position "index" in the layers list
* \brief Return a reference to the layer at position "index" in the layers list
*/
Layer & GetLayer(std::size_t index);
/**
* Must return a reference to the layer at position "index" in the layers list
* \brief Return a reference to the layer at position "index" in the layers list
*/
const Layer & GetLayer (std::size_t index) const;
const Layer & GetLayer(std::size_t index) const;
/**
* Must return the position of the layer called "name" in the layers list
* \brief Return the position of the layer called "name" in the layers list
*/
std::size_t GetLayerPosition(const gd::String & name) const;

View File

@@ -105,7 +105,7 @@ bool ResourcesManager::HasResource(const gd::String & name) const
return false;
}
std::vector<gd::String> ResourcesManager::GetAllResourcesList()
std::vector<gd::String> ResourcesManager::GetAllResourceNames()
{
std::vector<gd::String> allResources;
for (std::size_t i = 0;i<resources.size();++i)
@@ -146,7 +146,7 @@ bool ResourcesManager::AddResource(const gd::String & name, const gd::String & f
return true;
}
std::vector<gd::String> ResourceFolder::GetAllResourcesList()
std::vector<gd::String> ResourceFolder::GetAllResourceNames()
{
std::vector<gd::String> allResources;
for (std::size_t i = 0;i<resources.size();++i)
@@ -351,6 +351,25 @@ bool ResourcesManager::MoveResourceDownInList(const gd::String & name)
return gd::MoveResourceDownInList(resources, name);
}
std::size_t ResourcesManager::GetResourcePosition(const gd::String & name) const
{
for (std::size_t i = 0;i<resources.size();++i)
{
if (resources[i]->GetName() == name) return i;
}
return gd::String::npos;
}
void ResourcesManager::MoveResource(std::size_t oldIndex, std::size_t newIndex)
{
if ( oldIndex >= resources.size() || newIndex >= resources.size() )
return;
auto resource = resources[oldIndex];
resources.erase(resources.begin() + oldIndex);
resources.insert(resources.begin() + newIndex, resource);
}
bool ResourcesManager::MoveFolderUpInList(const gd::String & name)
{
for (std::size_t i =1;i<folders.size();++i)

View File

@@ -293,9 +293,9 @@ public:
std::shared_ptr<Resource> CreateResource(const gd::String & kind);
/**
* \brief Get a list containing the name of all of the resources.
* \brief Get a list containing the names of all resources.
*/
std::vector<gd::String> GetAllResourcesList();
std::vector<gd::String> GetAllResourceNames();
#if defined(GD_IDE_ONLY)
/**
@@ -324,6 +324,11 @@ public:
*/
void RenameResource(const gd::String & oldName, const gd::String & newName);
/**
* \brief Return the position of the layer called "name" in the layers list
*/
std::size_t GetResourcePosition(const gd::String & name) const;
/**
* \brief Move a resource up in the list
*/
@@ -334,6 +339,11 @@ public:
*/
bool MoveResourceDownInList(const gd::String & name);
/**
* Change the position of the specified resource.
*/
void MoveResource(std::size_t oldIndex, std::size_t newIndex);
/**
* \brief Return true if the folder exists.
*/
@@ -445,7 +455,7 @@ public:
/**
* Get a list containing the name of all of the resources.
*/
virtual std::vector<gd::String> GetAllResourcesList();
virtual std::vector<gd::String> GetAllResourceNames();
/**
* Move a resource up in the list

View File

@@ -84,7 +84,7 @@ TEST_CASE( "Resources", "[common][resources]" ) {
gd::ProjectResourcesAdder::RemoveAllUselessImages(project);
std::vector<gd::String> remainingResources =
project.GetResourcesManager().GetAllResourcesList();
project.GetResourcesManager().GetAllResourceNames();
REQUIRE(remainingResources.size() == 2);
REQUIRE(remainingResources[0] == "res1");
REQUIRE(remainingResources[1] == "res4");

View File

@@ -17,7 +17,7 @@ This project is released under the MIT License.
class ScenePhysicsDatas : public gd::BehaviorsSharedData
{
public:
ScenePhysicsDatas() : BehaviorsSharedData(), gravityX(0), gravityY(0), scaleX(100), scaleY(100)
ScenePhysicsDatas() : BehaviorsSharedData(), gravityX(0), gravityY(9), scaleX(100), scaleY(100)
{
};
virtual ~ScenePhysicsDatas() {};

View File

@@ -13,6 +13,7 @@ declare type gdExternalEvents = EmscriptenObject;
declare type gdSerializerElement = EmscriptenObject;
declare type gdInitialInstance = EmscriptenObject;
declare type gdBaseEvent = EmscriptenObject;
declare type gdResource = EmscriptenObject;
//Represents all objects that have serializeTo and unserializeFrom methods.
declare type gdSerializable = EmscriptenObject;

View File

@@ -25,27 +25,8 @@
<meta name="theme-color" content="#4ab0e4">
<link rel="manifest" href="%PUBLIC_URL%/manifest.json">
<!-- Fullstory user analytics -->
<script>
window['_fs_debug'] = false;
window['_fs_host'] = 'fullstory.com';
window['_fs_org'] = '8DWZ1';
window['_fs_namespace'] = 'FS';
(function(m,n,e,t,l,o,g,y){
if (e in m) {if(m.console && m.console.log) { m.console.log('FullStory namespace conflict. Please set window["_fs_namespace"].');} return;}
g=m[e]=function(a,b){g.q?g.q.push([a,b]):g._api(a,b);};g.q=[];
o=n.createElement(t);o.async=1;o.src='https://'+_fs_host+'/s/fs.js';
y=n.getElementsByTagName(t)[0];y.parentNode.insertBefore(o,y);
g.identify=function(i,v){g(l,{uid:i});if(v)g(l,v)};g.setUserVars=function(v){g(l,v)};
g.identifyAccount=function(i,v){o='account';v=v||{};v.acctId=i;g(o,v)};
g.clearUserCookie=function(c,d,i){if(!c || document.cookie.match('fs_uid=[`;`]*`[`;`]*`[`;`]*`')){
d=n.domain;while(1){n.cookie='fs_uid=;domain='+d+
';path=/;expires='+new Date(0).toUTCString();i=d.indexOf('.');if(i<0)break;d=d.slice(i+1)}}};
})(window,document,window['_fs_namespace'],'script','user');
</script>
<!-- Stripe.com Checkout -->
<script src="https://checkout.stripe.com/checkout.js"></script>
<!-- Stripe.com Checkout -->
<script src="https://checkout.stripe.com/checkout.js"></script>
</head>
<body>

View File

@@ -0,0 +1,21 @@
import React from 'react';
import BaseEditor from './BaseEditor';
import ResourcesFullEditor from '../../ResourcesEditor';
export default class ResourcesEditor extends BaseEditor {
updateToolbar() {
if (this.editor) this.editor.updateToolbar();
}
render() {
const { project } = this.props;
return (
<ResourcesFullEditor
{...this.props}
ref={editor => (this.editor = editor)}
project={project}
/>
);
}
}

View File

@@ -47,6 +47,7 @@ import ExternalEventsEditor from './Editors/ExternalEventsEditor';
import SceneEditor from './Editors/SceneEditor';
import ExternalLayoutEditor from './Editors/ExternalLayoutEditor';
import StartPage from './Editors/StartPage';
import ResourcesEditor from './Editors/ResourcesEditor';
import {
type PreferencesState,
getThemeName,
@@ -545,6 +546,36 @@ export default class MainFrame extends Component<*, State> {
);
};
openResources = () => {
this.setState(
{
editorTabs: openEditorTab(this.state.editorTabs, {
name: 'Resources',
editorCreator: () => (
<ResourcesEditor
project={this.state.currentProject}
setToolbar={this.setEditorToolbar}
onDeleteResource={(resource: gdResource, cb: boolean => void) => {
// TODO: Project wide refactoring of objects/events using the resource
cb(true);
}}
onRenameResource={(
resource: gdResource,
newName: string,
cb: boolean => void
) => {
// TODO: Project wide refactoring of objects/events using the resource
cb(true);
}}
/>
),
key: 'resources',
}),
},
() => this.updateToolbar()
);
};
openStartPage = () => {
this.setState(
{
@@ -814,6 +845,7 @@ export default class MainFrame extends Component<*, State> {
onCloseProject={this.askToCloseProject}
onExportProject={this.openExportDialog}
onOpenPreferences={() => this.openPreferences(true)}
onOpenResources={() => this.openResources()}
/>
)}
</Drawer>

View File

@@ -1,6 +1,6 @@
import React from 'react';
import ResourcesLoader from '../ObjectsRendering/ResourcesLoader';
import ResourceSelector from '../ResourcesEditor/ResourceSelector';
import ResourceSelector from '../ResourcesList/ResourceSelector';
import ImageThumbnail from './ImageThumbnail';
export default ({

View File

@@ -22,7 +22,7 @@ export default class PixiResourcesLoader {
const resourcesManager = project.getResourcesManager();
const loader = PIXI.loader;
const resourcesList = resourcesManager.getAllResourcesList().toJSArray();
const resourcesList = resourcesManager.getAllResourceNames().toJSArray();
const allResources = {};
resourcesList.forEach(resourceName => {
const resource = resourcesManager.getResource(resourceName);

View File

@@ -1,21 +1,6 @@
// @flow
import { mapFor } from '../Utils/MapFor';
//TODO: Layout, ExternalEvents and ExternalLayout should be moved to a common type definition file
//for all GDevelop.js
type Layout = {
getName: Function,
setName: Function,
};
type ExternalLayout = {
getName: Function,
setName: Function,
};
type ExternalEvents = {
getName: Function,
setName: Function,
};
export const enumerateLayouts = (project: any) =>
mapFor(0, project.getLayoutsCount(), i => project.getLayoutAt(i));
@@ -30,7 +15,7 @@ export const enumerateExternalLayouts = (project: any) =>
);
export const filterProjectItemsList = (
list: Array<Layout> | Array<ExternalLayout> | Array<ExternalEvents>,
list: Array<gdLayout> | Array<gdExternalLayout> | Array<gdExternalEvents>,
searchText: string
) => {
if (!searchText) return list;

View File

@@ -1,4 +1,5 @@
import React, { Component } from 'react';
// @flow
import * as React from 'react';
import { List, ListItem } from 'material-ui/List';
import TextField from 'material-ui/TextField';
import SearchBar from 'material-ui-search-bar';
@@ -65,7 +66,10 @@ const ThemableProjectStructureItem = ({ muiTheme, ...otherProps }) => (
const ProjectStructureItem = muiThemeable()(ThemableProjectStructureItem);
class ThemableItem extends Component {
class ThemableItem extends React.Component<*, *> {
textField: ?Object;
_iconMenu: ?Object;
componentDidUpdate(prevProps) {
if (!prevProps.editingName && this.props.editingName) {
setTimeout(() => {
@@ -126,7 +130,7 @@ class ThemableItem extends Component {
onKeyPress={event => {
if (event.charCode === 13) {
// enter key pressed
this.textField.blur();
if (this.textField) this.textField.blur();
this.props.onRename(event.target.value);
}
}}
@@ -157,33 +161,64 @@ const Item = muiThemeable()(ThemableItem);
const AddItem = makeAddItem(ListItem);
export default class ProjectManager extends React.Component {
type Props = {
project: gdProject,
onDeleteLayout: (gdLayout) => void,
onDeleteExternalEvents: (gdExternalEvents) => void,
onDeleteExternalLayout: (gdExternalLayout) => void,
onRenameLayout: (string, string) => void,
onRenameExternalEvents: (string, string) => void,
onRenameExternalLayout: (string, string) => void,
onOpenLayout: (string) => void,
onOpenExternalEvents: (string) => void,
onOpenExternalLayout: (string) => void,
onSaveProject: () => void,
onCloseProject: () => void,
onExportProject: () => void,
onOpenPreferences: () => void,
onOpenResources: () => void,
onAddLayout: () => void,
onAddExternalEvents: () => void,
onAddExternalLayout: () => void,
};
type State = {|
renamedItemKind: ?string,
renamedItemName: string,
searchText: string,
projectPropertiesDialogOpen: boolean,
variablesEditorOpen: boolean,
|};
export default class ProjectManager extends React.Component<Props, State> {
state = {
renamedItemKind: null,
renamedItemName: '',
searchText: '',
projectPropertiesDialogOpen: false,
variablesEditorOpen: false,
};
_onEditName = (kind, name) => {
_onEditName = (kind: ?string, name: string) => {
this.setState({
renamedItemKind: kind,
renamedItemName: name,
});
};
_copyLayout = layout => {
_copyLayout = (layout: gdLayout) => {
Clipboard.set(LAYOUT_CLIPBOARD_KIND, {
layout: serializeToJSObject(layout),
name: layout.getName(),
});
};
_cutLayout = layout => {
_cutLayout = (layout: gdLayout) => {
this._copyLayout(layout);
this.props.onDeleteLayout(layout);
};
_pasteLayout = index => {
_pasteLayout = (index: number) => {
if (!Clipboard.has(LAYOUT_CLIPBOARD_KIND)) return;
const { layout: copiedLayout, name } = Clipboard.get(LAYOUT_CLIPBOARD_KIND);
@@ -207,19 +242,19 @@ export default class ProjectManager extends React.Component {
this.forceUpdate();
};
_copyExternalEvents = externalEvents => {
_copyExternalEvents = (externalEvents: gdExternalEvents) => {
Clipboard.set(EXTERNAL_EVENTS_CLIPBOARD_KIND, {
externalEvents: serializeToJSObject(externalEvents),
name: externalEvents.getName(),
});
};
_cutExternalEvents = externalEvents => {
_cutExternalEvents = (externalEvents: gdExternalEvents) => {
this._copyExternalEvents(externalEvents);
this.props.onDeleteExternalEvents(externalEvents);
};
_pasteExternalEvents = index => {
_pasteExternalEvents = (index: number) => {
if (!Clipboard.has(EXTERNAL_EVENTS_CLIPBOARD_KIND)) return;
const { externalEvents: copiedExternalEvents, name } = Clipboard.get(
@@ -244,19 +279,19 @@ export default class ProjectManager extends React.Component {
this.forceUpdate();
};
_copyExternalLayout = externalLayout => {
_copyExternalLayout = (externalLayout: gdExternalLayout) => {
Clipboard.set(EXTERNAL_LAYOUT_CLIPBOARD_KIND, {
externalLayout: serializeToJSObject(externalLayout),
name: externalLayout.getName(),
});
};
_cutExternalLayout = externalLayout => {
_cutExternalLayout = (externalLayout: gdExternalLayout) => {
this._copyExternalLayout(externalLayout);
this.props.onDeleteExternalLayout(externalLayout);
};
_pasteExternalLayout = index => {
_pasteExternalLayout = (index: number) => {
if (!Clipboard.has(EXTERNAL_LAYOUT_CLIPBOARD_KIND)) return;
const { externalLayout: copiedExternalLayout, name } = Clipboard.get(
@@ -328,10 +363,6 @@ export default class ProjectManager extends React.Component {
<div style={styles.container}>
<List style={styles.list}>
{this._renderMenu()}
{/* <ProjectStructureItem
primaryText="Resources"
leftIcon={<ListIcon src="res/ribbon_default/image32.png" />}
/> */}
<ProjectStructureItem
primaryText="Game settings"
leftIcon={
@@ -354,6 +385,12 @@ export default class ProjectManager extends React.Component {
leftIcon={<ListIcon src="res/ribbon_default/editname32.png" />}
onClick={() => this.setState({ variablesEditorOpen: true })}
/>,
<ListItem
key="resources"
primaryText="Resources"
leftIcon={<ListIcon src="res/ribbon_default/image32.png" />}
onClick={() => this.props.onOpenResources()}
/>,
]}
/>
<ProjectStructureItem
@@ -367,7 +404,7 @@ export default class ProjectManager extends React.Component {
enumerateLayouts(project),
searchText
)
.map((layout, i) => {
.map((layout: gdLayout, i: number) => {
const name = layout.getName();
return (
<Item

View File

@@ -0,0 +1,52 @@
// @flow
import * as React from 'react';
import TextField from 'material-ui/TextField';
type State = {
focused: boolean,
text: ?string,
};
/**
* This component works like a material-ui TextField, except that
* the value passed as props is not forced into the text field when the user
* is typing. This is useful if the parent component can do modifications on the value:
* the user won't be interrupted or have the value changed until he blurs the field.
*/
export default class SemiControlledTextField extends React.Component<*, State> {
state = {
focused: false,
text: null,
};
render() {
const { value, onChange, ...otherProps } = this.props;
return (
<TextField
{...otherProps}
value={this.state.focused ? this.state.text : value}
onFocus={() => {
this.setState({
focused: true,
text: this.props.value,
});
}}
onChange={(event, newValue) => {
this.setState({
text: newValue,
});
onChange(newValue);
}}
onBlur={event => {
onChange(event.target.value);
this.setState({
focused: false,
text: null,
});
}}
/>
);
}
}

View File

@@ -1,5 +1,5 @@
import React, { Component } from 'react';
import TextField from 'material-ui/TextField';
import SemiControlledTextField from './SemiControlledTextField';
import Checkbox from 'material-ui/Checkbox';
import Subheader from 'material-ui/Subheader';
import FlatButton from 'material-ui/FlatButton';
@@ -53,13 +53,13 @@ export default class PropertiesEditor extends Component {
);
} else if (field.valueType === 'number') {
return (
<TextField
<SemiControlledTextField
value={this._getFieldValue(this.props.instances, field)}
key={field.name}
id={field.name}
floatingLabelText={field.name}
floatingLabelFixed={true}
onChange={(event, newValue) => {
floatingLabelFixed
onChange={newValue => {
this.props.instances.forEach(i =>
field.setValue(i, parseFloat(newValue) || 0)
);
@@ -72,16 +72,17 @@ export default class PropertiesEditor extends Component {
);
} else {
return (
<TextField
<SemiControlledTextField
value={this._getFieldValue(
this.props.instances,
field,
'(Multiple values)'
)}
key={field.name}
id={field.name}
floatingLabelText={field.name}
floatingLabelFixed={true}
onChange={(event, newValue) => {
floatingLabelFixed
onChange={newValue => {
this.props.instances.forEach(i =>
field.setValue(i, newValue || '')
);
@@ -107,7 +108,7 @@ export default class PropertiesEditor extends Component {
value={this._getFieldValue(this.props.instances, field)}
key={field.name}
floatingLabelText={field.name}
floatingLabelFixed={true}
floatingLabelFixed
onChange={(event, index, newValue) => {
this.props.instances.forEach(i =>
field.setValue(i, parseFloat(newValue) || 0)
@@ -130,7 +131,7 @@ export default class PropertiesEditor extends Component {
)}
key={field.name}
floatingLabelText={field.name}
floatingLabelFixed={true}
floatingLabelFixed
onChange={(event, index, newValue) => {
this.props.instances.forEach(i =>
field.setValue(i, newValue || '')

View File

@@ -0,0 +1,101 @@
// @flow
import * as React from 'react';
import Paper from 'material-ui/Paper';
import EmptyMessage from '../../UI/EmptyMessage';
import PropertiesEditor from '../../PropertiesEditor';
import ImagePreview from '../../ObjectEditor/ImagePreview'; //TODO: Move ImagePreview out of ObjectEditor
import ResourceLoader from '../../ObjectsRendering/ResourcesLoader';
const styles = {
container: {
display: 'flex',
flexDirection: 'column',
flex: 1,
width: '100%',
},
imagePreview: { flex: 1 },
propertiesContainer: {
padding: 10,
overflowY: 'scroll',
overflowX: 'hidden',
flex: 2,
},
};
type Props = {|
project: gdProject,
resourcesLoader: ResourceLoader,
resources: Array<gdResource>,
|};
export default class ResourcePropertiesEditor extends React.Component<
Props,
{}
> {
schema = [
{
name: 'Resource name',
valueType: 'string',
disabled: true,
getValue: (resource: gdResource) => resource.getName(),
setValue: (resource: gdResource, newValue: string) =>
resource.setName(newValue),
},
{
name: 'File',
valueType: 'string',
getValue: (resource: gdResource) => resource.getFile(),
setValue: (resource: gdResource, newValue: string) =>
resource.setFile(newValue),
},
];
_renderEmpty() {
return (
<EmptyMessage>
Resources are automatically added to your project whenever you add an
image to an object. Choose a resource to display its properties.
</EmptyMessage>
);
}
_renderResourcesProperties() {
const { resources } = this.props;
return (
<div
style={styles.propertiesContainer}
key={resources.map(resource => '' + resource.ptr).join(';')}
>
<PropertiesEditor schema={this.schema} instances={resources} />
</div>
);
}
_renderPreview() {
const { resources, project, resourcesLoader } = this.props;
if (!resources || !resources.length) return;
return (
<ImagePreview
style={styles.imagePreview}
resourceName={resources[0].getName()}
resourcesLoader={resourcesLoader}
project={project}
/>
);
}
render() {
const { resources } = this.props;
return (
<Paper style={styles.container}>
{this._renderPreview()}
{!resources || !resources.length
? this._renderEmpty()
: this._renderResourcesProperties()}
</Paper>
);
}
}

View File

@@ -0,0 +1,39 @@
// @flow
import React, { PureComponent } from 'react';
import { translate, type TranslatorProps } from 'react-i18next';
import { ToolbarGroup } from 'material-ui/Toolbar';
import ToolbarIcon from '../UI/ToolbarIcon';
import ToolbarSeparator from '../UI/ToolbarSeparator';
type Props = {|
onDeleteSelection: () => void,
canDelete: boolean,
onOpenProperties: () => void,
|} & TranslatorProps;
type State = {||};
export class Toolbar extends PureComponent<Props, State> {
render() {
const { t, canDelete } = this.props;
return (
<ToolbarGroup lastChild>
<ToolbarIcon
onClick={this.props.onOpenProperties}
src="res/ribbon_default/editprop32.png"
tooltip={t('Open the properties panel')}
/>
<ToolbarSeparator />
<ToolbarIcon
onClick={this.props.onDeleteSelection}
src="res/ribbon_default/deleteselected32.png"
disabled={!canDelete}
tooltip={t('Delete the selected resource')}
/>
</ToolbarGroup>
);
}
}
export default translate()(Toolbar);

View File

@@ -0,0 +1,156 @@
// @flow
import * as React from 'react';
import ResourcesList from '../ResourcesList';
import ResourcePropertiesEditor from './ResourcePropertiesEditor';
import Toolbar from './Toolbar';
import EditorMosaic, { MosaicWindow } from '../UI/EditorMosaic';
import InfoBar from '../UI/Messages/InfoBar';
import ResourcesLoader from '../ObjectsRendering/ResourcesLoader';
const styles = {
container: {
display: 'flex',
flex: 1,
position: 'relative',
overflow: 'hidden',
},
};
type State = {
showPropertiesInfoBar: boolean,
selectedResource: ?gdResource,
};
type Props = {
setToolbar: React.Node => void,
project: gdProject,
onDeleteResource: (resource: gdResource, cb: (boolean) => void) => void,
onRenameResource: (
resource: gdResource,
newName: string,
cb: (boolean) => void
) => void,
};
export default class InstancesFullEditor extends React.Component<Props, State> {
static defaultProps = {
setToolbar: () => {},
};
editorMosaic: ?EditorMosaic = null;
_propertiesEditor: ?ResourcePropertiesEditor = null;
_resourcesList: ?ResourcesList = null;
resourcesLoader = ResourcesLoader;
state = {
showPropertiesInfoBar: false,
selectedResource: null,
};
updateToolbar() {
this.props.setToolbar(
<Toolbar
onOpenProperties={this.openProperties}
canDelete={!!this.state.selectedResource}
onDeleteSelection={() =>
this.deleteResource(this.state.selectedResource)}
/>
);
}
deleteResource = (resource: ?gdResource) => {
const { project, onDeleteResource } = this.props;
if (!resource) return;
//eslint-disable-next-line
const answer = confirm(
"Are you sure you want to remove this resource? This can't be undone."
);
if (!answer) return;
onDeleteResource(resource, doRemove => {
if (!doRemove || !resource) return;
project.getResourcesManager().removeResource(resource.getName());
this.setState(
{
selectedResource: null,
},
() => {
if (this._resourcesList) this._resourcesList.forceUpdateList();
this.updateToolbar();
}
);
});
};
openProperties = () => {
if (!this.editorMosaic) return;
if (!this.editorMosaic.openEditor('properties')) {
this.setState({
showPropertiesInfoBar: true,
});
}
};
_onResourceSelected = (selectedResource: ?gdResource) => {
this.setState(
{
selectedResource,
},
() => {
if (this._propertiesEditor) this._propertiesEditor.forceUpdate();
this.updateToolbar();
}
);
};
render() {
const { project, onRenameResource } = this.props;
const { selectedResource } = this.state;
console.log(selectedResource);
const editors = {
properties: (
<MosaicWindow
title="Properties"
// Pass resources to force MosaicWindow update when selectedResource is changed
resources={selectedResource ? [selectedResource] : []}
>
<ResourcePropertiesEditor
key={selectedResource ? selectedResource.ptr : undefined}
resources={selectedResource ? [selectedResource] : []}
project={project}
resourcesLoader={this.resourcesLoader}
ref={propertiesEditor =>
(this._propertiesEditor = propertiesEditor)}
/>
</MosaicWindow>
),
'resources-list': (
<ResourcesList
project={project}
onDeleteResource={this.deleteResource}
onRenameResource={onRenameResource}
onSelectResource={this._onResourceSelected}
selectedResource={selectedResource}
ref={resourcesList => (this._resourcesList = resourcesList)}
/>
),
};
return (
<div style={styles.container}>
<EditorMosaic
editors={editors}
ref={editorMosaic => (this.editorMosaic = editorMosaic)}
initialEditorNames={['properties', 'resources-list']}
/>
<InfoBar
message="Properties panel is already opened"
show={!!this.state.showPropertiesInfoBar}
/>
</div>
);
}
}

View File

@@ -0,0 +1,19 @@
// @flow
export const filterResourcesList = (
list: Array<gdResource>,
searchText: string
): Array<gdResource> => {
if (!searchText) return list;
const lowercaseSearchText = searchText.toLowerCase();
return list.filter((resource: gdResource) => {
return (
resource
.getName()
.toLowerCase()
.indexOf(lowercaseSearchText) !== -1
);
});
};

View File

@@ -40,7 +40,7 @@ export default class ResourceSelector extends Component {
}
_loadFrom(resourcesManager) {
this.allResourcesNames = resourcesManager.getAllResourcesList().toJSArray();
this.allResourcesNames = resourcesManager.getAllResourceNames().toJSArray();
if (this.props.resourceKind) {
this.allResourcesNames = this.allResourcesNames.filter(resourceName => {
return (

View File

@@ -0,0 +1,187 @@
// @flow
import * as React from 'react';
import { AutoSizer } from 'react-virtualized';
import SortableVirtualizedItemList from '../UI/SortableVirtualizedItemList';
import Paper from 'material-ui/Paper';
import SearchBar from 'material-ui-search-bar';
import { showWarningBox } from '../UI/Messages/MessageBox';
import { filterResourcesList } from './EnumerateResources';
const styles = {
container: {
flex: 1,
display: 'flex',
height: '100%',
flexDirection: 'column',
},
listContainer: {
flex: 1,
},
};
type State = {|
renamedResource: ?gdResource,
searchText: string,
|};
type Props = {|
project: gdProject,
selectedResource: ?gdResource,
onSelectResource: (resource: gdResource) => void,
onDeleteResource: (resource: gdResource) => void,
onRenameResource: (
resource: gdResource,
newName: string,
cb: (boolean) => void
) => void,
|};
export default class ResourcesList extends React.Component<Props, State> {
static defaultProps = {
onDeleteResource: (resource: gdResource, cb: boolean => void) => cb(true),
onRenameResource: (
resource: gdResource,
newName: string,
cb: boolean => void
) => cb(true),
};
sortableList: any;
state: State = {
renamedResource: null,
searchText: '',
};
shouldComponentUpdate(nextProps: Props, nextState: State) {
// The component is costly to render, so avoid any re-rendering as much
// as possible.
// We make the assumption that no changes to resources list is made outside
// from the component.
// If a change is made, the component won't notice it: you have to manually
// call forceUpdate.
if (
this.state.renamedResource !== nextState.renamedResource ||
this.state.searchText !== nextState.searchText
)
return true;
if (
this.props.project !== nextProps.project ||
this.props.selectedResource !== nextProps.selectedResource
)
return true;
return false;
}
_deleteResource = (resource: gdResource) => {
this.props.onDeleteResource(resource);
};
_editName = (resource: ?gdResource) => {
this.setState(
{
renamedResource: resource,
},
() => this.sortableList.getWrappedInstance().forceUpdateGrid()
);
};
_rename = (resource: gdResource, newName: string) => {
const { project } = this.props;
this.setState({
renamedResource: null,
});
if (resource.getName() === newName) return;
if (project.getResourcesManager().hasResource(newName)) {
showWarningBox('Another resource with this name already exists');
return;
}
this.props.onRenameResource(resource, newName, doRename => {
if (!doRename) return;
resource.setName(newName);
this.forceUpdate();
});
};
_move = (oldIndex: number, newIndex: number) => {
const { project } = this.props;
project.getResourcesManager().moveResource(oldIndex, newIndex);
this.forceUpdateList();
};
forceUpdateList = () => {
this.forceUpdate();
this.sortableList.getWrappedInstance().forceUpdateGrid();
};
_renderResourceMenuTemplate = (resource: gdResource) => {
return [
{
label: 'Rename',
click: () => this._editName(resource),
},
{
label: 'Delete',
click: () => this._deleteResource(resource),
},
];
};
render() {
const { project, selectedResource, onSelectResource } = this.props;
const { searchText } = this.state;
const resourcesManager = project.getResourcesManager();
const allResourcesList = resourcesManager
.getAllResourceNames()
.toJSArray()
.map(resourceName => resourcesManager.getResource(resourceName));
const filteredList = filterResourcesList(allResourcesList, searchText);
// Force List component to be mounted again if project or objectsContainer
// has been changed. Avoid accessing to invalid objects that could
// crash the app.
const listKey = project.ptr;
return (
<Paper style={styles.container}>
<div style={styles.listContainer}>
<AutoSizer>
{({ height, width }) => (
<SortableVirtualizedItemList
key={listKey}
ref={sortableList => (this.sortableList = sortableList)}
fullList={filteredList}
width={width}
height={height}
selectedItem={selectedResource}
onItemSelected={onSelectResource}
renamedItem={this.state.renamedResource}
onRename={this._rename}
onSortEnd={({ oldIndex, newIndex }) =>
this._move(oldIndex, newIndex)}
helperClass="sortable-helper"
distance={30}
buildMenuTemplate={this._renderResourceMenuTemplate}
/>
)}
</AutoSizer>
</div>
<SearchBar
value={searchText}
onRequestSearch={() => {}}
onChange={text =>
this.setState({
searchText: text,
})}
/>
</Paper>
);
}
}

View File

@@ -8,7 +8,6 @@ import EmptyMessage from '../../UI/EmptyMessage';
import PropertiesEditor from '../../PropertiesEditor';
import propertiesMapToSchema from '../../PropertiesEditor/PropertiesMapToSchema';
import some from 'lodash/some';
const gd = global.gd;
export default class ScenePropertiesDialog extends Component {
constructor(props) {
@@ -60,6 +59,7 @@ export default class ScenePropertiesDialog extends Component {
// />,
<FlatButton
label="Ok"
key="ok"
primary={true}
keyboardFocused={true}
onClick={this._onApply}

View File

@@ -0,0 +1,116 @@
import React from 'react';
import { ListItem } from 'material-ui/List';
import IconMenu from '../Menu/IconMenu';
import ListIcon from '../ListIcon';
import IconButton from 'material-ui/IconButton';
import TextField from 'material-ui/TextField';
import MoreVertIcon from 'material-ui/svg-icons/navigation/more-vert';
import muiThemeable from 'material-ui/styles/muiThemeable';
import { type Item } from '.';
const styles = {
itemName: {
overflow: 'hidden',
whiteSpace: 'nowrap',
textOverflow: 'ellipsis',
},
textField: {
top: -16,
},
};
type Props = {
index: number,
item: Item,
onRename: (string) => void,
editingName: boolean,
getThumbnail?: () => string,
selected: true,
onItemSelected: () => void,
buildMenuTemplate: Item => any,
};
class ThemableItemRow extends React.Component<Props, *> {
_renderItemMenu(item) {
return (
<IconMenu
ref={iconMenu => (this._iconMenu = iconMenu)}
iconButtonElement={
<IconButton>
<MoreVertIcon />
</IconButton>
}
buildMenuTemplate={() => this.props.buildMenuTemplate(item)}
/>
);
}
componentDidUpdate(prevProps) {
if (!prevProps.editingName && this.props.editingName) {
setTimeout(() => {
if (this.textField) this.textField.focus();
}, 100);
}
}
_onContextMenu = event => {
if (this._iconMenu) this._iconMenu.open(event);
};
render() {
const { item, selected, style, getThumbnail, muiTheme } = this.props;
const itemName = item.getName();
const label = this.props.editingName ? (
<TextField
id="rename-item-field"
ref={textField => (this.textField = textField)}
defaultValue={itemName}
onBlur={e => this.props.onRename(e.target.value)}
onKeyPress={event => {
if (event.charCode === 13) {
// enter key pressed
this.textField.blur();
}
}}
fullWidth
style={styles.textField}
/>
) : (
<div
style={{
...styles.itemName,
color: selected ? muiTheme.listItem.selectedTextColor : undefined,
}}
>
{itemName}
</div>
);
const itemStyle = {
borderBottom: `1px solid ${muiTheme.listItem.separatorColor}`,
backgroundColor: selected
? muiTheme.listItem.selectedBackgroundColor
: undefined,
};
return (
<ListItem
style={{ ...itemStyle, ...style }}
onContextMenu={this._onContextMenu}
primaryText={label}
leftIcon={getThumbnail && <ListIcon src={getThumbnail()} />}
rightIconButton={this._renderItemMenu(item)}
onClick={() => {
if (!this.props.onItemSelected) return;
if (this.props.editingName) return;
this.props.onItemSelected(selected ? null : item);
}}
/>
);
}
}
const ItemRow = muiThemeable()(ThemableItemRow);
export default ItemRow;

View File

@@ -0,0 +1,108 @@
// @flow
import * as React from 'react';
import { List } from 'react-virtualized';
import { ListItem } from 'material-ui/List';
import ItemRow from './ItemRow';
import { makeAddItem } from '../ListAddItem';
import { SortableContainer, SortableElement } from 'react-sortable-hoc';
const listItemHeight = 48; // TODO: Move this into theme?
const AddItemRow = makeAddItem(ListItem);
const SortableItemRow = SortableElement(props => {
const { style, ...otherProps } = props;
return (
<div style={style}>
<ItemRow {...otherProps} />
</div>
);
});
const SortableAddItemRow = SortableElement(props => {
return <AddItemRow {...props} />;
});
export type Item = {
key: string | number,
getName: () => string,
};
type ItemsListProps = {
height: number,
width: number,
fullList: Array<Item>,
selectedItem: ?Item,
onAddNewItem?: () => void,
onRename: (Item, string) => void,
getThumbnail?: Item => string,
onItemSelected: ?Item => void,
renamedItem: ?Item,
addNewItemLabel: React.Node | string,
buildMenuTemplate: Item => any,
};
class ItemsList extends React.Component<ItemsListProps, *> {
list: any;
forceUpdateGrid() {
if (this.list) this.list.forceUpdateGrid();
}
render() {
const {
height,
width,
fullList,
selectedItem,
addNewItemLabel,
renamedItem,
getThumbnail,
} = this.props;
return (
<List
ref={list => (this.list = list)}
height={height}
rowCount={fullList.length}
rowHeight={listItemHeight}
rowRenderer={({ index, key, style }) => {
const item = fullList[index];
if (item.key === 'add-item-row') {
return (
<SortableAddItemRow
index={fullList.length}
key={key}
style={style}
disabled
onClick={this.props.onAddNewItem}
primaryText={addNewItemLabel}
/>
);
}
const nameBeingEdited = renamedItem === item;
return (
<SortableItemRow
index={index}
key={key}
item={item}
style={style}
onRename={newName => this.props.onRename(item, newName)}
editingName={nameBeingEdited}
getThumbnail={getThumbnail ? () => getThumbnail(item) : undefined}
selected={item === selectedItem}
onItemSelected={this.props.onItemSelected}
buildMenuTemplate={this.props.buildMenuTemplate}
/>
);
}}
width={width}
/>
);
}
}
const SortableItemsList = SortableContainer(ItemsList, { withRef: true });
export default SortableItemsList;

View File

@@ -1,89 +1,109 @@
// @flow
import Keen from 'keen-tracking';
import Window from '../Window';
import { getUserUUID } from './UserUUID';
import Authentification from '../GDevelopServices/Authentification';
import {
getProgramOpeningCount,
incrementProgramOpeningCount,
} from './LocalStats';
const sessionCookie = Keen.utils.cookie('visitor-stats');
const sessionTimer = Keen.utils.timer();
sessionTimer.start();
const isDev = Window.isDev();
let client = null;
const isDev = process.env.NODE_ENV === 'development';
export const installAnalyticsEvents = (authentification: Authentification) => {
const sessionCookie = Keen.utils.cookie('visitor-stats');
const sessionTimer = Keen.utils.timer();
sessionTimer.start();
var client = new Keen({
projectId: '593d9f0595cfc907a1f8126a',
writeKey:
'B917F1DB50EE4C8949DBB374D2962845A22838B425AA43322A37138691A5270EB0358AEE45A4F61AFA7713B9765B4980517A1E276D4973A2E546EA851BF7757523706367ED430C041D2728A63BF61B5D1B2079C75E455DDDFAAC4324128AC2DB',
});
client = new Keen({
projectId: '593d9f0595cfc907a1f8126a',
writeKey:
'B917F1DB50EE4C8949DBB374D2962845A22838B425AA43322A37138691A5270EB0358AEE45A4F61AFA7713B9765B4980517A1E276D4973A2E546EA851BF7757523706367ED430C041D2728A63BF61B5D1B2079C75E455DDDFAAC4324128AC2DB',
});
client.extendEvents(function() {
return {
user: {
uuid: getUserUUID(),
},
page: {
title: document.title,
url: document.location.href,
// info: {} (add-on)
},
referrer: {
url: document.referrer,
// info: {} (add-on)
},
tech: {
browser: Keen.helpers.getBrowserProfile(),
// info: {} (add-on)
ip: '${keen.ip}', // eslint-disable-line
ua: '${keen.user_agent}', // eslint-disable-line
},
time: Keen.helpers.getDatetimeIndex(),
visitor: {
id: sessionCookie.get('user_id'),
time_on_page: sessionTimer.value(),
},
// geo: {} (add-on)
keen: {
timestamp: new Date().toISOString(),
addons: [
{
name: 'keen:ip_to_geo',
input: {
ip: 'tech.ip',
client.extendEvents(function() {
const userProfile = authentification.getUserProfileSync();
return {
user: {
uuid: getUserUUID(),
uid: userProfile ? userProfile.uid : undefined,
providerId: userProfile ? userProfile.providerId : undefined,
email: userProfile ? userProfile.email : undefined,
emailVerified: userProfile ? userProfile.emailVerified : undefined,
},
localStats: {
programOpeningCount: getProgramOpeningCount(),
},
page: {
title: document.title,
url: document.location.href,
// info: {} (add-on)
},
referrer: {
url: document.referrer,
// info: {} (add-on)
},
tech: {
browser: Keen.helpers.getBrowserProfile(),
// info: {} (add-on)
ip: '${keen.ip}', // eslint-disable-line
ua: '${keen.user_agent}', // eslint-disable-line
},
time: Keen.helpers.getDatetimeIndex(),
visitor: {
id: sessionCookie.get('user_id'),
time_on_page: sessionTimer.value(),
},
// geo: {} (add-on)
keen: {
timestamp: new Date().toISOString(),
addons: [
{
name: 'keen:ip_to_geo',
input: {
ip: 'tech.ip',
},
output: 'geo',
},
output: 'geo',
},
{
name: 'keen:ua_parser',
input: {
ua_string: 'tech.ua',
{
name: 'keen:ua_parser',
input: {
ua_string: 'tech.ua',
},
output: 'tech.info',
},
output: 'tech.info',
},
{
name: 'keen:url_parser',
input: {
url: 'page.url',
{
name: 'keen:url_parser',
input: {
url: 'page.url',
},
output: 'page.info',
},
output: 'page.info',
},
{
name: 'keen:referrer_parser',
input: {
page_url: 'page.url',
referrer_url: 'referrer.url',
{
name: 'keen:referrer_parser',
input: {
page_url: 'page.url',
referrer_url: 'referrer.url',
},
output: 'referrer.info',
},
output: 'referrer.info',
},
],
},
};
});
],
},
};
});
};
export const sendProgramOpening = () => {
if (isDev) return;
if (isDev || !client) return;
incrementProgramOpeningCount();
client.recordEvent('program_opening');
};
export const sendExportLaunched = exportKind => {
if (isDev) return;
export const sendExportLaunched = (exportKind: string) => {
if (isDev || !client) return;
client.recordEvent('export_launched', {
platform: 'GDevelop JS Platform', // Hardcoded here for now
@@ -91,8 +111,8 @@ export const sendExportLaunched = exportKind => {
});
};
export const sendNewGameCreated = templateName => {
if (isDev) return;
export const sendNewGameCreated = (templateName: string) => {
if (isDev || !client) return;
client.recordEvent('new_game_creation', {
platform: 'GDevelop JS Platform', // Hardcoded here for now
@@ -100,16 +120,20 @@ export const sendNewGameCreated = templateName => {
});
};
export const sendTutorialOpened = tutorialName => {
if (isDev) return;
export const sendTutorialOpened = (tutorialName: string) => {
if (isDev || !client) return;
client.recordEvent('tutorial_opened', {
tutorialName,
});
};
export const sendErrorMessage = (errorMessage, type, rawError) => {
if (isDev) return;
export const sendErrorMessage = (
errorMessage: string,
type: string,
rawError: any
) => {
if (isDev || !client) return;
client.recordEvent('error_message', {
message: errorMessage,
@@ -118,8 +142,8 @@ export const sendErrorMessage = (errorMessage, type, rawError) => {
});
};
export const sendSignupDone = email => {
if (isDev) return;
export const sendSignupDone = (email: string) => {
if (isDev || !client) return;
client.recordEvent('signup', {
email,

View File

@@ -1,9 +1,32 @@
import { getUserUUID } from './UserUUID';
const FS = global.FS;
import optionalRequire from '../OptionalRequire';
import Window from '../Window';
const electron = optionalRequire('electron');
export const installFullstory = () => {
if (FS) {
FS.identify(getUserUUID(), {
// prettier-ignore
if (!electron && !Window.isDev()) {
window['_fs_debug'] = false;
window['_fs_host'] = 'fullstory.com';
window['_fs_org'] = '8DWZ1';
window['_fs_namespace'] = 'FS';
(function(m,n,e,t,l,o,g,y){
if (e in m) {if(m.console && m.console.log) { m.console.log('FullStory namespace conflict. Please set window["_fs_namespace"].');} return;}
g=m[e]=function(a,b){g.q?g.q.push([a,b]):g._api(a,b);};g.q=[];
o=n.createElement(t);o.async=1;o.src='https://'+_fs_host+'/s/fs.js'; //eslint-disable-line
y=n.getElementsByTagName(t)[0];y.parentNode.insertBefore(o,y);
g.identify=function(i,v){g(l,{uid:i});if(v)g(l,v)};g.setUserVars=function(v){g(l,v)};
g.identifyAccount=function(i,v){o='account';v=v||{};v.acctId=i;g(o,v)};
g.clearUserCookie=function(c,d,i){if(!c || document.cookie.match('fs_uid=[`;`]*`[`;`]*`[`;`]*`')){
d=n.domain;while(1){n.cookie='fs_uid=;domain='+d+
';path=/;expires='+new Date(0).toUTCString();i=d.indexOf('.');if(i<0)break;d=d.slice(i+1)}}};
})(window,document,window['_fs_namespace'],'script','user');
} else {
console.info("Electron or development build - Fullstory disabled");
}
if (window.FS) {
window.FS.identify(getUserUUID(), {
// displayName: 'Daniel Falko',
// email: 'danielfalko@example.com',
});

View File

@@ -0,0 +1,23 @@
// @flow
const localStoragePrefix = 'gd-local-stats';
export const getProgramOpeningCount = (): number => {
try {
const count = localStorage.getItem(`${localStoragePrefix}-program-opening`);
if (count !== null) return parseInt(count, 10);
} catch (e) {
console.warn('Unable to load stored program opening count', e);
}
return 0;
};
export const incrementProgramOpeningCount = () => {
const count = getProgramOpeningCount() + 1;
try {
localStorage.setItem(`${localStoragePrefix}-program-opening`, '' + count);
} catch (e) {
console.warn('Unable to store program opening count', e);
}
};

View File

@@ -4,7 +4,7 @@ import { GDevelopFirebaseConfig } from './ApiConfigs';
export type Profile = {
uid: string, // This represents the userId
picture: string,
providerId: string,
email: string,
emailVerified: boolean,
};
@@ -76,6 +76,10 @@ export default class Authentification {
cb(null, this.user);
};
getUserProfileSync = (): ?Profile => {
return this.user;
};
logout = () => {
firebase
.auth()

View File

@@ -8,7 +8,7 @@ import { type Profile } from '../Utils/GDevelopServices/Authentification';
export const profileForIndieUser: Profile = {
uid: 'indie-user',
picture: '',
providerId: 'fake-provider.com',
email: 'indie-user@example.com',
emailVerified: true,
};

View File

@@ -1,3 +1,4 @@
// @flow
import 'element-closest';
import React from 'react';
import ReactDOM from 'react-dom';
@@ -6,10 +7,10 @@ import Window from './Utils/Window';
import ExportDialog from './Export/ExportDialog';
import CreateProjectDialog from './ProjectCreation/CreateProjectDialog';
import Authentification from './Utils/GDevelopServices/Authentification';
import { sendProgramOpening } from './Utils/Analytics/EventSender';
import { sendProgramOpening, installAnalyticsEvents } from './Utils/Analytics/EventSender';
import { installRaven } from './Utils/Analytics/Raven';
import { installFullstory } from './Utils/Analytics/Fullstory';
import registerServiceWorker from './registerServiceWorker';
import { unregister } from './registerServiceWorker';
import './UI/iconmoon-font.css'; // Styles for Iconmoon font.
import 'react-virtualized/styles.css'; // Styles for react-virtualized Table
@@ -18,7 +19,7 @@ import BrowserExamples from './ProjectCreation/BrowserExamples';
import BrowserProjectOpener from './ProjectsStorage/BrowserProjectOpener';
import BrowserSaveDialog from './ProjectsStorage/BrowserSaveDialog';
import BrowserIntroDialog from './MainFrame/BrowserIntroDialog';
import browserResourceSources from './ResourcesEditor/BrowserResourceSources';
import browserResourceSources from './ResourcesList/BrowserResourceSources';
import BrowserS3PreviewLauncher from './Export/BrowserExporters/BrowserS3PreviewLauncher';
import { getBrowserExporters } from './Export/BrowserExporters';
@@ -26,7 +27,7 @@ import { getBrowserExporters } from './Export/BrowserExporters';
import ExternalEditor from './ExternalEditor';
import optionalRequire from './Utils/OptionalRequire.js';
import LocalExamples from './ProjectCreation/LocalExamples';
import localResourceSources from './ResourcesEditor/LocalResourceSources';
import localResourceSources from './ResourcesList/LocalResourceSources';
import LocalProjectWriter from './ProjectsStorage/LocalProjectWriter';
import LocalProjectOpener from './ProjectsStorage/LocalProjectOpener';
import LocalPreviewLauncher from './Export/LocalExporters/LocalPreviewLauncher';
@@ -35,12 +36,14 @@ import ElectronEventsBridge from './MainFrame/ElectronEventsBridge';
import LocalIntroDialog from './MainFrame/LocalIntroDialog';
const electron = optionalRequire('electron');
const authentification = new Authentification();
installAnalyticsEvents(authentification);
installRaven();
installFullstory();
Window.setUpContextMenu();
const authentification = new Authentification();
let app = null;
if (electron) {
@@ -97,6 +100,10 @@ if (electron) {
);
}
ReactDOM.render(app, document.getElementById('root'));
registerServiceWorker();
const rootElement = document.getElementById('root');
if (rootElement) ReactDOM.render(app, rootElement);
else console.error("No root element defined in index.html");
// registerServiceWorker();
unregister();
sendProgramOpening();

View File

@@ -539,8 +539,6 @@ storiesOf('ProfileDetails', module)
<ProfileDetails
profile={{
email: 'test@example.com',
picture:
'"https://s.gravatar.com/avatar/d6fc8df7ddfe938cc379c53bfb5645fc?s=480&r=pg&d=https%3A%2F%2Fcdn.auth0.com%2Favatars%2Ffl.png',
}}
/>
))

View File

@@ -2,7 +2,7 @@
"name": "gdevelop",
"productName": "GDevelop 5",
"description": "GDevelop 5 IDE running on the Electron runtime",
"version": "5.0.0-beta22",
"version": "5.0.0-beta24",
"author": "Florian Rival",
"license": "MIT",
"homepage": "http://compilgames.net",