Skip to content

Commit

Permalink
Merge pull request #695 from blurymind/master
Browse files Browse the repository at this point in the history
 Add jsfx sound effects editor
  • Loading branch information
4ian committed Oct 20, 2018
2 parents 9f5b63b + 82442fe commit 6a163a5
Show file tree
Hide file tree
Showing 13 changed files with 538 additions and 99 deletions.
3 changes: 2 additions & 1 deletion newIDE/app/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
"i18next": "^10.0.3",
"keen-tracking": "1.1.3",
"lodash": "4.17.4",
"loov-jsfx": "1.2.0",
"material-ui": "0.20",
"material-ui-search-bar": "0.4.1",
"node-require-function": "^1.2.0",
Expand Down Expand Up @@ -60,7 +61,7 @@
},
"scripts": {
"postinstall": "npm run import-resources",
"import-resources": "cd scripts && node import-libGD.js && node import-res-folder.js && node import-GDJS-Runtime.js && node import-piskel-editor.js && node import-monaco-editor.js",
"import-resources": "cd scripts && node import-libGD.js && node import-res-folder.js && node import-GDJS-Runtime.js && node import-piskel-editor.js && node import-monaco-editor.js && node import-jsfx-editor.js",
"start": "npm run import-resources && react-scripts start",
"build": "npm run import-resources && react-scripts build",
"format": "prettier --write \"src/**/*.js\"",
Expand Down
138 changes: 138 additions & 0 deletions newIDE/app/public/External/Utils/pathEditor.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
const electron = require('electron');
const fs = require('fs');
const remote = electron.remote;
const {
dialog
} = remote;

export const createPathEditorHeader = ({
parentElement,
editorContentDocument,
onSaveToGd,
onCancelChanges,
projectPath,
initialResourcePath,
extension,
headerStyle,
}) => {
if (fs.lstatSync(initialResourcePath).isDirectory()) {
initialResourcePath = initialResourcePath + '/NewFile' + extension;
}

const headerObject = {
state: {
folderPath: initialResourcePath.substring(
0,
initialResourcePath.lastIndexOf('/')
),
name: initialResourcePath.substring(
initialResourcePath.lastIndexOf('/') + 1,
initialResourcePath.lastIndexOf('.')
),
extension: initialResourcePath.substring(
initialResourcePath.lastIndexOf('.'),
initialResourcePath.length
),
projectBasePath: projectPath,
},
};

const state = headerObject.state;
// create the dom elements of the ui
headerObject.saveFolderLabel = editorContentDocument.createElement('label');
headerObject.saveFolderLabel.style = headerStyle.saveFolderLabel;
headerObject.saveFolderLabel.textContent = state.folderPath;
parentElement.appendChild(headerObject.saveFolderLabel);

headerObject.nameInput = editorContentDocument.createElement('input');
headerObject.nameInput.type = 'text';
headerObject.nameInput.style = headerStyle.nameInput;
headerObject.nameInput.value = state.name;
parentElement.appendChild(headerObject.nameInput);

headerObject.fileExistsLabel = editorContentDocument.createElement('label');
headerObject.fileExistsLabel.style = headerStyle.fileExistsLabel;
parentElement.appendChild(headerObject.fileExistsLabel);

headerObject.saveButton = editorContentDocument.createElement('button');
headerObject.saveButton.textContent = 'Save';
headerObject.saveButton.style = headerStyle.saveButton;
parentElement.appendChild(headerObject.saveButton);

headerObject.cancelButton = editorContentDocument.createElement('button');
headerObject.cancelButton.textContent = 'Cancel';
headerObject.cancelButton.style = headerStyle.cancelButton;
parentElement.appendChild(headerObject.cancelButton);

headerObject.setFolderButton = editorContentDocument.createElement('button');
headerObject.setFolderButton.textContent = 'Set Folder';
headerObject.setFolderButton.style = headerStyle.setFolderButton;
parentElement.appendChild(headerObject.setFolderButton);

// From here on we hook the dom with the imported or local methods via event listeners
headerObject.nameInput.addEventListener('input', () => {
render(headerObject);
});
headerObject.saveButton.addEventListener('click', () => {
onSaveToGd(headerObject);
});
headerObject.cancelButton.addEventListener('click', onCancelChanges);
headerObject.saveFolderLabel.addEventListener('click', () => {
selectBaseFolderPath(headerObject);
});
headerObject.setFolderButton.addEventListener('click', () => {
selectBaseFolderPath(headerObject);
});
render(headerObject);
};

const render = headerObject => {
headerObject.nameInput.value = headerObject.nameInput.value.replace(
/[^a-zA-Z0-9_-]/g,
''
); // Don't allow the user to enter any characters that would lead to an invalid path
const state = headerObject.state;
state.name = headerObject.nameInput.value;
state.baseExportPath = state.folderPath + '/' + state.name;
state.fullPath = state.folderPath + '/' + state.name + state.extension;
headerObject.saveFolderLabel.textContent = state.folderPath + '/';
headerObject.saveFolderLabel.title =
'Click to change path: \n' + state.folderPath;

headerObject.nameInput.style.width =
(headerObject.nameInput.value.length + 1) * 10 + 'px';
// check if it will overwrite a file and notify the user in a subtle, but obvious way
if (fs.existsSync(state.fullPath)) {
headerObject.fileExistsLabel.style.color = 'red';
headerObject.fileExistsLabel.textContent =
state.extension + ' (Overwrite)';
} else {
headerObject.fileExistsLabel.style.color = 'grey';
headerObject.fileExistsLabel.textContent = state.extension + ' (New)';
}
};

const selectBaseFolderPath = headerObject => {
const state = headerObject.state;
if (!state.projectBasePath) {
state.projectBasePath = state.folderPath;
}
const selectedDir = dialog.showOpenDialog(remote.getCurrentWindow(), {
properties: ['openDirectory'],
defaultPath: state.projectBasePath,
});
if (!selectedDir) {
return;
}
if (!selectedDir.toString().startsWith(state.projectBasePath)) {
alert(
'Please select a folder inside your project path!\n' +
state.projectBasePath +
'\n\nSelected:\n' +
selectedDir
);
return;
}
state.folderPath = selectedDir;
render(headerObject);
};
14 changes: 14 additions & 0 deletions newIDE/app/public/External/jsfx/jsfx-index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
<html>

<head>
<title>GDevelop Jsfx Editor</title>
<link rel="stylesheet" type="text/css" href="jsfx-style.css">
</head>

<body>
<div id='path-editor-header'></div>
<iframe id='jsfx-frame' src="loov-jsfx/index.html"></iframe>
<script type="module" src="jsfx-main.js"></script>
</body>

</html>
104 changes: 104 additions & 0 deletions newIDE/app/public/External/jsfx/jsfx-main.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
import {
createPathEditorHeader
} from '../Utils/pathEditor.js';

const electron = require('electron');
const ipcRenderer = electron.ipcRenderer;
const fs = require('fs');
const remote = electron.remote;

let editorContentDocument,
jsfx = null;

const loadMetaData = metaData => {
jsfx.CurrentParams = metaData;
jsfx.UpdateCurrentView();
jsfx.PlayCurrent();
};

const closeWindow = () => {
remote.getCurrentWindow().close();
};

const saveSoundEffect = pathEditor => {
jsfx.UpdateDownloadLink(); //Update base64 data
const rawData = editorContentDocument
.getElementById('download')
.href.replace(/^data:audio\/wav;base64,/, '');
fs.writeFile(pathEditor.state.fullPath, rawData, 'base64', err => {
ipcRenderer.send(
'jsfx-changes-saved',
pathEditor.state.fullPath,
jsfx.CurrentParams
);
closeWindow();
});
};

// we need to first declare when the window is ready to be initiated
document.addEventListener('DOMContentLoaded', function () {
ipcRenderer.send('jsfx-ready');
});
// then trigger bellow from main, this ensures the dom is loaded
ipcRenderer.on('jsfx-open', (event, receivedOptions) => {
const editorFrameEl = document.getElementById('jsfx-frame');
jsfx = editorFrameEl.contentWindow;
editorContentDocument = editorFrameEl.contentDocument;
const presetsPanel = editorContentDocument.getElementById('presets');

// Load metadata, if it exists
if ('jsfx' in receivedOptions.metadata) {
loadMetaData(receivedOptions.metadata.jsfx);
} else {
// If not, simulate a click on the 'Lucky' button to create a random sound effect
const generateRandomSoundEffectButton = presetsPanel.childNodes[11];
generateRandomSoundEffectButton.click();
}
// load a custom header
const pathEditorHeaderDiv = document.getElementById('path-editor-header');
const headerStyle = {
saveFolderLabel: 'height:27px;color:SlateGrey;float: left;margin-left: 2px;margin-top: 10px; font-size:15px;',
nameInput: 'font-family:"Courier New";height:27px;width:90px;color:SlateGrey;float:left;margin-left: 2px;padding:4px;margin-top: 4px;font-size:15px;border: 2px solid #e5cd50;border-radius: 3px; ',
fileExistsLabel: 'height:27px;color:blue;float: left;margin-left: 2px;margin-top: 10px; font-size:15px;',
saveButton: 'height:27px;float:right;margin-left:2px;margin-right:4px;border: 2px solid DeepSkyBlue;border-radius: 1px;margin-top: 5px;background-color:white;',
cancelButton: 'height:27px;float:right;margin-right:2px;border: 2px solid DeepSkyBlue;border-radius: 1px;margin-top: 5px;background-color:white;',
setFolderButton: 'height:27px;float:right;margin-left:2px;margin-right:4px;border: 2px solid DeepSkyBlue;border-radius: 1px;margin-top: 5px;background-color:white;',
};
createPathEditorHeader({
parentElement: pathEditorHeaderDiv,
editorContentDocument: document,
onSaveToGd: saveSoundEffect,
onCancelChanges: closeWindow,
projectPath: receivedOptions.projectPath,
initialResourcePath: receivedOptions.resourcePath,
extension: '.wav',
headerStyle,
});

// alter the interface of the external editor
editorContentDocument.getElementById('jsfx').firstChild.style = 'float:top';
const defaultTitle = editorContentDocument.getElementsByClassName('title')[0]
.firstChild;
defaultTitle.remove();

presetsPanel.className = 'description';
presetsPanel.style = 'float:left;';

const generatorsTitle = editorContentDocument.getElementsByClassName(
'panel-title'
)[1].firstChild;
generatorsTitle.data = 'Create a Random Sound Effect:';

const description = editorContentDocument.getElementsByClassName(
'description'
)[0];
description.remove();

const libraryPanel = editorContentDocument.getElementsByClassName(
'panel wide'
)[0];
libraryPanel.style = 'visibility:hidden;height:0px;width:0px';

const defaultButtons = editorContentDocument.getElementById('control');
defaultButtons.style = 'visibility:hidden;height:0px;width:0px';
});
18 changes: 18 additions & 0 deletions newIDE/app/public/External/jsfx/jsfx-style.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
/**
* Additional styles for the embedded Jsfx editor
*/

html,
body {
font-family: Helvetica;
margin: 0;
overflow-y: hidden;
background-color: white;
}

#jsfx-frame {
width: 100%;
height: 100%;
border: none;
overflow-y: hidden;
}
20 changes: 20 additions & 0 deletions newIDE/app/scripts/import-jsfx-editor.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
var shell = require('shelljs');

