feat: Sound
This commit is contained in:
parent
c14cc581f1
commit
60a308a0ed
|
@ -41,6 +41,7 @@
|
|||
"@persea/core": "^1.0.0",
|
||||
"@persea/entity": "^1.0.0",
|
||||
"@persea/json": "^1.0.0",
|
||||
"@persea/sound": "^1.0.0",
|
||||
"@persea/timing": "^1.0.0",
|
||||
"connected-react-router": "^6.8.0",
|
||||
"dotenv": "8.2.0",
|
||||
|
|
|
@ -1485,6 +1485,16 @@
|
|||
fast-json-patch "^3.0.0-1"
|
||||
react-syntax-highlighter "^15.4.3"
|
||||
|
||||
"@persea/sound@^1.0.0":
|
||||
version "1.0.0"
|
||||
resolved "http://npm.cha0sdev/@persea%2fsound/-/sound-1.0.0.tgz#fde466c89f24323311145d26150f3e74bfbdbbc6"
|
||||
integrity sha512-T1r7Vxt3DJljcrRv9MiVviS66GDy2BPM2Y4jDkGc0J8heleiJtxCUDr2k/qQN1lMFVWcUteR87yhLAzmzOGYKw==
|
||||
dependencies:
|
||||
"@latus/core" "^2.0.0"
|
||||
"@latus/react" "^2.0.0"
|
||||
"@persea/core" "^1.0.0"
|
||||
debug "4.3.1"
|
||||
|
||||
"@persea/timing@^1.0.0":
|
||||
version "1.0.0"
|
||||
resolved "http://npm.cha0sdev/@persea%2ftiming/-/timing-1.0.0.tgz#6163db519682e69f51e914fc7c447961963c7b12"
|
||||
|
|
1
packages/sound/.eslintrc.js
Normal file
1
packages/sound/.eslintrc.js
Normal file
|
@ -0,0 +1 @@
|
|||
module.exports = require('../../config/.eslintrc');
|
7
packages/sound/.gitignore
vendored
Normal file
7
packages/sound/.gitignore
vendored
Normal file
|
@ -0,0 +1,7 @@
|
|||
**/*.js
|
||||
**/*.map
|
||||
!/.*
|
||||
!/postcss.config.js
|
||||
!/webpack.config.js
|
||||
!src/**/*.js
|
||||
!/test/**/*.js
|
1
packages/sound/.neutrinorc.js
Normal file
1
packages/sound/.neutrinorc.js
Normal file
|
@ -0,0 +1 @@
|
|||
module.exports = require('../../config/.neutrinorc');
|
45
packages/sound/package.json
Normal file
45
packages/sound/package.json
Normal file
|
@ -0,0 +1,45 @@
|
|||
{
|
||||
"name": "@persea/sound",
|
||||
"version": "1.0.0",
|
||||
"main": "index.js",
|
||||
"author": "cha0s",
|
||||
"license": "MIT",
|
||||
"scripts": {
|
||||
"build": "NODE_PATH=./node_modules webpack --mode production",
|
||||
"clean": "rm -rf yarn.lock node_modules $(node -e \"process.stdout.write(require('./package.json').files.filter((file) => {const parts = file.split('/'); return 1 === parts.length || 'test' !== parts[0];}).join(' '));\") && yarn",
|
||||
"forcepub": "npm unpublish --force $(node -e 'const {name, version} = require(`./package.json`); process.stdout.write(`${name}@${version}`)') && npm publish",
|
||||
"lint": "NODE_PATH=./node_modules eslint --format codeframe --ext mjs,js .",
|
||||
"test": "yarn --silent run build --display none && mocha --colors test.js"
|
||||
},
|
||||
"files": [
|
||||
"index.js",
|
||||
"index.js.map",
|
||||
"test.js",
|
||||
"test.js.map"
|
||||
],
|
||||
"dependencies": {
|
||||
"@avocado/resource": "^2.0.0",
|
||||
"@latus/core": "^2.0.0",
|
||||
"@latus/react": "^2.0.0",
|
||||
"@persea/core": "^1.0.0",
|
||||
"@persea/json": "^1.0.0",
|
||||
"debug": "4.3.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@neutrinojs/airbnb": "^9.4.0",
|
||||
"@neutrinojs/banner": "^9.4.0",
|
||||
"@neutrinojs/copy": "^9.4.0",
|
||||
"@neutrinojs/mocha": "^9.4.0",
|
||||
"@neutrinojs/react": "^9.4.0",
|
||||
"autoprefixer": "^9.8.6",
|
||||
"chai": "4.2.0",
|
||||
"eslint": "^7",
|
||||
"eslint-import-resolver-webpack": "0.13.0",
|
||||
"glob": "7.1.6",
|
||||
"mocha": "^8",
|
||||
"neutrino": "^9.4.0",
|
||||
"source-map-support": "0.5.19",
|
||||
"webpack": "^4",
|
||||
"webpack-cli": "^3"
|
||||
}
|
||||
}
|
6
packages/sound/postcss.config.js
Normal file
6
packages/sound/postcss.config.js
Normal file
|
@ -0,0 +1,6 @@
|
|||
/* eslint-disable global-require */
|
||||
module.exports = {
|
||||
plugins: [
|
||||
require('autoprefixer'),
|
||||
],
|
||||
};
|
27
packages/sound/src/index.js
Normal file
27
packages/sound/src/index.js
Normal file
|
@ -0,0 +1,27 @@
|
|||
import {basename, extname} from 'path';
|
||||
|
||||
import {camelCase} from '@latus/core';
|
||||
|
||||
import AudioController from './resource-controllers/audio';
|
||||
import SoundController from './resource-controllers/sound';
|
||||
|
||||
export {
|
||||
AudioController,
|
||||
SoundController,
|
||||
};
|
||||
|
||||
export default {
|
||||
hooks: {
|
||||
'@persea/core/resource-controllers': () => [
|
||||
AudioController,
|
||||
SoundController,
|
||||
],
|
||||
'@persea/entity/trait-components': () => {
|
||||
const context = require.context('./trait-components', false, /\.jsx$/);
|
||||
return context.keys().reduce((r, key) => ({
|
||||
...r,
|
||||
[camelCase(basename(key, extname(key)))]: context(key).default,
|
||||
}), {});
|
||||
},
|
||||
},
|
||||
};
|
42
packages/sound/src/resource-controllers/audio/index.jsx
Normal file
42
packages/sound/src/resource-controllers/audio/index.jsx
Normal file
|
@ -0,0 +1,42 @@
|
|||
import {
|
||||
PropTypes,
|
||||
React,
|
||||
} from '@latus/react';
|
||||
import {
|
||||
BinaryResourceController,
|
||||
} from '@persea/core';
|
||||
|
||||
export const AudioComponent = ({resource}) => {
|
||||
// eslint-disable-next-line no-use-before-define
|
||||
const buffer = AudioController.decode(resource);
|
||||
const objectUrl = URL.createObjectURL(new Blob([buffer]));
|
||||
return (
|
||||
<div className="audio">
|
||||
{/* eslint-disable-next-line jsx-a11y/media-has-caption */}
|
||||
<audio
|
||||
controls
|
||||
src={objectUrl}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
AudioComponent.displayName = 'AudioComponent';
|
||||
|
||||
AudioComponent.propTypes = {
|
||||
resource: PropTypes.string.isRequired,
|
||||
};
|
||||
|
||||
export default class AudioController extends BinaryResourceController {
|
||||
|
||||
static Component({resource}) {
|
||||
return (
|
||||
<AudioComponent resource={resource} />
|
||||
);
|
||||
}
|
||||
|
||||
static get matcher() {
|
||||
return /\.(?:wav|ogg|mp3)$/;
|
||||
}
|
||||
|
||||
}
|
5
packages/sound/src/resource-controllers/audio/index.scss
Normal file
5
packages/sound/src/resource-controllers/audio/index.scss
Normal file
|
@ -0,0 +1,5 @@
|
|||
.audio {
|
||||
height: 100%;
|
||||
padding: 1em;
|
||||
width: 100%;
|
||||
}
|
112
packages/sound/src/resource-controllers/sound/index.jsx
Normal file
112
packages/sound/src/resource-controllers/sound/index.jsx
Normal file
|
@ -0,0 +1,112 @@
|
|||
import {join} from 'path';
|
||||
|
||||
import {
|
||||
PropTypes,
|
||||
React,
|
||||
} from '@latus/react';
|
||||
import {
|
||||
Number,
|
||||
} from '@persea/core';
|
||||
import {
|
||||
JsonResourceController,
|
||||
useJsonPatcher,
|
||||
} from '@persea/json';
|
||||
|
||||
import SoundSource from './src';
|
||||
|
||||
export const SoundComponent = ({
|
||||
path,
|
||||
resource,
|
||||
}) => {
|
||||
const patch = useJsonPatcher();
|
||||
const sources = (resource.src || []).map((src, i) => (
|
||||
<div className="sound__audio" key={src}>
|
||||
{/* eslint-disable-next-line jsx-a11y/media-has-caption */}
|
||||
<input
|
||||
className="sound__src"
|
||||
onChange={(event) => {
|
||||
patch({
|
||||
path: join(path, 'src', i.toString()),
|
||||
value: event.target.value,
|
||||
});
|
||||
}}
|
||||
type="text"
|
||||
value={src}
|
||||
/>
|
||||
<SoundSource src={src} volume={resource.volume} />
|
||||
<button
|
||||
className="sound__source-remove"
|
||||
onClick={() => {
|
||||
patch({
|
||||
op: 'remove',
|
||||
path: join(path, 'src', i.toString()),
|
||||
});
|
||||
}}
|
||||
type="button"
|
||||
>
|
||||
✕
|
||||
</button>
|
||||
</div>
|
||||
));
|
||||
return (
|
||||
<div className="sound">
|
||||
<div className="sound__sources-label label">
|
||||
<div className="vertical">Sources</div>
|
||||
<div className="sound__sources">
|
||||
{sources}
|
||||
<hr />
|
||||
<button
|
||||
className="sound__source-add"
|
||||
onClick={() => {
|
||||
patch({
|
||||
op: 'add',
|
||||
path: join(path, 'src/-'),
|
||||
value: '',
|
||||
});
|
||||
}}
|
||||
type="button"
|
||||
>
|
||||
Add source
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="label">
|
||||
Volume
|
||||
<Number
|
||||
onChange={(event, value) => {
|
||||
patch({
|
||||
path: join(path, 'volume'),
|
||||
value,
|
||||
});
|
||||
}}
|
||||
value={resource.volume || 1}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
SoundComponent.defaultProps = {
|
||||
path: '/',
|
||||
};
|
||||
|
||||
SoundComponent.displayName = 'SoundComponent';
|
||||
|
||||
SoundComponent.propTypes = {
|
||||
path: PropTypes.string,
|
||||
resource: PropTypes.string.isRequired,
|
||||
};
|
||||
|
||||
export default class SoundController extends JsonResourceController {
|
||||
|
||||
static Component({resource}) {
|
||||
return (
|
||||
<SoundComponent resource={resource} />
|
||||
);
|
||||
}
|
||||
|
||||
static get matcher() {
|
||||
return /\.sound\.json$/;
|
||||
}
|
||||
|
||||
}
|
38
packages/sound/src/resource-controllers/sound/index.scss
Normal file
38
packages/sound/src/resource-controllers/sound/index.scss
Normal file
|
@ -0,0 +1,38 @@
|
|||
.sound {
|
||||
padding: 1em;
|
||||
hr {
|
||||
flex-basis: 100%;
|
||||
height: 0;
|
||||
margin: 0;
|
||||
border: 0;
|
||||
}
|
||||
}
|
||||
|
||||
// .sound__sources-label.label {
|
||||
// flex-wrap: wrap;
|
||||
// }
|
||||
|
||||
.sound__audio {
|
||||
align-items: center;
|
||||
display: flex;
|
||||
padding: 0.5em;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.sound-source button {
|
||||
margin-left: 0.5em;
|
||||
}
|
||||
|
||||
.sound__source-remove {
|
||||
background-color: transparent;
|
||||
border: none;
|
||||
height: 2em;
|
||||
margin: 0;
|
||||
margin-left: auto;
|
||||
padding: 0;
|
||||
width: 2em;
|
||||
}
|
||||
|
||||
.sound__source-add {
|
||||
margin: 0.5em;
|
||||
}
|
65
packages/sound/src/resource-controllers/sound/src.jsx
Normal file
65
packages/sound/src/resource-controllers/sound/src.jsx
Normal file
|
@ -0,0 +1,65 @@
|
|||
import {Resource} from '@avocado/resource';
|
||||
import {
|
||||
PropTypes,
|
||||
React,
|
||||
useEffect,
|
||||
useRef,
|
||||
useState,
|
||||
} from '@latus/react';
|
||||
|
||||
const SoundSource = ({
|
||||
src,
|
||||
volume,
|
||||
}) => {
|
||||
const ref = useRef();
|
||||
const [url, setUrl] = useState();
|
||||
useEffect(() => {
|
||||
const loadAudio = async () => {
|
||||
setUrl(URL.createObjectURL(new Blob([await Resource.read(src)])));
|
||||
};
|
||||
loadAudio();
|
||||
return () => {
|
||||
URL.revokeObjectURL(url);
|
||||
};
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [src]);
|
||||
useEffect(() => {
|
||||
if (!ref.current) {
|
||||
return;
|
||||
}
|
||||
ref.current.volume = volume;
|
||||
});
|
||||
return (
|
||||
<div className="sound-source">
|
||||
{/* eslint-disable-next-line jsx-a11y/media-has-caption */}
|
||||
<audio
|
||||
ref={ref}
|
||||
src={url}
|
||||
/>
|
||||
<button
|
||||
onClick={() => {
|
||||
if (ref.current) {
|
||||
ref.current.currentTime = 0;
|
||||
ref.current.play();
|
||||
}
|
||||
}}
|
||||
type="button"
|
||||
>
|
||||
⊳
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
SoundSource.defaultProps = {
|
||||
volume: 1.0,
|
||||
};
|
||||
|
||||
SoundSource.displayName = 'SoundSource';
|
||||
|
||||
SoundSource.propTypes = {
|
||||
src: PropTypes.string.isRequired,
|
||||
volume: PropTypes.number,
|
||||
};
|
||||
|
||||
export default SoundSource;
|
106
packages/sound/src/trait-components/audible.jsx
Normal file
106
packages/sound/src/trait-components/audible.jsx
Normal file
|
@ -0,0 +1,106 @@
|
|||
import {join} from 'path';
|
||||
|
||||
import {PropTypes, React} from '@latus/react';
|
||||
import {useJsonPatcher} from '@persea/json';
|
||||
|
||||
const Audible = ({json, path}) => {
|
||||
const patch = useJsonPatcher();
|
||||
const soundsPath = join(path, 'params/sounds');
|
||||
const sounds = Object.entries(json.params.sounds).map(([key, sound]) => (
|
||||
<div className="audible__sound" key={key}>
|
||||
<label>
|
||||
Key
|
||||
<input
|
||||
onChange={(event) => {
|
||||
patch({
|
||||
op: 'move',
|
||||
from: join(soundsPath, key),
|
||||
path: join(soundsPath, event.target.value),
|
||||
});
|
||||
}}
|
||||
type="text"
|
||||
value={key}
|
||||
/>
|
||||
</label>
|
||||
<label>
|
||||
URI
|
||||
<input
|
||||
onChange={(event) => {
|
||||
if (event.target.value) {
|
||||
patch({
|
||||
path: join(soundsPath, key, 'extends'),
|
||||
value: event.target.value,
|
||||
});
|
||||
}
|
||||
}}
|
||||
type="text"
|
||||
value={sound.extends}
|
||||
/>
|
||||
</label>
|
||||
<button
|
||||
className="audible__sound-remove"
|
||||
onClick={() => {
|
||||
patch({
|
||||
op: 'remove',
|
||||
path: join(soundsPath, key),
|
||||
});
|
||||
}}
|
||||
type="button"
|
||||
>
|
||||
✕
|
||||
</button>
|
||||
</div>
|
||||
));
|
||||
return (
|
||||
<div className="audible">
|
||||
<div className="label">
|
||||
<div className="vertical">Sounds</div>
|
||||
<div className="audible__sounds">
|
||||
{sounds}
|
||||
<button
|
||||
onClick={() => {
|
||||
let i = 1;
|
||||
let name = 'untitled';
|
||||
while (json.params.sounds[name]) {
|
||||
name = `untitled (${i++})`;
|
||||
}
|
||||
patch({
|
||||
op: 'add',
|
||||
path: join(soundsPath, name),
|
||||
value: {
|
||||
extends: '',
|
||||
},
|
||||
});
|
||||
}}
|
||||
type="button"
|
||||
>
|
||||
Add a sound
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
Audible.defaultProps = {};
|
||||
|
||||
Audible.displayName = 'Audible';
|
||||
|
||||
Audible.propTypes = {
|
||||
entity: PropTypes.shape({
|
||||
context: PropTypes.shape({}),
|
||||
}).isRequired,
|
||||
json: PropTypes.shape({
|
||||
params: PropTypes.shape({
|
||||
sounds: PropTypes.objectOf(PropTypes.shape({
|
||||
uri: PropTypes.string,
|
||||
})),
|
||||
}),
|
||||
state: PropTypes.shape({
|
||||
}),
|
||||
}).isRequired,
|
||||
path: PropTypes.string.isRequired,
|
||||
};
|
||||
Audible.propTypes = {};
|
||||
|
||||
export default Audible;
|
16
packages/sound/src/trait-components/audible.scss
Normal file
16
packages/sound/src/trait-components/audible.scss
Normal file
|
@ -0,0 +1,16 @@
|
|||
.audible__sound {
|
||||
> label {
|
||||
display: inline-flex;
|
||||
width: calc(50% - 1.5em);
|
||||
}
|
||||
}
|
||||
|
||||
.audible__sound-remove {
|
||||
background-color: transparent;
|
||||
border: none;
|
||||
height: 2em;
|
||||
margin: 0;
|
||||
margin-left: 0.5em;
|
||||
padding: 0;
|
||||
width: 2em;
|
||||
}
|
5
packages/sound/test/exists.js
Normal file
5
packages/sound/test/exists.js
Normal file
|
@ -0,0 +1,5 @@
|
|||
import {expect} from 'chai';
|
||||
|
||||
it('exists', () => {
|
||||
expect(true).to.be.true;
|
||||
});
|
3
packages/sound/webpack.config.js
Normal file
3
packages/sound/webpack.config.js
Normal file
|
@ -0,0 +1,3 @@
|
|||
const neutrino = require('neutrino');
|
||||
|
||||
module.exports = neutrino(require(`${__dirname}/.neutrinorc`)).webpack();
|
8951
packages/sound/yarn.lock
Normal file
8951
packages/sound/yarn.lock
Normal file
File diff suppressed because it is too large
Load Diff
Loading…
Reference in New Issue
Block a user