Skip to content
This repository has been archived by the owner on Oct 20, 2022. It is now read-only.

Commit

Permalink
Add H5P plugin
Browse files Browse the repository at this point in the history
* Create basic H5P for local development with hard-coded paths
* Wrap Minio-Client into priority queue
* Enable creation of new documents
  • Loading branch information
ahelmberger committed Jun 24, 2018
1 parent 79cd2dd commit add8c1c
Show file tree
Hide file tree
Showing 939 changed files with 169,105 additions and 47 deletions.
2 changes: 2 additions & 0 deletions .eslintignore
@@ -1 +1,3 @@
dist/**
test/**
src/plugins/*/static/**
11 changes: 11 additions & 0 deletions db-seed
Expand Up @@ -109,6 +109,17 @@ const section4 = {
de: section0
}
},
{
_id: null,
key: 'Sylsu$4ke8',
order: null,
type: 'h5p-player',
updatedContent: {
de: {
contentId: 'rkN0Hrnb7'
}
}
},
{
_id: null,
key: 'HybdE$6pTf',
Expand Down
10 changes: 8 additions & 2 deletions gulpfile.js
Expand Up @@ -64,8 +64,14 @@ const ensureContainerRunning = async ({ containerName, runArgs, afterRun }) => {

const ensureContainerRemoved = async ({ containerName }) => {
const docker = new Docker();
await docker.command(`rm -f ${containerName}`);
await delay(1000);
try {
await docker.command(`rm -f ${containerName}`);
await delay(1000);
} catch (err) {
if (!err.toString().includes('No such container')) {
throw err;
}
}
};

gulp.task('clean', () => {
Expand Down
5 changes: 5 additions & 0 deletions package.json
Expand Up @@ -20,6 +20,7 @@
"dependencies": {
"antd": "~3.6.2",
"array-shuffle": "~1.0.1",
"async": "~2.6.1",
"aurelia-dependency-injection": "~1.3.2",
"auto-bind": "~1.2.1",
"babel-plugin-transform-object-rest-spread": "~6.26.0",
Expand All @@ -29,7 +30,9 @@
"babel-register": "~6.26.0",
"body-parser": "~1.18.3",
"classnames": "~2.2.6",
"decompress": "~4.2.0",
"express": "~4.16.3",
"he": "~1.1.1",
"htmlescape": "~1.1.1",
"markdown-it": "~8.4.1",
"mime": "~2.3.1",
Expand All @@ -42,9 +45,11 @@
"prop-types": "~15.6.1",
"react": "~16.3.2",
"react-dom": "~16.3.2",
"recursive-readdir": "~2.2.2",
"shortid": "~2.2.8",
"superagent": "~3.8.3",
"thenby": "~1.2.5",
"toposort": "~2.0.2",
"video.js": "~7.0.3",
"videojs-youtube": "~2.5.0"
},
Expand Down
4 changes: 2 additions & 2 deletions s3-seed
Expand Up @@ -17,13 +17,13 @@ const serverBootstrapper = require('./src/bootstrap/server-bootstrapper');

await testHelper.ensurePublicBucketExists(cdn);

const rootDir = path.join(__dirname, './src');
const rootDir = path.join(__dirname, './test/h5p-test-applications');

const files = await util.promisify(glob)(`${rootDir}/**/*`, { nodir: true });
const uploads = files
.map(f => ({
src: f,
dst: `test/${path.relative(rootDir, f)}`
dst: `plugins/h5p-player/content/${path.relative(rootDir, f)}`
}))
.map(f => cdn.uploadObject(f.dst, f.src, {}));

Expand Down
24 changes: 24 additions & 0 deletions src/common/priority-queue.js
@@ -0,0 +1,24 @@
const { priorityQueue } = require('async');

class PriorityQueue {
constructor(maxConcurrency) {
this.tasks = priorityQueue(this._runTask, maxConcurrency);
}

_runTask(task, callback) {
task.func((err, result) => {
if (err) {
task.reject(err);
} else {
task.resolve(result);
}
callback(err);
});
}

push(func, priority = 0) {
return new Promise((resolve, reject) => this.tasks.push({ func, reject, resolve }, priority));
}
}

module.exports = PriorityQueue;
110 changes: 93 additions & 17 deletions src/components/pages/docs.jsx
@@ -1,30 +1,106 @@
const React = require('react');
const autoBind = require('auto-bind');
const PropTypes = require('prop-types');
const { Button, Input, Modal } = require('antd');
const uniqueId = require('../../utils/unique-id');
const PageHeader = require('./../page-header.jsx');
const { inject } = require('../container-context.jsx');
const DocumentApiClient = require('../../services/document-api-client');

function Docs({ initialState }) {
return (
<React.Fragment>
<PageHeader />
<div className="PageContent">
<h1>Docs</h1>
<ul>
{initialState.map(doc => (
<li key={doc._id}>
<a href={`/docs/${doc._id}`}>{doc.title}</a>
</li>
))}
</ul>
</div>
</React.Fragment>
);
class Docs extends React.Component {
constructor(props) {
super(props);
autoBind.react(this);
this.state = {
newDocKey: null,
visible: false,
loading: false
};
}

createNewDocument(key, title) {
return {
doc: {
key: key,
title: title || 'Unbenannt'
},
sections: [],
user: {
name: 'Mr. Browser'
}
};
}

handleNewDocumentClick() {
this.setState({
newDocKey: uniqueId.create(),
visible: true
});
}

handleNewDocKeyChange(event) {
this.setState({ newDocKey: event.target.value });
}

async handleOk() {
const { newDocKey } = this.state;
const { documentApiClient } = this.props;

this.setState({ loading: true });

await documentApiClient.saveDocument(this.createNewDocument(newDocKey));

this.setState({
newDocKey: null,
visible: false,
loading: false
});
}

handleCancel() {
this.setState({ visible: false });
}

render() {
const { initialState } = this.props;
const { newDocKey, visible, loading } = this.state;
return (
<React.Fragment>
<PageHeader />
<div className="PageContent">
<h1>Docs</h1>
<ul>
{initialState.map(doc => (
<li key={doc._id}>
<a href={`/docs/${doc._id}`}>{doc.title}</a>
</li>
))}
</ul>
<Button type="primary" shape="circle" icon="plus" size="large" onClick={this.handleNewDocumentClick} />
<Modal
title="Neues Dokument"
visible={visible}
onOk={this.handleOk}
onCancel={this.handleCancel}
>
<p>ID</p>
<p><Input value={newDocKey} onChange={this.handleNewDocKeyChange} /></p>
{loading && <p>Wird erstellt ...</p>}
</Modal>
</div>
</React.Fragment>
);
}
}

Docs.propTypes = {
documentApiClient: PropTypes.instanceOf(DocumentApiClient).isRequired,
initialState: PropTypes.arrayOf(PropTypes.shape({
_id: PropTypes.string.isRequired,
title: PropTypes.string.isRequired
})).isRequired
};

module.exports = Docs;
module.exports = inject({
documentApiClient: DocumentApiClient
}, Docs);
17 changes: 17 additions & 0 deletions src/elmu-server.js
Expand Up @@ -14,6 +14,7 @@ const ReactDOMServer = require('react-dom/server');
const Docs = require('./components/pages/docs.jsx');
const Edit = require('./components/pages/edit.jsx');
const Index = require('./components/pages/index.jsx');
const H5pPlayerApi = require('./plugins/h5p-player/api');
const serverSettings = require('./bootstrap/server-settings');
const DocumentService = require('./services/document-service');

Expand Down Expand Up @@ -52,6 +53,10 @@ class ElmuServer {
.map(dir => path.join(__dirname, dir))
.forEach(dir => this.app.use(express.static(dir)));

// Register H5P plugin static dirs
this.app.use('/plugins/h5p-player/static', express.static(path.join(__dirname, './plugins/h5p-player/static')));
this.app.use('/plugins/h5p-player/applications', express.static(path.join(__dirname, '../test/h5p-test-applications')));

this.app.get('/', (req, res) => {
return this._sendPage(res, 'index', Index, {});
});
Expand Down Expand Up @@ -92,6 +97,18 @@ class ElmuServer {

return res.send({});
});

// Register H5P plugin upload route
this.app.post('/plugins/h5p-player/upload', multipartParser.single('file'), async (req, res) => {
const api = container.get(H5pPlayerApi);
await api.handlePostUpload(req, res);
});

// Register H5P play route
this.app.get('/plugins/h5p-player/play/:contentId', async (req, res) => {
const api = container.get(H5pPlayerApi);
await api.handleGetPlay(req, res);
});
}

_sendPage(res, bundleName, PageComponent, initialState) {
Expand Down
3 changes: 2 additions & 1 deletion src/plugins/editor-factory.js
@@ -1,11 +1,12 @@
const { Container } = require('../common/di');
const AudioPlugin = require('./audio/editor');
const MarkdownPlugin = require('./markdown/editor');
const H5pPlayerPlugin = require('./h5p-player/editor');
const PluginFactoryBase = require('./plugin-factory-base');
const QuickTesterPlugin = require('./quick-tester/editor');
const YoutubeVideoPlugin = require('./youtube-video/editor');

const editors = [MarkdownPlugin, QuickTesterPlugin, YoutubeVideoPlugin, AudioPlugin];
const editors = [MarkdownPlugin, QuickTesterPlugin, YoutubeVideoPlugin, AudioPlugin, H5pPlayerPlugin];

class EditorFactory extends PluginFactoryBase {
static get inject() { return [Container]; }
Expand Down
63 changes: 63 additions & 0 deletions src/plugins/h5p-player/api.js
@@ -0,0 +1,63 @@
const he = require('he');
const htmlescape = require('htmlescape');
const h5pHelper = require('./h5p-helper');
const Cdn = require('../../repositories/cdn');

const renderPlayTemplate = (contentId, integration) => `
<!DOCTYPE html>
<html>
<head>
<title>H5P Player</title>
<script src="/plugins/h5p-player/static/js/jquery.js"></script>
<script src="/plugins/h5p-player/static/js/h5p.js"></script>
<script src="/plugins/h5p-player/static/js/h5p-event-dispatcher.js"></script>
<script src="/plugins/h5p-player/static/js/h5p-x-api-event.js"></script>
<script src="/plugins/h5p-player/static/js/h5p-x-api.js"></script>
<script src="/plugins/h5p-player/static/js/h5p-content-type.js"></script>
<script src="/plugins/h5p-player/static/js/h5p-confirmation-dialog.js"></script>
<script src="/plugins/h5p-player/static/js/h5p-action-bar.js"></script>
<link rel="stylesheet" href="/plugins/h5p-player/static/styles/h5p.css">
<link rel="stylesheet" href="/plugins/h5p-player/static/styles/h5p-confirmation-dialog.css">
<link rel="stylesheet" href="/plugins/h5p-player/static/styles/h5p-core-button.css">
<style>
body { margin: 0; }
</style>
</head>
<body>
<div class="h5p-iframe-wrapper">
<iframe id="h5p-iframe-${he.encode(contentId)}" class="h5p-iframe" data-content-id="${he.encode(contentId)}" style="height:1px" src="about:blank" frameborder="0" scrolling="no"></iframe>
</div>
<script>
window.H5PIntegration = ${htmlescape(integration)};
</script>
</body>
</html>
`;

class H5pPlayer {
static get inject() { return [Cdn]; }

static get typeName() { return 'h5p-player'; }

constructor(cdn) {
this.cdn = cdn;
}

async handlePostUpload(req, res) {
if (!req.file) {
return res.sendStatus(400);
}

const result = await h5pHelper.install(req.file.path, this.cdn);
return res.send(result);
}

async handleGetPlay(req, res) {
const { contentId } = req.params;
const integration = await h5pHelper.createIntegration(contentId);
const html = renderPlayTemplate(contentId, integration);
return res.type('html').send(html);
}
}

module.exports = H5pPlayer;
48 changes: 48 additions & 0 deletions src/plugins/h5p-player/display/h5p-player-display.jsx
@@ -0,0 +1,48 @@
const React = require('react');
const autoBind = require('auto-bind');
const PropTypes = require('prop-types');

class H5pPlayerDisplay extends React.Component {
constructor(props) {
super(props);
autoBind.react(this);
this.contentFrame = React.createRef();
}

componentDidMount() {
const { preferredLanguages, section } = this.props;
const data = section.content[preferredLanguages[0]];
const playUrl = `/plugins/h5p-player/play/${data.contentId}`;

fetch(playUrl).then(x => x.text()).then(html => {
const iframe = this.contentFrame.current;
iframe.onload = () => {
iframe.style.height = `${iframe.contentWindow.document.body.scrollHeight}px`;
};
iframe.contentWindow.document.open();
iframe.contentWindow.document.write(html);
iframe.contentWindow.document.close();
});
}

shouldComponentUpdate() {
return false;
}

render() {
return (
<div className="H5pPlayer">
<iframe className="H5pPlayer-contentFrame" frameBorder="0" scrolling="no" ref={this.contentFrame} />
</div>
);
}
}

H5pPlayerDisplay.propTypes = {
preferredLanguages: PropTypes.arrayOf(PropTypes.string).isRequired,
section: PropTypes.shape({
content: PropTypes.object
}).isRequired
};

module.exports = H5pPlayerDisplay;

0 comments on commit add8c1c

Please sign in to comment.