From 89ef401c7afa2e29141bcb0db8b45f347c3281c8 Mon Sep 17 00:00:00 2001 From: Paul Kaplan Date: Tue, 19 Jun 2018 08:51:34 -0400 Subject: [PATCH 1/8] Costume and sound serializers for backpack --- src/lib/backpack/costume-payload.js | 33 +++++++++++++++++++++++++++++ src/lib/backpack/jpeg-thumbnail.js | 20 +++++++++++++++++ src/lib/backpack/sound-payload.js | 31 +++++++++++++++++++++++++++ src/lib/backpack/sound-thumbnail.js | 3 +++ 4 files changed, 87 insertions(+) create mode 100644 src/lib/backpack/costume-payload.js create mode 100644 src/lib/backpack/jpeg-thumbnail.js create mode 100644 src/lib/backpack/sound-payload.js create mode 100644 src/lib/backpack/sound-thumbnail.js diff --git a/src/lib/backpack/costume-payload.js b/src/lib/backpack/costume-payload.js new file mode 100644 index 000000000..a376c1111 --- /dev/null +++ b/src/lib/backpack/costume-payload.js @@ -0,0 +1,33 @@ +import jpegThumbnail from './jpeg-thumbnail'; +import storage from '../storage'; + +const costumePayload = costume => { + // TODO is it ok to base64 encode SVGs? What about unicode text inside them? + const assetDataUrl = storage.get(costume.assetId).encodeDataURI(); + const assetDataFormat = costume.dataFormat; + const payload = { + type: 'costume', + name: costume.name, + // Params to be filled in below + mime: '', + body: '', + thumbnail: '' + }; + + switch (assetDataFormat) { + case 'svg': + payload.mime = 'image/svg+xml'; + payload.body = assetDataUrl.replace('data:image/svg+xml;base64,', ''); + break; + default: + alert(`Cannot serialize for format: ${assetDataFormat}`); // eslint-disable-line + } + + // TODO we may be able to optimize by not actually generating thumbnails... + return jpegThumbnail(assetDataUrl).then(thumbnail => { + payload.thumbnail = thumbnail.replace('data:image/jpeg;base64,', ''); + return payload; + }); +}; + +export default costumePayload; diff --git a/src/lib/backpack/jpeg-thumbnail.js b/src/lib/backpack/jpeg-thumbnail.js new file mode 100644 index 000000000..623daf3be --- /dev/null +++ b/src/lib/backpack/jpeg-thumbnail.js @@ -0,0 +1,20 @@ +const jpegThumbnail = dataUrl => new Promise(resolve => { + const image = new Image(); + image.onload = () => { + const canvas = document.createElement('canvas'); + const ctx = canvas.getContext('2d'); + // TODO we may want to draw to a smaller, fixed size to optimize file size + canvas.width = image.width; + canvas.height = image.height; + + ctx.fillStyle = 'white'; // Create white background, since jpeg doesn't have transparency + ctx.fillRect(0, 0, canvas.width, canvas.height); + + ctx.drawImage(image, 0, 0); + // TODO we can play with the `quality` option here to optimize file size + resolve(canvas.toDataURL('image/jpeg', 0.92 /* quality */)); // Default quality is 0.92 + }; + image.src = dataUrl; +}); + +export default jpegThumbnail; diff --git a/src/lib/backpack/sound-payload.js b/src/lib/backpack/sound-payload.js new file mode 100644 index 000000000..e5e1c992a --- /dev/null +++ b/src/lib/backpack/sound-payload.js @@ -0,0 +1,31 @@ +import soundThumbnail from './sound-thumbnail'; +import storage from '../storage'; + +const soundPayload = sound => { + const assetDataUrl = storage.get(sound.assetId).encodeDataURI(); + const assetDataFormat = sound.dataFormat; + const payload = { + type: 'sound', + name: sound.name, + // Params to be filled in below + mime: '', + body: '', + thumbnail: '' + }; + + switch (assetDataFormat) { + case 'wav': + payload.mime = 'audio/x-wav'; + payload.body = assetDataUrl.replace('data:audio/x-wav;base64,', ''); + break; + default: + alert(`Cannot serialize for format: ${assetDataFormat}`); // eslint-disable-line + } + + payload.thumbnail = soundThumbnail; + + // Return a promise to make it consistent with other payload constructors like costume-payload + return new Promise(resolve => resolve(payload)); +}; + +export default soundPayload; diff --git a/src/lib/backpack/sound-thumbnail.js b/src/lib/backpack/sound-thumbnail.js new file mode 100644 index 000000000..f965cc8e1 --- /dev/null +++ b/src/lib/backpack/sound-thumbnail.js @@ -0,0 +1,3 @@ +// image/jpeg base64 encoded sound thumbnail image +// eslint-disable-next-line max-len +export default '/9j/4AAQSkZJRgABAgAAZABkAAD/7AARRHVja3kAAQAEAAAAPAAA/+4AJkFkb2JlAGTAAAAAAQMAFQQDBgoNAAACZgAAAwoAAAPwAAAE/P/bAIQABgQEBAUEBgUFBgkGBQYJCwgGBggLDAoKCwoKDBAMDAwMDAwQDA4PEA8ODBMTFBQTExwbGxscHx8fHx8fHx8fHwEHBwcNDA0YEBAYGhURFRofHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8f/8IAEQgAQABAAwERAAIRAQMRAf/EALAAAQADAQEBAAAAAAAAAAAAAAAEBQcGAgMBAQEBAQAAAAAAAAAAAAAAAAABAgMQAAEEAgICAwAAAAAAAAAAAAMBAgQFIBEQQDASYCQGEQACAQICBggHAAAAAAAAAAABAgMAESFBEDFRcRIEIDBhoSIyQhNAgZGxwWJTEgEAAAAAAAAAAAAAAAAAAABgEwEAAQIGAgIDAQAAAAAAAAABEQAhECAxQVFhQIEwcZGhsfD/2gAMAwEAAhEDEQAAAdUAAAAAIVk2UACFZz2pwvSa1x19ordLrNGVdsVNDvedjlXppXLQy7ril0He87GKzTSuWgKazOOudU5a9RWaXuaAAAAAAAB//9oACAEBAAEFAvGWbFE/E82KBZt8JGuc5zoa/SOdogV1myZzaKq2HE8b30lAX3ZW7BbcW4iMncTyuZSfnxeqV2z2/M2rjyWHjGAWMPUM8dpY9bWJD6X/2gAIAQIAAQUC6Ws0wXBMFx3hvpf/2gAIAQMAAQUC+Af/2gAIAQICBj8CAf/aAAgBAwIGPwIB/9oACAEBAQY/Aur9uSUK5y6VpZApOVFeV8Tn15D60WY3J1moCf5r9qabWFF8KYcPA6+m98O7TNfbphKnBFjLDaLVNyz4pa4G/A0I/wBmjOmQsMHN1OmEAedUUn5XqbmWwS3CD3mhJtZpD0DgElycfmvakXxZdu6oo5BqRQyndTw+UMLYZUzFuN2wv2fBf//aAAgBAQMBPyH4w+6y888e8wK/Yd2OYJamasJkfQXL6ikQPlLqtEOQX1+tJ6/x3VtNruFuMYphTp9AYwnOihP0s1zyk4sfdqu5Y7GJP6GLvTiVE2cZP3XRH9ointmkOrvq1QQW6QXf65Is2rxfoalPUHFc4PKtckUTcIiUEb+UburVx0sIgbb6+F//2gAIAQIDAT8h+OM0UYHBIx0YmuF0zDXC6ZCk04MvC//aAAgBAwMBPyHwpzuQyOQyxkjwv//aAAwDAQACEQMRAAAQkkkkkhkklIrEhkPEnkPEk6Pkkkkkkkkk/9oACAEBAwE/EPjj8Ql0GhYJP6UIgjI3EylYiQdlZgdxTFjQf2wHCn2aU4Yj6glVdVokAKqA31aJdngG0sDpd3oXX8WyOINm3HOLiiIlvAAejFrIbqFisawfSkBz6gD8RlZ90xKenkOY72Ng0hMc/A2cVHA50nk/5TSE7IQn4yF33UxR3IRz6DFBESRsjSwYJBTwiTd34oYrYAQuoCx6/tIGo6gKAkslIWGBBsIIQO1OlGBFumAquhfo8L//2gAIAQIDAT8Q+MTmE6Vy4amgljJtDF30N6vxK2JvpbVbkIUBJpXoYZ8Of//aAAgBAwMBPxD45zLTgNKWhnHVi6YDXHVi6YDXIypKKSiHhf/Z'; From c9ad894003fedf1a4944e01e3ace2ef6c214864a Mon Sep 17 00:00:00 2001 From: Paul Kaplan Date: Tue, 19 Jun 2018 10:51:32 -0400 Subject: [PATCH 2/8] Create API method for saving a backpack object --- src/lib/backpack-api.js | 30 +++++++++++++++++++++++++++++- 1 file changed, 29 insertions(+), 1 deletion(-) diff --git a/src/lib/backpack-api.js b/src/lib/backpack-api.js index 0ec46260a..3b1a55e15 100644 --- a/src/lib/backpack-api.js +++ b/src/lib/backpack-api.js @@ -1,4 +1,6 @@ import xhr from 'xhr'; +import costumePayload from './backpack/costume-payload'; +import soundPayload from './backpack/sound-payload'; const getBackpackContents = ({ host, @@ -24,6 +26,32 @@ const getBackpackContents = ({ }); }); +const saveBackpackObject = ({ + host, + username, + token, + type, // Type of object being saved to the backpack + mime, // Mime-type of the object being saved + name, // User-facing name of the object being saved + body, // Base64-encoded body of the object being saved + thumbnail // Base64-encoded JPEG thumbnail of the object being saved +}) => new Promise((resolve, reject) => { + xhr({ + method: 'POST', + uri: `${host}${username}`, + headers: {'x-token': token}, + json: {type, mime, name, body, thumbnail} + }, (error, response) => { + if (error || response.statusCode !== 200) { + return reject(); + } + return resolve(response.body); + }); +}); + export { - getBackpackContents + getBackpackContents, + saveBackpackObject, + costumePayload, + soundPayload }; From 292655d9e8ac999360e38cb9b139c9a486c60975 Mon Sep 17 00:00:00 2001 From: Paul Kaplan Date: Tue, 19 Jun 2018 10:51:59 -0400 Subject: [PATCH 3/8] Add drop handler to the backpack for consuming sounds and costumes --- src/components/asset-panel/selector.jsx | 2 + src/components/backpack/backpack.css | 12 ++++ src/components/backpack/backpack.jsx | 13 ++++- src/containers/backpack.jsx | 73 ++++++++++++++++++++++++- src/containers/costume-tab.jsx | 3 +- src/containers/sound-tab.jsx | 3 +- 6 files changed, 99 insertions(+), 7 deletions(-) diff --git a/src/components/asset-panel/selector.jsx b/src/components/asset-panel/selector.jsx index 88d609723..5c48ae253 100644 --- a/src/components/asset-panel/selector.jsx +++ b/src/components/asset-panel/selector.jsx @@ -15,6 +15,7 @@ const Selector = props => { buttons, containerRef, dragType, + dragPayload, items, selectedItemIndex, draggingIndex, @@ -67,6 +68,7 @@ const Selector = props => { })} costumeURL={item.url} details={item.details} + dragPayload={item.dragPayload} dragType={dragType} id={index} index={index} diff --git a/src/components/backpack/backpack.css b/src/components/backpack/backpack.css index 229344a2d..9143844d0 100644 --- a/src/components/backpack/backpack.css +++ b/src/components/backpack/backpack.css @@ -42,6 +42,18 @@ overflow-x: auto; } +.drag-over:after { + content: ''; + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + opacity: 0.75; + background-color: #8cbcff; + transition: all 0.25s ease; +} + .status-message { width: 100%; text-align: center; diff --git a/src/components/backpack/backpack.jsx b/src/components/backpack/backpack.jsx index 563ec84a3..0d64307b7 100644 --- a/src/components/backpack/backpack.jsx +++ b/src/components/backpack/backpack.jsx @@ -1,5 +1,6 @@ import React from 'react'; import PropTypes from 'prop-types'; +import classNames from 'classnames'; import {FormattedMessage} from 'react-intl'; import DragConstants from '../../lib/drag-constants'; import {ComingSoonTooltip} from '../coming-soon/coming-soon.jsx'; @@ -16,7 +17,7 @@ const dragTypeMap = { sprite: DragConstants.BACKPACK_SPRITE }; -const Backpack = ({contents, error, expanded, loading, onToggle}) => ( +const Backpack = ({contents, dragOver, dropAreaRef, error, expanded, loading, onToggle}) => (
(
) : ( contents.length > 0 ? ( -
+
{contents.map(item => ( left && x < right && y > top && y < bottom) { + this.setState({dragOver: true}); + } else { + this.setState({dragOver: false}); + } + } + } handleToggle () { const newState = !this.state.expanded; this.setState({expanded: newState, offset: 0}); @@ -39,6 +63,27 @@ class Backpack extends React.Component { this.refreshContents(); } } + handleDrop (dragInfo) { + let payloader = null; + switch (dragInfo.dragType) { + case DragConstants.COSTUME: + payloader = costumePayload; + break; + case DragConstants.SOUND: + payloader = soundPayload; + break; + } + if (!payloader) return; + + payloader(dragInfo.payload) + .then(payload => saveBackpackObject({ + host: this.props.host, + token: this.props.token, + username: this.props.username, + ...payload + })) + .then(this.refreshContents); + } refreshContents () { if (this.props.token && this.props.username) { this.setState({loading: true, error: false}); @@ -57,10 +102,15 @@ class Backpack extends React.Component { }); } } + setRef (ref) { + this.ref = ref; + } render () { return ( { +const getTokenAndUsername = state => { // Look for the session state provided by scratch-www if (state.session && state.session.session) { return { @@ -94,4 +153,12 @@ const mapStateToProps = state => { }; }; +const mapStateToProps = state => Object.assign( + { + dragInfo: state.scratchGui.assetDrag, + vm: state.scratchGui.vm + }, + getTokenAndUsername(state) +); + export default connect(mapStateToProps)(Backpack); diff --git a/src/containers/costume-tab.jsx b/src/containers/costume-tab.jsx index 9cf6051a2..a4537eb36 100644 --- a/src/containers/costume-tab.jsx +++ b/src/containers/costume-tab.jsx @@ -252,7 +252,8 @@ class CostumeTab extends React.Component { const costumeData = target.costumes ? target.costumes.map(costume => ({ name: costume.name, assetId: costume.assetId, - details: costume.size ? this.formatCostumeDetails(costume.size, costume.bitmapResolution) : null + details: costume.size ? this.formatCostumeDetails(costume.size, costume.bitmapResolution) : null, + dragPayload: costume })) : []; return ( Date: Tue, 19 Jun 2018 12:04:18 -0400 Subject: [PATCH 4/8] Add serializer for png assets --- src/lib/backpack/costume-payload.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/lib/backpack/costume-payload.js b/src/lib/backpack/costume-payload.js index a376c1111..9786ec8d3 100644 --- a/src/lib/backpack/costume-payload.js +++ b/src/lib/backpack/costume-payload.js @@ -19,6 +19,10 @@ const costumePayload = costume => { payload.mime = 'image/svg+xml'; payload.body = assetDataUrl.replace('data:image/svg+xml;base64,', ''); break; + case 'png': + payload.mime = 'image/png'; + payload.body = assetDataUrl.replace('data:image/png;base64,', ''); + break; default: alert(`Cannot serialize for format: ${assetDataFormat}`); // eslint-disable-line } From 8f03486e20e9b91b7991d2c8c6715109017c86b1 Mon Sep 17 00:00:00 2001 From: Paul Kaplan Date: Tue, 19 Jun 2018 16:52:29 -0400 Subject: [PATCH 5/8] Add deleteting via context menu --- src/components/backpack/backpack.jsx | 11 ++++++++--- src/containers/backpack.jsx | 12 +++++++++++- src/lib/backpack-api.js | 19 +++++++++++++++++++ 3 files changed, 38 insertions(+), 4 deletions(-) diff --git a/src/components/backpack/backpack.jsx b/src/components/backpack/backpack.jsx index 0d64307b7..0f7b0b89f 100644 --- a/src/components/backpack/backpack.jsx +++ b/src/components/backpack/backpack.jsx @@ -17,7 +17,7 @@ const dragTypeMap = { sprite: DragConstants.BACKPACK_SPRITE }; -const Backpack = ({contents, dragOver, dropAreaRef, error, expanded, loading, onToggle}) => ( +const Backpack = ({contents, dragOver, dropAreaRef, error, expanded, loading, onToggle, onDelete}) => (
{expanded ? ( -
+
{error ? (
{contents.map(item => ( ))}
@@ -111,6 +115,7 @@ Backpack.propTypes = { error: PropTypes.bool, expanded: PropTypes.bool, loading: PropTypes.bool, + onDelete: PropTypes.func, onToggle: PropTypes.func }; diff --git a/src/containers/backpack.jsx b/src/containers/backpack.jsx index 8bf57d6f9..2d986fbe1 100644 --- a/src/containers/backpack.jsx +++ b/src/containers/backpack.jsx @@ -2,7 +2,7 @@ import React from 'react'; import PropTypes from 'prop-types'; import bindAll from 'lodash.bindall'; import BackpackComponent from '../components/backpack/backpack.jsx'; -import {getBackpackContents, saveBackpackObject, soundPayload, costumePayload} from '../lib/backpack-api'; +import {getBackpackContents, saveBackpackObject, deleteBackpackObject, soundPayload, costumePayload} from '../lib/backpack-api'; import DragConstants from '../lib/drag-constants'; import {connect} from 'react-redux'; @@ -14,6 +14,7 @@ class Backpack extends React.Component { bindAll(this, [ 'handleDrop', 'handleToggle', + 'handleDelete', 'refreshContents', 'setRef' ]); @@ -84,6 +85,14 @@ class Backpack extends React.Component { })) .then(this.refreshContents); } + handleDelete (id) { + deleteBackpackObject({ + host: this.props.host, + token: this.props.token, + username: this.props.username, + id: id + }).then(this.refreshContents); + } refreshContents () { if (this.props.token && this.props.username) { this.setState({loading: true, error: false}); @@ -114,6 +123,7 @@ class Backpack extends React.Component { error={this.state.error} expanded={this.state.expanded} loading={this.state.loading} + onDelete={this.handleDelete} onToggle={this.props.host ? this.handleToggle : null} /> ); diff --git a/src/lib/backpack-api.js b/src/lib/backpack-api.js index 3b1a55e15..9a2b19e95 100644 --- a/src/lib/backpack-api.js +++ b/src/lib/backpack-api.js @@ -49,9 +49,28 @@ const saveBackpackObject = ({ }); }); +const deleteBackpackObject = ({ + host, + username, + token, + id +}) => new Promise((resolve, reject) => { + xhr({ + method: 'DELETE', + uri: `${host}${username}/${id}`, + headers: {'x-token': token} + }, (error, response) => { + if (error || response.statusCode !== 200) { + return reject(); + } + return resolve(response.body); + }); +}); + export { getBackpackContents, saveBackpackObject, + deleteBackpackObject, costumePayload, soundPayload }; From 2ea15a9a6723f3ae216a21e49f3d47f8764a2bfc Mon Sep 17 00:00:00 2001 From: Paul Kaplan Date: Thu, 21 Jun 2018 18:36:12 -0400 Subject: [PATCH 6/8] Fix linting --- src/components/asset-panel/selector.jsx | 1 - src/containers/backpack.jsx | 8 +++++++- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/src/components/asset-panel/selector.jsx b/src/components/asset-panel/selector.jsx index 5c48ae253..80bbeb1df 100644 --- a/src/components/asset-panel/selector.jsx +++ b/src/components/asset-panel/selector.jsx @@ -15,7 +15,6 @@ const Selector = props => { buttons, containerRef, dragType, - dragPayload, items, selectedItemIndex, draggingIndex, diff --git a/src/containers/backpack.jsx b/src/containers/backpack.jsx index 2d986fbe1..0a75a0c6f 100644 --- a/src/containers/backpack.jsx +++ b/src/containers/backpack.jsx @@ -2,7 +2,13 @@ import React from 'react'; import PropTypes from 'prop-types'; import bindAll from 'lodash.bindall'; import BackpackComponent from '../components/backpack/backpack.jsx'; -import {getBackpackContents, saveBackpackObject, deleteBackpackObject, soundPayload, costumePayload} from '../lib/backpack-api'; +import { + getBackpackContents, + saveBackpackObject, + deleteBackpackObject, + soundPayload, + costumePayload +} from '../lib/backpack-api'; import DragConstants from '../lib/drag-constants'; import {connect} from 'react-redux'; From 6a712450f52f1d547ecc514119edaf464351f322 Mon Sep 17 00:00:00 2001 From: Paul Kaplan Date: Mon, 25 Jun 2018 15:28:01 -0400 Subject: [PATCH 7/8] Add comments to make it more clear. --- src/containers/backpack.jsx | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/containers/backpack.jsx b/src/containers/backpack.jsx index 0a75a0c6f..35fc11bf1 100644 --- a/src/containers/backpack.jsx +++ b/src/containers/backpack.jsx @@ -46,13 +46,17 @@ class Backpack extends React.Component { } componentWillReceiveProps (newProps) { const dragTypes = [DragConstants.COSTUME, DragConstants.SOUND]; + // If `dragging` becomes true, record the drop area rectangle if (newProps.dragInfo.dragging && !this.props.dragInfo.dragging) { this.dropAreaRect = this.ref && this.ref.getBoundingClientRect(); + // If `dragging` becomes false, call the drop handler } else if (!newProps.dragInfo.dragging && this.props.dragInfo.dragging && this.state.dragOver) { this.handleDrop(this.props.dragInfo); this.setState({dragOver: false}); } + // If a drag is in progress (currentOffset) and it matches the relevant drag types, + // test if the drag is within the drop area rect and set the state accordingly. if (this.dropAreaRect && newProps.dragInfo.currentOffset && dragTypes.includes(newProps.dragInfo.dragType)) { const {x, y} = newProps.dragInfo.currentOffset; const {top, right, bottom, left} = this.dropAreaRect; From 63e1efa9932cdd10a58a0f73cf102855710c91fb Mon Sep 17 00:00:00 2001 From: Paul Kaplan Date: Tue, 26 Jun 2018 11:05:37 -0400 Subject: [PATCH 8/8] Use a jpg image and base64 encoder to make it easier to work with --- package.json | 1 + src/lib/backpack/sound-payload.js | 9 ++++----- src/lib/backpack/sound-thumbnail.jpg | Bin 0 -> 1278 bytes src/lib/backpack/sound-thumbnail.js | 3 --- 4 files changed, 5 insertions(+), 8 deletions(-) create mode 100644 src/lib/backpack/sound-thumbnail.jpg delete mode 100644 src/lib/backpack/sound-thumbnail.js diff --git a/package.json b/package.json index be7a89e1a..e8ba5a10a 100644 --- a/package.json +++ b/package.json @@ -39,6 +39,7 @@ "babel-plugin-transform-object-rest-spread": "^6.22.0", "babel-preset-env": "^1.6.1", "babel-preset-react": "^6.22.0", + "base64-loader": "1.0.0", "bowser": "1.9.3", "chromedriver": "2.40.0", "classnames": "2.2.6", diff --git a/src/lib/backpack/sound-payload.js b/src/lib/backpack/sound-payload.js index e5e1c992a..7ae573648 100644 --- a/src/lib/backpack/sound-payload.js +++ b/src/lib/backpack/sound-payload.js @@ -1,4 +1,5 @@ -import soundThumbnail from './sound-thumbnail'; +// eslint-disable-next-line import/no-unresolved +import soundThumbnail from '!base64-loader!./sound-thumbnail.jpg'; import storage from '../storage'; const soundPayload = sound => { @@ -7,10 +8,10 @@ const soundPayload = sound => { const payload = { type: 'sound', name: sound.name, + thumbnail: soundThumbnail, // Params to be filled in below mime: '', - body: '', - thumbnail: '' + body: '' }; switch (assetDataFormat) { @@ -22,8 +23,6 @@ const soundPayload = sound => { alert(`Cannot serialize for format: ${assetDataFormat}`); // eslint-disable-line } - payload.thumbnail = soundThumbnail; - // Return a promise to make it consistent with other payload constructors like costume-payload return new Promise(resolve => resolve(payload)); }; diff --git a/src/lib/backpack/sound-thumbnail.jpg b/src/lib/backpack/sound-thumbnail.jpg new file mode 100644 index 0000000000000000000000000000000000000000..f709aaa137f42fba5368fb432fb4af7cadb9b7b4 GIT binary patch literal 1278 zcmex=@azt76w5@M*+jYLLn9fgT#eOMoz*GjT1K> zObQl4wiBcwL6{L}2OH3Mm<-UNiUJCTj)92|jRu7i7jFFU{}uxeP?|}QSrF);s~~NP zk!Ar5t?9Na4}F<+UAA`7cJDP8rYJ(})#@AJm4vnK1Jm}Z@M zysp$Q>Z|CixMvL@{S5U$LpgwcWn^XgI4vj8UifSm>+Bkp>^aY^r2Z<;UBp^CTdg=_ z)ixP_>HD4z@nvCe#O_3Q^vpf-Y3G#r^Tk(%=Y0Efc6GmMdryLx!B?J-w<5l7?c`qy zHkAov>dS1Cg9Sd~vJdx72MaQT1b?tY6fm*bGciJh5llu#py;c=J5{-!>rGucLDMoY zF8s*z>l1!mOfy%HT*3J-``fbF5rH+D?flH^mwi9H+voc2WkSnYB2M!zx@xZTW9d4! zKfE0O8Pa-QPS|kp?3I@-jL$=Y>cUs4w=ZGZvaCZXwXCOK zX$sf{z1reDAwGVs`udM|qkY#s%ropt^qcRH%z3U$_{-*)Pm{al%U179-q>lsuzRn? zRN-_<`_hZ@x0L4HTw=BR)m0X;m#p_+Ptv)yCw}2d|A4}n2cOm!xe6C_OcC5aCGXYM zn$~%Lz*^H|ujl!Vo0?VY&5$6Hkned3@Lp z6hx#Kfk~b!lO$E9bRB7PJ~Gew$mGROKq8(hV6BWmtpYzDf8;EY663UA%F$$GF02{R z$)1|VUg#5Xp+1M5t9Dj!oAN8p(9$RshE)ys!#c|<*e)G-SzaX`e{_>YbK0Ha?k*+O zJq&U^Q77^iCT(0J@8wYZis6r2SnXw@dGnlSyiZQJBrNlXcTPxw<)updrzS0LTGSsa zmf!UWywg(r!q8DrC8t+MLPg|5r+N9KHthr!y#+c||1PmDUctrWt*V&V$aaXs;jQ14 z1lL^&TD!zwfPKgT%w{0BA9*IcwCLf=1=Eajd1Akqul}f=l)rRq^n_MMwW%)>=kq~g klNqe`wbA8GK1%S%(Ez4iZ102be|@Bjb+ literal 0 HcmV?d00001 diff --git a/src/lib/backpack/sound-thumbnail.js b/src/lib/backpack/sound-thumbnail.js deleted file mode 100644 index f965cc8e1..000000000 --- a/src/lib/backpack/sound-thumbnail.js +++ /dev/null @@ -1,3 +0,0 @@ -// image/jpeg base64 encoded sound thumbnail image -// eslint-disable-next-line max-len -export default '/9j/4AAQSkZJRgABAgAAZABkAAD/7AARRHVja3kAAQAEAAAAPAAA/+4AJkFkb2JlAGTAAAAAAQMAFQQDBgoNAAACZgAAAwoAAAPwAAAE/P/bAIQABgQEBAUEBgUFBgkGBQYJCwgGBggLDAoKCwoKDBAMDAwMDAwQDA4PEA8ODBMTFBQTExwbGxscHx8fHx8fHx8fHwEHBwcNDA0YEBAYGhURFRofHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8f/8IAEQgAQABAAwERAAIRAQMRAf/EALAAAQADAQEBAAAAAAAAAAAAAAAEBQcGAgMBAQEBAQAAAAAAAAAAAAAAAAABAgMQAAEEAgICAwAAAAAAAAAAAAMBAgQFIBEQQDASYCQGEQACAQICBggHAAAAAAAAAAABAgMAESFBEDFRcRIEIDBhoSIyQhNAgZGxwWJTEgEAAAAAAAAAAAAAAAAAAABgEwEAAQIGAgIDAQAAAAAAAAABEQAhECAxQVFhQIEwcZGhsfD/2gAMAwEAAhEDEQAAAdUAAAAAIVk2UACFZz2pwvSa1x19ordLrNGVdsVNDvedjlXppXLQy7ril0He87GKzTSuWgKazOOudU5a9RWaXuaAAAAAAAB//9oACAEBAAEFAvGWbFE/E82KBZt8JGuc5zoa/SOdogV1myZzaKq2HE8b30lAX3ZW7BbcW4iMncTyuZSfnxeqV2z2/M2rjyWHjGAWMPUM8dpY9bWJD6X/2gAIAQIAAQUC6Ws0wXBMFx3hvpf/2gAIAQMAAQUC+Af/2gAIAQICBj8CAf/aAAgBAwIGPwIB/9oACAEBAQY/Aur9uSUK5y6VpZApOVFeV8Tn15D60WY3J1moCf5r9qabWFF8KYcPA6+m98O7TNfbphKnBFjLDaLVNyz4pa4G/A0I/wBmjOmQsMHN1OmEAedUUn5XqbmWwS3CD3mhJtZpD0DgElycfmvakXxZdu6oo5BqRQyndTw+UMLYZUzFuN2wv2fBf//aAAgBAQMBPyH4w+6y888e8wK/Yd2OYJamasJkfQXL6ikQPlLqtEOQX1+tJ6/x3VtNruFuMYphTp9AYwnOihP0s1zyk4sfdqu5Y7GJP6GLvTiVE2cZP3XRH9ointmkOrvq1QQW6QXf65Is2rxfoalPUHFc4PKtckUTcIiUEb+UburVx0sIgbb6+F//2gAIAQIDAT8h+OM0UYHBIx0YmuF0zDXC6ZCk04MvC//aAAgBAwMBPyHwpzuQyOQyxkjwv//aAAwDAQACEQMRAAAQkkkkkhkklIrEhkPEnkPEk6Pkkkkkkkkk/9oACAEBAwE/EPjj8Ql0GhYJP6UIgjI3EylYiQdlZgdxTFjQf2wHCn2aU4Yj6glVdVokAKqA31aJdngG0sDpd3oXX8WyOINm3HOLiiIlvAAejFrIbqFisawfSkBz6gD8RlZ90xKenkOY72Ng0hMc/A2cVHA50nk/5TSE7IQn4yF33UxR3IRz6DFBESRsjSwYJBTwiTd34oYrYAQuoCx6/tIGo6gKAkslIWGBBsIIQO1OlGBFumAquhfo8L//2gAIAQIDAT8Q+MTmE6Vy4amgljJtDF30N6vxK2JvpbVbkIUBJpXoYZ8Of//aAAgBAwMBPxD45zLTgNKWhnHVi6YDXHVi6YDXIypKKSiHhf/Z';