Add AudioResourceField

This commit is contained in:
Florian Rival
2018-04-04 10:36:59 +02:00
parent 29808c8800
commit e501fd4a09
18 changed files with 345 additions and 125 deletions

View File

@@ -15,6 +15,7 @@ declare type gdInitialInstance = EmscriptenObject;
declare type gdBaseEvent = EmscriptenObject;
declare type gdResource = EmscriptenObject;
declare type gdObject = EmscriptenObject;
declare type gdResourcesManager = EmscriptenObject;
declare type gdInstruction = EmscriptenObject;
declare type gdInstructionMetadata = EmscriptenObject;

View File

@@ -114,6 +114,8 @@ export default class InstructionParametersEditor extends Component {
this.forceUpdate();
}}
parameterRenderingService={ParameterRenderingService}
resourceSources={this.props.resourceSources}
onChooseResource={this.props.onChooseResource}
/>
);
})}

View File

@@ -0,0 +1,33 @@
// @flow
import React, { Component } from 'react';
import ResourceSelector from '../../../ResourcesList/ResourceSelector';
import { type ParameterFieldProps } from './ParameterFieldProps.flow';
export default class AudioResourceField extends Component<
ParameterFieldProps,
void
> {
focus() {
// TODO
}
render() {
if (!this.props.resourceSources || !this.props.onChooseResource) {
console.error("Missing resourceSources or onChooseResource for AudioResourceField");
return null;
}
return (
<ResourceSelector
project={this.props.project}
resourceSources={this.props.resourceSources}
onChooseResource={this.props.onChooseResource}
resourceKind="audio"
fullWidth
initialResourceName={this.props.value}
onChange={this.props.onChange}
floatingLabelText="Choose the audio file to use"
/>
);
}
}

View File

@@ -1,4 +1,10 @@
// @flow
import {
type ResourceSource,
type ChooseResourceFunction,
} from '../../../ResourcesList/ResourceSource.flow';
export type ParameterFieldProps = {|
parameterMetadata?: gdParameterMetadata,
project: gdProject,
@@ -10,4 +16,6 @@ export type ParameterFieldProps = {|
components: any,
getParameterComponent: (type: string) => any,
},
resourceSources?: Array<ResourceSource>,
onChooseResource?: ChooseResourceFunction,
|};

View File

@@ -14,6 +14,7 @@ import SceneVariableField from './ParameterFields/SceneVariableField';
import GlobalVariableField from './ParameterFields/GlobalVariableField';
import ObjectVariableField from './ParameterFields/ObjectVariableField';
import LayerField from './ParameterFields/LayerField';
import AudioResourceField from './ParameterFields/AudioResourceField';
const gd = global.gd;
const components = {
@@ -33,8 +34,8 @@ const components = {
layer: LayerField,
key: KeyField,
file: DefaultField, //TODO
musicfile: DefaultField, //TODO
soundfile: DefaultField, //TODO
musicfile: AudioResourceField,
soundfile: AudioResourceField,
color: DefaultField, //TODO
police: DefaultField, //TODO
joyaxis: DefaultField, //TODO

View File

@@ -42,6 +42,8 @@ export default class InstructionEditor extends Component {
layout={layout}
isCondition={isCondition}
instruction={instruction}
resourceSources={this.props.resourceSources}
onChooseResource={this.props.onChooseResource}
/>
</Paper>
</div>

View File

@@ -654,6 +654,8 @@ export default class EventsSheet extends Component {
this.closeInstructionEditor(true);
this._eventsTree.forceEventsUpdate();
}}
resourceSources={this.props.resourceSources}
onChooseResource={this.props.onChooseResource}
/>
)}
</div>

View File

@@ -45,6 +45,8 @@ export default class EventsEditor extends BaseEditor {
events={layout.getEvents()}
onPreview={(options) => this.props.onPreview(project, layout, options)}
onOpenExternalEvents={this.props.onOpenExternalEvents}
resourceSources={this.props.resourceSources}
onChooseResource={this.props.onChooseResource}
/>
);
}

View File

