openvibers/openvibe.tips Commits
Use shared upload kit for paid sounds (#263)
Changes
nnew file mode 100644@@@ -0,0 +1,85 @@++/* Shared picker for the dashboard's existing multipart paid-sound form. */++(function () {++ 'use strict';++ var form = document.querySelector('form.sound-upload');++ var input = form && form.querySelector('[data-sound-file]');++ var root = form && form.querySelector('[data-sound-picker]');++ var error = form && form.querySelector('[data-sound-error]');++ if (!form || !input || !root || !error || !window.OVUpload || !window.DataTransfer) return;++++ var file = null;++ var row = null;++ var meter = null;++ var sending = false;++ function fail(message) { error.textContent = message; error.hidden = false; }++ function clear() {++ file = null; row = null; input.value = '';++ if (meter) meter.destroy();++ meter = null;++ }++ var pick = OVUpload.picker(root, {++ label: 'Choose an MP3, OGG or WAV sound',++ accept: input.accept, multiple: false, onRemove: clear,++ onFile: function (next, nextRow) {++ if (row) { row.dispatchEvent(new Event('remove')); row.remove(); }++ error.hidden = true;++ if (!['audio/mpeg', 'audio/ogg', 'audio/wav', 'audio/x-wav'].includes(next.type) &&++ !/\.(mp3|ogg|wav)$/i.test(next.name)) {++ nextRow.dispatchEvent(new Event('remove')); nextRow.remove();++ return fail('Choose an MP3, OGG or WAV sound.');++ }++ if (next.size > Number(root.dataset.maxBytes)) {++ nextRow.dispatchEvent(new Event('remove')); nextRow.remove();++ return fail('A sound can be at most 512 KB.');++ }++ var transfer = new DataTransfer();++ transfer.items.add(next);++ input.files = transfer.files;++ file = next; row = nextRow;++ var meterRoot = document.createElement('div');++ row.append(meterRoot);++ meter = OVUpload.progress(meterRoot, { compact: true, onRetry: send });++ meter.update({ state: 'waiting', total: file.size });++ }++ });++ input.required = false;++ input.hidden = true;++++ function send() {++ if (sending || !file) return;++ sending = true;++ error.hidden = true;++ var button = form.querySelector('[type="submit"]');++ var remove = row.querySelector('.ov-upload__remove');++ button.disabled = true; remove.disabled = true;++ var size = file.size;++ var xhr = new XMLHttpRequest();++ xhr.open('POST', form.action);++ xhr.upload.onprogress = function (event) {++ if (event.lengthComputable) meter.update({ state: 'sending', done: Math.min(event.loaded, size), total: event.total });++ };++ xhr.upload.onload = function () { meter.update({ state: 'processing', done: size, total: size }); };++ xhr.onload = function () {++ if (xhr.status >= 200 && xhr.status < 400 && xhr.responseURL && xhr.responseURL !== form.action) {++ meter.update({ state: 'done', done: size, total: size });++ window.location.assign(xhr.responseURL);++ return;++ }++ sending = false; button.disabled = false; remove.disabled = false;++ var page = new DOMParser().parseFromString(xhr.responseText || '', 'text/html');++ var reason = page.querySelector('.notice.error[role="alert"]');++ meter.update({ state: 'failed', message: reason ? reason.textContent : 'The sound could not be added. Try again.' });++ };++ xhr.onerror = function () {++ sending = false; button.disabled = false; remove.disabled = false;++ meter.update({ state: 'failed', message: 'The connection stopped. Try again.' });++ };++ meter.update({ state: 'sending', done: 0, total: size });++ xhr.send(new FormData(form));++ }++ form.addEventListener('submit', function (event) {++ event.preventDefault();++ if (!file) { fail('Choose a sound first.'); pick.input.focus(); return; }++ send();++ });++})();@@@ -91,6 +91,9 @@ function createLayout({ config, release }) {`<link rel="stylesheet" href="${ovServe.url('kit.css')}">`,// openvibe-shared stylesheets a page asks for by name (the home's showcase.css)....(o.styles || []).map((name) => `<link rel="stylesheet" href="${ovServe.url(name)}">`),++ o.uploadKit ? `<link rel="stylesheet" href="${ovServe.url('ov-upload.css')}">` : '',++ o.uploadKit ? `<script src="${ovServe.url('ov-upload.js')}" defer></script>` : '',++ o.uploadKit ? `<script src="${asset('js/sound-upload.js')}" defer></script>` : '',`<meta name="ov-boost" content="tips@${esc(RELEASE)}">`,`<script src="${ovServe.url('boost.js')}" data-main="#main" defer></script>`,`<script src="${asset('js/tips.js')}" defer></script>`,@@@ -401,7 +401,8 @@ function soundsCard({ profile: p, sounds, csrf, idem }) {<form method="post" action="/dashboard/sounds/${esc(x.id)}/delete">${hidden({ csrf, idem: `${idem}-sd-${x.id}` })}<button class="btn btn-small" type="submit">Delete</button></form></li>`).join('')}</ul>` : '<p class="muted">No sounds yet.</p>'}${full ? '<p class="muted">You have 12 sounds: delete one to add another.</p>' : `<form method="post" action="/dashboard/sounds" enctype="multipart/form-data" class="sound-upload">${hidden({ csrf, idem: `${idem}-su` })}<label>Name <input type="text" name="name" maxlength="40" placeholder="Airhorn"></label>-- <label>File <input type="file" name="file" accept="audio/mpeg,audio/ogg,audio/wav,.mp3,.ogg,.wav" required></label>++ <label>File <input type="file" name="file" accept="audio/mpeg,audio/ogg,audio/wav,.mp3,.ogg,.wav" required data-sound-file></label>++ <div data-sound-picker data-max-bytes="524288"></div><p role="alert" data-sound-error hidden></p><button class="btn" type="submit">Add sound</button></form>`}</section>`;@@@ -256,7 +256,7 @@ function createWebRoutes({ domain, config, layout, userAuth, liveNow = { list: adomain.vip.tiers(s),]);html(res, pageFor(req, {-- title: 'Dashboard', active: 'dashboard', robots: 'noindex,nofollow', canonicalPath: '/dashboard',++ title: 'Dashboard', active: 'dashboard', robots: 'noindex,nofollow', canonicalPath: '/dashboard', uploadKit: true,body: pages.dashboard({profile: p, goals: goalRows, tokens: tokens.map(overlays.presentToken),configs: configs.map(overlays.presentConfig), totals, recent: recentRows,@@@ -51,6 +51,10 @@ const WAV = Buffer.concat([Buffer.from('RIFF'), Buffer.from([0x24, 0, 0, 0]), Buconst page = await get('/dashboard', alex);assert.match(page.text, /id="sounds"/);assert.match(page.text, /enctype="multipart\/form-data"/);++ assert.match(page.text, /ov-upload\.css/);++ assert.match(page.text, /ov-upload\.js/);++ assert.match(page.text, /data-sound-picker data-max-bytes="524288"/);++ assert.match(page.text, /data-sound-error hidden/);const ok = await upload(alex, { name: 'Airhorn', file: MP3 });assert.strictEqual(ok.status, 303, ok.text.slice(0, 300));assert.match(ok.location, /done=Sound%20added/);
new file mode 100644
new file mode 100644
@@ -0,0 +1,85 @@
@@ -0,0 +1,85 @@
+/* Shared picker for the dashboard's existing multipart paid-sound form. */
+(function () {
+ 'use strict';
+ var form = document.querySelector('form.sound-upload');
+ var input = form && form.querySelector('[data-sound-file]');
+ var root = form && form.querySelector('[data-sound-picker]');
+ var error = form && form.querySelector('[data-sound-error]');
+ if (!form || !input || !root || !error || !window.OVUpload || !window.DataTransfer) return;
+
+ var file = null;
+ var row = null;
+ var meter = null;
+ var sending = false;
+ function fail(message) { error.textContent = message; error.hidden = false; }
+ function clear() {
+ file = null; row = null; input.value = '';
+ if (meter) meter.destroy();
+ meter = null;
+ }
+ var pick = OVUpload.picker(root, {
+ label: 'Choose an MP3, OGG or WAV sound',
+ accept: input.accept, multiple: false, onRemove: clear,
+ onFile: function (next, nextRow) {
+ if (row) { row.dispatchEvent(new Event('remove')); row.remove(); }
+ error.hidden = true;
+ if (!['audio/mpeg', 'audio/ogg', 'audio/wav', 'audio/x-wav'].includes(next.type) &&
+ !/\.(mp3|ogg|wav)$/i.test(next.name)) {
+ nextRow.dispatchEvent(new Event('remove')); nextRow.remove();
+ return fail('Choose an MP3, OGG or WAV sound.');
+ }
+ if (next.size > Number(root.dataset.maxBytes)) {
+ nextRow.dispatchEvent(new Event('remove')); nextRow.remove();
+ return fail('A sound can be at most 512 KB.');
+ }
+ var transfer = new DataTransfer();
+ transfer.items.add(next);
+ input.files = transfer.files;
+ file = next; row = nextRow;
+ var meterRoot = document.createElement('div');
+ row.append(meterRoot);
+ meter = OVUpload.progress(meterRoot, { compact: true, onRetry: send });
+ meter.update({ state: 'waiting', total: file.size });
+ }
+ });
+ input.required = false;
+ input.hidden = true;
+
+ function send() {
+ if (sending || !file) return;
+ sending = true;
+ error.hidden = true;
+ var button = form.querySelector('[type="submit"]');
+ var remove = row.querySelector('.ov-upload__remove');
+ button.disabled = true; remove.disabled = true;
+ var size = file.size;
+ var xhr = new XMLHttpRequest();
+ xhr.open('POST', form.action);
+ xhr.upload.onprogress = function (event) {
+ if (event.lengthComputable) meter.update({ state: 'sending', done: Math.min(event.loaded, size), total: event.total });
+ };
+ xhr.upload.onload = function () { meter.update({ state: 'processing', done: size, total: size }); };
+ xhr.onload = function () {
+ if (xhr.status >= 200 && xhr.status < 400 && xhr.responseURL && xhr.responseURL !== form.action) {
+ meter.update({ state: 'done', done: size, total: size });
+ window.location.assign(xhr.responseURL);
+ return;
+ }
+ sending = false; button.disabled = false; remove.disabled = false;
+ var page = new DOMParser().parseFromString(xhr.responseText || '', 'text/html');
+ var reason = page.querySelector('.notice.error[role="alert"]');
+ meter.update({ state: 'failed', message: reason ? reason.textContent : 'The sound could not be added. Try again.' });
+ };
+ xhr.onerror = function () {
+ sending = false; button.disabled = false; remove.disabled = false;
+ meter.update({ state: 'failed', message: 'The connection stopped. Try again.' });
+ };
+ meter.update({ state: 'sending', done: 0, total: size });
+ xhr.send(new FormData(form));
+ }
+ form.addEventListener('submit', function (event) {
+ event.preventDefault();
+ if (!file) { fail('Choose a sound first.'); pick.input.focus(); return; }
+ send();
+ });
+})();
@@ -91,6 +91,9 @@ function createLayout({ config, release }) {
@@ -91,6 +91,9 @@ function createLayout({ config, release }) {
`<link rel="stylesheet" href="${ovServe.url('kit.css')}">`,
`<link rel="stylesheet" href="${ovServe.url('kit.css')}">`,
// openvibe-shared stylesheets a page asks for by name (the home's showcase.css).
// openvibe-shared stylesheets a page asks for by name (the home's showcase.css).
...(o.styles || []).map((name) => `<link rel="stylesheet" href="${ovServe.url(name)}">`),
...(o.styles || []).map((name) => `<link rel="stylesheet" href="${ovServe.url(name)}">`),
+ o.uploadKit ? `<link rel="stylesheet" href="${ovServe.url('ov-upload.css')}">` : '',
+ o.uploadKit ? `<script src="${ovServe.url('ov-upload.js')}" defer></script>` : '',
+ o.uploadKit ? `<script src="${asset('js/sound-upload.js')}" defer></script>` : '',
`<meta name="ov-boost" content="tips@${esc(RELEASE)}">`,
`<meta name="ov-boost" content="tips@${esc(RELEASE)}">`,
`<script src="${ovServe.url('boost.js')}" data-main="#main" defer></script>`,
`<script src="${ovServe.url('boost.js')}" data-main="#main" defer></script>`,
`<script src="${asset('js/tips.js')}" defer></script>`,
`<script src="${asset('js/tips.js')}" defer></script>`,
@@ -401,7 +401,8 @@ function soundsCard({ profile: p, sounds, csrf, idem }) {
@@ -401,7 +401,8 @@ function soundsCard({ profile: p, sounds, csrf, idem }) {
<form method="post" action="/dashboard/sounds/${esc(x.id)}/delete">${hidden({ csrf, idem: `${idem}-sd-${x.id}` })}<button class="btn btn-small" type="submit">Delete</button></form></li>`).join('')}</ul>` : '<p class="muted">No sounds yet.</p>'}
<form method="post" action="/dashboard/sounds/${esc(x.id)}/delete">${hidden({ csrf, idem: `${idem}-sd-${x.id}` })}<button class="btn btn-small" type="submit">Delete</button></form></li>`).join('')}</ul>` : '<p class="muted">No sounds yet.</p>'}
${full ? '<p class="muted">You have 12 sounds: delete one to add another.</p>' : `<form method="post" action="/dashboard/sounds" enctype="multipart/form-data" class="sound-upload">${hidden({ csrf, idem: `${idem}-su` })}
${full ? '<p class="muted">You have 12 sounds: delete one to add another.</p>' : `<form method="post" action="/dashboard/sounds" enctype="multipart/form-data" class="sound-upload">${hidden({ csrf, idem: `${idem}-su` })}
<label>Name <input type="text" name="name" maxlength="40" placeholder="Airhorn"></label>
<label>Name <input type="text" name="name" maxlength="40" placeholder="Airhorn"></label>
- <label>File <input type="file" name="file" accept="audio/mpeg,audio/ogg,audio/wav,.mp3,.ogg,.wav" required></label>
+ <label>File <input type="file" name="file" accept="audio/mpeg,audio/ogg,audio/wav,.mp3,.ogg,.wav" required data-sound-file></label>
+ <div data-sound-picker data-max-bytes="524288"></div><p role="alert" data-sound-error hidden></p>
<button class="btn" type="submit">Add sound</button>
<button class="btn" type="submit">Add sound</button>
</form>`}
</form>`}
</section>`;
</section>`;
@@ -256,7 +256,7 @@ function createWebRoutes({ domain, config, layout, userAuth, liveNow = { list: a
@@ -256,7 +256,7 @@ function createWebRoutes({ domain, config, layout, userAuth, liveNow = { list: a
domain.vip.tiers(s),
domain.vip.tiers(s),
]);
]);
html(res, pageFor(req, {
html(res, pageFor(req, {
- title: 'Dashboard', active: 'dashboard', robots: 'noindex,nofollow', canonicalPath: '/dashboard',
+ title: 'Dashboard', active: 'dashboard', robots: 'noindex,nofollow', canonicalPath: '/dashboard', uploadKit: true,
body: pages.dashboard({
body: pages.dashboard({
profile: p, goals: goalRows, tokens: tokens.map(overlays.presentToken),
profile: p, goals: goalRows, tokens: tokens.map(overlays.presentToken),
configs: configs.map(overlays.presentConfig), totals, recent: recentRows,
configs: configs.map(overlays.presentConfig), totals, recent: recentRows,
@@ -51,6 +51,10 @@ const WAV = Buffer.concat([Buffer.from('RIFF'), Buffer.from([0x24, 0, 0, 0]), Bu
@@ -51,6 +51,10 @@ const WAV = Buffer.concat([Buffer.from('RIFF'), Buffer.from([0x24, 0, 0, 0]), Bu
const page = await get('/dashboard', alex);
const page = await get('/dashboard', alex);
assert.match(page.text, /id="sounds"/);
assert.match(page.text, /id="sounds"/);
assert.match(page.text, /enctype="multipart\/form-data"/);
assert.match(page.text, /enctype="multipart\/form-data"/);
+ assert.match(page.text, /ov-upload\.css/);
+ assert.match(page.text, /ov-upload\.js/);
+ assert.match(page.text, /data-sound-picker data-max-bytes="524288"/);
+ assert.match(page.text, /data-sound-error hidden/);
const ok = await upload(alex, { name: 'Airhorn', file: MP3 });
const ok = await upload(alex, { name: 'Airhorn', file: MP3 });
assert.strictEqual(ok.status, 303, ok.text.slice(0, 300));
assert.strictEqual(ok.status, 303, ok.text.slice(0, 300));
assert.match(ok.location, /done=Sound%20added/);
assert.match(ok.location, /done=Sound%20added/);