var source = '../node_modules/loov-jsfx';

var success = true;
success &= shell.mkdir('-p', '../public/External/jsfx');
success &= shell.cp(
'-Rf',
source,
'../public/External/jsfx'
);
if (success) {
shell.echo(
`❌ Error(s) occurred while copying Jsfx Editor sources from node_modules/loov-jsfx`
);
} else {
shell.echo(
'✅ Sources of Jsfx Editor properly copied in public folder'
);
}
53 changes: 53 additions & 0 deletions newIDE/app/src/ResourcesList/LocalJsfxBridge.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import optionalRequire from '../Utils/OptionalRequire.js';
import { type ExternalEditorOpenOptions } from './ResourceExternalEditor.flow';

const electron = optionalRequire('electron');
const path = optionalRequire('path');
const ipcRenderer = electron ? electron.ipcRenderer : null;
const gd = global.gd;

/**
* Open JSFX to create wav resources.
*/
export const openJsfx = ({
project,
resourcesLoader,
resourceNames,
onChangesSaved,
resourcePath,
extraOptions,
}: ExternalEditorOpenOptions) => {
if (!electron || !ipcRenderer) return;
const projectPath = path.dirname(project.getProjectFile());

let initialResourcePath = '';
initialResourcePath = resourcesLoader.getFullUrl(project, resourceNames[0]);
initialResourcePath = initialResourcePath.substring(
7,
initialResourcePath.lastIndexOf('?cache=')
);

const jsfxData = {
resourcePath: initialResourcePath,
metadata: extraOptions.initialResourceMetadata,
projectPath,
};

ipcRenderer.removeAllListeners('jsfx-changes-saved');
ipcRenderer.on('jsfx-changes-saved', (event, newFilePath, fileMetadata) => {
const resourcesManager = project.getResourcesManager();
const resourceName = path.relative(projectPath, newFilePath); //Still needed for onChangesSaved()
const audioResource = new gd.AudioResource();
audioResource.setFile(resourceName);
audioResource.setName(resourceName);
resourcesManager.addResource(audioResource);
audioResource.delete();
const newMetadata = {
jsfx: fileMetadata,
};

onChangesSaved([{ metadata: newMetadata }], resourceName);
});

ipcRenderer.send('jsfx-create-wav', jsfxData);
};
10 changes: 10 additions & 0 deletions newIDE/app/src/ResourcesList/LocalResourceExternalEditors.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
// @flow
import { openPiskel } from './LocalPiskelBridge';
import { openJsfx } from './LocalJsfxBridge';
import { type ResourceExternalEditor } from './ResourceExternalEditor.flow';
import { sendExternalEditorOpened } from '../Utils/Analytics/EventSender';

Expand All @@ -17,6 +18,15 @@ const editors: Array<ResourceExternalEditor> = [
return openPiskel(options);
},
},
{
name: 'Jsfx',
displayName: 'Create a New Sound effect with Jsxf (*.wav)',
kind: 'audio',
edit: (options) => {
sendExternalEditorOpened('jsfx');
return openJsfx(options);
},
},
];

export default editors;

0 comments on commit 6a163a5

Please sign in to comment.