@@ -98,6 +98,8 @@ export default class ExternalEventsEditor extends BaseEditor {
onPreview={() => this.props.onPreview(project, layout)}
onOpenSettings={this.openLayoutChooser}
onOpenExternalEvents={this.props.onOpenExternalEvents}
resourceSources={this.props.resourceSources}
onChooseResource={this.props.onChooseResource}
/>
)}
{!layout && (

View File

@@ -527,8 +527,10 @@ export default class MainFrame extends React.Component<Props, State> {
openEventsEditor: true,
openSceneEditor: false,
})}
isActive={isActive}
ref={editorRef}
resourceSources={this.props.resourceSources}
onChooseResource={this._onChooseResource}
isActive={isActive}
ref={editorRef}
/>
),
key: 'layout events ' + name,
@@ -563,8 +565,8 @@ export default class MainFrame extends React.Component<Props, State> {
openEventsEditor: true,
openSceneEditor: false,
})}
isActive={isActive}
ref={editorRef}
isActive={isActive}
ref={editorRef}
/>
),
key: 'external events ' + name,
@@ -941,7 +943,8 @@ export default class MainFrame extends React.Component<Props, State> {
hideLabels={!!this.props.integratedEditor}
>
{getEditors(this.state.editorTabs).map((editorTab, id) => {
const isCurrentTab = getCurrentTabIndex(this.state.editorTabs) === id;
const isCurrentTab =
getCurrentTabIndex(this.state.editorTabs) === id;
return (
<Tab
label={editorTab.name}
@@ -952,7 +955,9 @@ export default class MainFrame extends React.Component<Props, State> {
closable={editorTab.closable}
>
<div style={{ display: 'flex', flex: 1, height: '100%' }}>
<ErrorBoundary>{editorTab.render(isCurrentTab)}</ErrorBoundary>
<ErrorBoundary>
{editorTab.render(isCurrentTab)}
</ErrorBoundary>
</div>
</Tab>
);

View File

@@ -111,9 +111,12 @@ export default class SpritesList extends Component {
project,
direction,
} = this.props;
if (!resourceSources || !resourceSources.length) return;
if (!resourceSources) return;
onChooseResource(resourceSources[0].name).then(resources => {
const sources = resourceSources.filter(source => source.kind === 'image');
if (!sources.length) return;
onChooseResource(sources[0].name).then(resources => {
resources.forEach(resource => {
project.getResourcesManager().addResource(resource);

View File

@@ -1,9 +1,25 @@
// @flow
import React from 'react';
import ResourcesLoader from '../ResourcesLoader';
import ResourceSelector from '../ResourcesList/ResourceSelector';
import {
type ResourceSource,
type ChooseResourceFunction,
type ResourceKind,
} from '../ResourcesList/ResourceSource.flow';
import ImageThumbnail from './ImageThumbnail';
export default ({
type Props = {|
project: gdProject,
resourceSources: Array<ResourceSource>,
onChooseResource: ChooseResourceFunction,
resourceKind: ResourceKind,
resourceName: string,
onChange: (string) => void,
floatingLabelText?: string,
|};
const ResourceSelectorWithThumbnail = ({
project,
resourceSources,
onChooseResource,
@@ -11,7 +27,7 @@ export default ({
resourceName,
onChange,
floatingLabelText,
}) => {
}: Props) => {
return (
<div style={{ flex: 1, display: 'flex', alignItems: 'flex-end' }}>
<div style={{ flex: 1 }}>
@@ -35,3 +51,5 @@ export default ({
</div>
);
};
export default ResourceSelectorWithThumbnail;

View File

@@ -219,7 +219,6 @@ export default class PlatformSpecificAssetsDialog extends React.Component<
onChooseResource={onChooseResource}
resourceKind="image"
resourceName={androidIconResourceNames[index]}
resourcesLoader={ResourcesLoader}
onChange={resourceName => {
const newIcons = [...androidIconResourceNames];
newIcons[index] = resourceName;
@@ -239,7 +238,6 @@ export default class PlatformSpecificAssetsDialog extends React.Component<
onChooseResource={onChooseResource}
resourceKind="image"
resourceName={iosIconResourceNames[index]}
resourcesLoader={ResourcesLoader}
onChange={resourceName => {
const newIcons = [...iosIconResourceNames];
newIcons[index] = resourceName;

View File

@@ -74,119 +74,171 @@ const publicImageUrls = [
'https://df5lqcdudryde.cloudfront.net/examples/space-shooter/laserRed11.png',
];
const publicAudioUrls = [
// Platformer audio (see platformer.json in fixtures)
'https://df5lqcdudryde.cloudfront.net/examples/platformer/jump.wav',
'https://df5lqcdudryde.cloudfront.net/examples/platformer/coin.wav',
// Space shooter audio (see space-shooter.json in fixtures)
'https://df5lqcdudryde.cloudfront.net/examples/space-shooter/sfx_laser1.ogg',
'https://df5lqcdudryde.cloudfront.net/examples/space-shooter/sfx_twoTone.ogg',
'https://df5lqcdudryde.cloudfront.net/examples/space-shooter/sfx_shieldDown.ogg',
'https://df5lqcdudryde.cloudfront.net/examples/space-shooter/sfx_lose.ogg',
];
const nameFromUrl = (url: string): string => {
const urlParts = url.split('/');
return urlParts[urlParts.length - 1].replace('.png', '');
return urlParts[urlParts.length - 1]
.replace('.png', '')
.replace('.wav', '')
.replace('.ogg', '');
};
class GenericResourcesChooser extends Component {
state = {
open: false,
resolveWithResources: null,
chosenResourceUrl: null,
};
constructor(props) {
super(props);
// Cache rendered public images list to avoid doing this each time
// a rendering is done.
this.listItems = props.urls.map((url: string) => {
return (
<ListItem
value={url}
key={url}
primaryText={nameFromUrl(url)}
leftAvatar={props.urlsAreImages && <ListIcon src={url} />}
/>
);
});
}
chooseResources = (project, multiSelections = true): Promise<Array<any>> => {
return new Promise(resolve => {
this.setState({
open: true,
resolveWithResources: resolve,
});
});
};
_onChoose = () => {
const { resolveWithResources, chosenResourceUrl } = this.state;
if (!resolveWithResources) return;
const newResource = this.props.createNewResource();
newResource.setFile(chosenResourceUrl);
newResource.setName(chosenResourceUrl);
resolveWithResources([newResource]);
this.setState({
open: false,
resolveWithResources: null,
});
};
_onClose = () => {
const { resolveWithResources } = this.state;
if (!resolveWithResources) return;
resolveWithResources([]);
this.setState({
open: false,
resolveWithResources: null,
});
};
_handleChangeResource = (e, chosenResourceUrl) => {
this.setState({
chosenResourceUrl,
});
};
render() {
// Avoid rendering the dialog if it's not opened.
if (!this.state.open) return null;
const actions = [
<FlatButton label="Close" primary={false} onClick={this._onClose} />,
<FlatButton
label="Choose"
primary={false}
disabled={!this.state.chosenResourceUrl}
onClick={this._onChoose}
/>,
];
return (
<Dialog
title={this.props.title}
actions={actions}
open={this.state.open}
noMargin
autoScrollBodyContent
>
<div style={styles.explanations}>
<p>
Adding resources from Dropbox, Google Drive... is coming soon!
Download GDevelop desktop version to use your own assets.
</p>
</div>
<SelectableList
value={this.state.chosenResourceUrl}
onChange={this._handleChangeResource}
>
{this.listItems}
</SelectableList>
</Dialog>
);
}
}
export default [
{
name: 'gdResourcesChooser',
name: 'publicAudioUrlChooser',
displayName: 'Choose an audio file from library',
kind: 'audio',
component: class AudioResourceChooser extends React.Component {
chooseResources = () => {
if (this._chooser) return this._chooser.chooseResources();
};
render() {
return (
<GenericResourcesChooser
{...this.props}
urls={publicAudioUrls}
urlsAreImages={false}
createNewResource={() => new gd.AudioResource()}
title="Choose an audio file from the library"
ref={chooser => (this._chooser = chooser)}
/>
);
}
},
},
{
name: 'publicImageUrlChooser',
displayName: 'Choose an image from library',
kind: 'image',
component: class GdResourcesChooser extends Component {
state = {
open: false,
resolveWithResources: null,
chosenImageUrl: null,
component: class AudioResourceChooser extends React.Component {
chooseResources = () => {
if (this._chooser) return this._chooser.chooseResources();
};
constructor(props) {
super(props);
// Cache rendered public images list to avoid doing this each time
// a rendering is done.
this.listItems = publicImageUrls.map((url: string) => {
return (
<ListItem
value={url}
key={url}
primaryText={nameFromUrl(url)}
leftAvatar={<ListIcon src={url} />}
/>
);
});
}
chooseResources = (
project,
multiSelections = true
): Promise<Array<any>> => {
return new Promise(resolve => {
this.setState({
open: true,
resolveWithResources: resolve,
});
});
};
_onChoose = () => {
const { resolveWithResources, chosenImageUrl } = this.state;
if (!resolveWithResources) return;
const imageResource = new gd.ImageResource();
imageResource.setFile(chosenImageUrl);
imageResource.setName(chosenImageUrl);
resolveWithResources([imageResource]);
this.setState({
open: false,
resolveWithResources: null,
});
};
_onClose = () => {
const { resolveWithResources } = this.state;
if (!resolveWithResources) return;
resolveWithResources([]);
this.setState({
open: false,
resolveWithResources: null,
});
};
_handleChangeImage = (e, chosenImageUrl) => {
this.setState({
chosenImageUrl,
});
};
render() {
// Avoid rendering the dialog if it's not opened.
if (!this.state.open) return null;
const actions = [
<FlatButton label="Close" primary={false} onClick={this._onClose} />,
<FlatButton
label="Choose"
primary={false}
disabled={!this.state.chosenImageUrl}
onClick={this._onChoose}
/>,
];
return (
<Dialog
<GenericResourcesChooser
{...this.props}
urls={publicImageUrls}
urlsAreImages
createNewResource={() => new gd.ImageResource()}
title="Choose an image from the library"
actions={actions}
open={this.state.open}
noMargin
autoScrollBodyContent
>
<div style={styles.explanations}>
<p>
Adding images from Dropbox, Google Drive... is coming soon!
Download GDevelop desktop version to use your own assets.
</p>
</div>
<SelectableList
value={this.state.chosenImageUrl}
onChange={this._handleChangeImage}
>
{this.listItems}
</SelectableList>
</Dialog>
ref={chooser => (this._chooser = chooser)}
/>
);
}
},

View File

@@ -6,6 +6,51 @@ const dialog = electron ? electron.remote.dialog : null;
const gd = global.gd;
export default [
{
name: 'localAudioFileOpener',
displayName: 'Choose a new audio file',
kind: 'audio',
component: class LocalAudioFileOpener extends Component {
chooseResources = (
project,
multiSelections = true
): Promise<Array<any>> => {
return new Promise((resolve, reject) => {
if (!dialog) return reject('Not supported');
const properties = ['openFile'];
if (multiSelections) properties.push('multiSelections');
const browserWindow = electron.remote.getCurrentWindow();
dialog.showOpenDialog(
browserWindow,
{
title: 'Choose an audio file',
properties,
filters: [{ name: 'Audio files', extensions: ['wav', 'mp3', 'ogg'] }],
},
paths => {
if (!paths) return resolve([]);
const resources = paths.map(resourcePath => {
const audioResource = new gd.AudioResource();
const projectPath = path.dirname(project.getProjectFile());
audioResource.setFile(path.relative(projectPath, resourcePath));
audioResource.setName(path.basename(resourcePath));
return audioResource;
});
return resolve(resources);
}
);
});
};
render() {
return null;
}
},
},
{
name: 'localFileOpener',
displayName: 'Choose a new image',

View File

@@ -1,12 +1,40 @@
import React, { Component } from 'react';
// @flow
import * as React from 'react';
import AutoComplete from 'material-ui/AutoComplete';
import MenuItem from 'material-ui/MenuItem';
import Divider from 'material-ui/Divider';
import Add from 'material-ui/svg-icons/content/add';
import { fuzzyOrEmptyFilter } from '../Utils/FuzzyOrEmptyFilter';
import {
type ResourceSource,
type ChooseResourceFunction,
type ResourceKind,
} from '../ResourcesList/ResourceSource.flow';
export default class ResourceSelector extends Component {
constructor(props) {
type Props = {|
project: gdProject,
resourceSources: Array<ResourceSource>,
onChooseResource: ChooseResourceFunction,
resourceKind: ResourceKind,
fullWidth?: boolean,
initialResourceName: string,
onChange: (string) => void,
floatingLabelText?: string,
|};
type State ={|
notExistingError: boolean,
resourceName: string,
|};
type AutoCompleteItem = {|
text: string,
value: React.Node,
onClick?: () => void,
|} | string;
export default class ResourceSelector extends React.Component<Props, State> {
constructor(props: Props) {
super(props);
this.state = {
@@ -20,7 +48,11 @@ export default class ResourceSelector extends Component {
}
}
componentWillReceiveProps(nextProps) {
allResourcesNames: Array<string>;
defaultItems: Array<AutoCompleteItem>;
autoCompleteData: ?Array<AutoCompleteItem>;
componentWillReceiveProps(nextProps: Props) {
if (nextProps.initialResourceName !== this.props.initialResourceName) {
this.setState({
resourceName: nextProps.initialResourceName || '',
@@ -28,7 +60,7 @@ export default class ResourceSelector extends Component {
}
}
_getDefaultItems() {
_getDefaultItems(): Array<AutoCompleteItem> {
const sources = this.props.resourceSources || [];
return [
...sources
@@ -47,7 +79,7 @@ export default class ResourceSelector extends Component {
];
}
_loadFrom(resourcesManager) {
_loadFrom(resourcesManager: gdResourcesManager) {
this.allResourcesNames = resourcesManager.getAllResourceNames().toJSArray();
if (this.props.resourceKind) {
this.allResourcesNames = this.allResourcesNames.filter(resourceName => {
@@ -61,7 +93,7 @@ export default class ResourceSelector extends Component {
this.autoCompleteData = [...this.defaultItems, ...this.allResourcesNames];
}
_addFrom = source => {
_addFrom = (source: ResourceSource) => {
if (!source) return;
const { project, onChooseResource } = this.props;
@@ -70,6 +102,8 @@ export default class ResourceSelector extends Component {
if (!resources.length) return;
const resource = resources[0];
// addResource will check if a resource with the same name exists, and if it is
// the case, no new resource will be added.
project.getResourcesManager().addResource(resource);
this._loadFrom(project.getResourcesManager());
@@ -81,7 +115,7 @@ export default class ResourceSelector extends Component {
});
};
_onUpdate = searchText => {
_onUpdate = (searchText: string) => {
this.setState(
{
resourceName: searchText,
@@ -99,7 +133,11 @@ export default class ResourceSelector extends Component {
if (index === -1 || index >= this.defaultItems.length)
return this._onUpdate(text);
this.defaultItems[index].onClick();
// We're now sure that onClick is defined
// $FlowFixMe
const onClick = this.defaultItems[index].onClick;
if (onClick)
onClick();
};
render() {

View File

@@ -1,10 +1,12 @@
// @flow
import * as React from 'react';
export type ResourceKind = 'image' | 'audio';
export type ResourceSource = {
name: string,
displayName: string,
kind: 'image' | 'audio',
kind: ResourceKind,
component: React.Component<*, *>,
};

View File

@@ -329,6 +329,9 @@ storiesOf('EventsSheet', module)
layout={testLayout}
events={testLayout.getEvents()}
onOpenExternalEvents={action('Open external events')}
resourceSources={[]}
onChooseResource={source =>
action('Choose resource from source', source)}
/>
</DragDropContextProvider>
))
@@ -339,6 +342,9 @@ storiesOf('EventsSheet', module)
layout={emptyLayout}
events={emptyLayout.getEvents()}
onOpenExternalEvents={action('Open external events')}
resourceSources={[]}
onChooseResource={source =>
action('Choose resource from source', source)}
/>
</DragDropContextProvider>
));