diff --git a/client/scss/_dropzone.scss b/client/scss/_dropzone.scss
index 09b8b1bc..a3934edd 100644
--- a/client/scss/_dropzone.scss
+++ b/client/scss/_dropzone.scss
@@ -4,6 +4,7 @@
// be a flex container for children
display: flex;
flex-direction: column;
+ position: relative;
}
.dropzone {
@@ -12,6 +13,7 @@
flex: 1 0 auto;
// be a flex container for children
display: flex;
+ padding: 1em;
-webkit-flex-direction: column;
flex-direction: column;
justify-content: center;
@@ -52,8 +54,37 @@
.dropzone-preview-image {
display: block;
- padding: 1em;
- width: calc(100% - 2em);
+ width: 100%;
+}
+
+.dropzone-preview-memeify {
+ margin-top: 3em;
+}
+
+.dropzone-memeify-button {
+ background: $primary-color;
+ color: #fff;
+ cursor: pointer;
+ font-size: .8em;
+ padding: 3px 6px;
+ position: absolute;
+ right: 0;
+ top: 0;
+ z-index: 3;
+}
+
+.dropzone-memeify-saveMessage {
+ padding-top: .25em;
+ position: relative;
+ top: .5em;
+}
+
+.dropzone-memeify-toolbar {
+ /* TODO: Cleanup `!important` */
+ background: $primary-color !important;
+ left: -1em !important;
+ right: -1em !important;
+ top: -4em !important;
}
.dropzone-instructions-display__chooser-label {
diff --git a/client/src/components/Creatify/EditableFontface/index.js b/client/src/components/Creatify/EditableFontface/index.js
new file mode 100644
index 00000000..2ef131e0
--- /dev/null
+++ b/client/src/components/Creatify/EditableFontface/index.js
@@ -0,0 +1,99 @@
+import React, { Component } from 'react';
+
+const DEFAULT_TEXT_RENDER = (text) => text;
+
+export default class EditableFontface extends Component {
+ constructor(props) {
+ super(props);
+
+ this.state = {
+ blinkSelection: props.blinkSelection == false ? false : true,
+ value: props.value,
+ };
+
+ this.textInput = React.createRef();
+ }
+
+ componentDidMount() {
+ const textInput = this.textInput.current;
+
+ if(textInput) {
+ textInput.focus();
+ }
+ }
+
+ render() {
+ const me = this;
+
+ const {
+ blinkSelection,
+ value
+ } = me.state;
+
+ const {
+ editable = true,
+ fontFace,
+ preview,
+ } = me.props;
+
+ const textRender = fontFace.textRender || DEFAULT_TEXT_RENDER;
+
+ const textStyles = Object.assign({
+ ...(blinkSelection ? {
+ animation: 'textBlink 1s infinite',
+ } : {}),
+ minHeight: '20px',
+ WebkitFontSmoothing: 'antialiased',
+ MozOsxFontSmoothing: 'grayscale',
+ }, fontFace.text, preview ? fontFace.previewOverrides : {});
+
+ const fontInput = (editable === true) ? (
+ me.onKeyPress(e)} onChange={(e) => me.onChange(e)} style={{
+ ...{
+ bottom: 0,
+ opacity: 0,
+ padding: 0,
+ left: 0,
+ position: 'absolute',
+ top: 0,
+ width: '100%',
+ zIndex: 1,
+ },
+ ...(fontFace.editorStyle || {}),
+ }} />
+ ) : null;
+
+ return (
+
+
+ {fontInput}
+
{textRender(value)}
+
+ );
+ }
+
+ onKeyPress(e) {
+ this.setState({
+ blinkSelection: false,
+ value: e.target.value
+ });
+ }
+
+ onChange(e) {
+ this.setState({
+ blinkSelection: false,
+ value: e.target.value
+ });
+ }
+};
+
+export const PRESETS = {
+ 'Green Machine': require('../FontFaces/GreenMachine'),
+ 'Inferno': require('../FontFaces/Inferno'),
+ 'Lazer': require('../FontFaces/Lazer'),
+ 'Neon': require('../FontFaces/Neon'),
+ 'Old Blue': require('../FontFaces/OldBlue'),
+ 'Retro Rainbow': require('../FontFaces/RetroRainbow'),
+ 'The Special': require('../FontFaces/TheSpecial'),
+ 'Vapor Wave': require('../FontFaces/VaporWave'),
+}
diff --git a/client/src/components/Creatify/FontFaces/.gitkeep b/client/src/components/Creatify/FontFaces/.gitkeep
new file mode 100644
index 00000000..e69de29b
diff --git a/client/src/components/Creatify/FontFaces/GreenMachine.js b/client/src/components/Creatify/FontFaces/GreenMachine.js
new file mode 100644
index 00000000..60b0b462
--- /dev/null
+++ b/client/src/components/Creatify/FontFaces/GreenMachine.js
@@ -0,0 +1,15 @@
+module.exports = {
+ container: {},
+ editorStyle: {
+ fontFamily: 'courier, Courier New',
+ fontWeight: 'bold',
+ fontSize: '2em',
+ },
+ text: {
+ color: '#00b700',
+ fontFamily: 'courier, Courier New',
+ fontSize: '2rem',
+ fontWeight: 'bold',
+ textShadow: '1px 1px 2px #003605',
+ },
+};
diff --git a/client/src/components/Creatify/FontFaces/Inferno.js b/client/src/components/Creatify/FontFaces/Inferno.js
new file mode 100644
index 00000000..45fc4b20
--- /dev/null
+++ b/client/src/components/Creatify/FontFaces/Inferno.js
@@ -0,0 +1,19 @@
+module.exports = {
+ container: {},
+ editorStyle: {
+ fontFamily: 'helvetica, Helvetica Nue',
+ fontWeight: 'bold',
+ fontSize: '2em',
+ },
+ text: {
+ fontFamily: 'helvetica, Helvetica Nue',
+ fontWeight: 'bold',
+ fontSize: '2em',
+ color: '#fff',
+ textShadow: '0px 0px 3px #c20000, 0px 0px 3px #e0f2ff, 0px 0px 5px #532761, 0px 0px 20px #670606, 0 0 10px rgba(0, 0, 0, .8), 0 0 10px #fefcc9, 5px -5px 15px #feec85, -10px -10px 20px #ffae34, 10px -20px 25px #ec760c, -10px -30px 30px #cd4606, 0 -40px 35px #973716, 5px -45px 40px #451b0e, 0 -2px 15px rgba(255, 200, 160, .5)',
+ },
+ previewOverrides: {
+ fontSize: '1.5em',
+ padding: '0 1rem 0 1rem',
+ },
+};
diff --git a/client/src/components/Creatify/FontFaces/Lazer.js b/client/src/components/Creatify/FontFaces/Lazer.js
new file mode 100644
index 00000000..96373b4c
--- /dev/null
+++ b/client/src/components/Creatify/FontFaces/Lazer.js
@@ -0,0 +1,23 @@
+module.exports = {
+ container: {},
+ editorStyle: {
+ fontFamily: 'helvetica, Helvetica Nue',
+ fontWeight: 'bold',
+ fontSize: '2em',
+ textTransform: 'uppercase',
+ whiteSpace: 'nowrap',
+ },
+ text: {
+ fontFamily: 'helvetica, Helvetica Nue',
+ fontWeight: 'bold',
+ backgroundImage: 'linear-gradient(180deg, #249bff 0%, #e1f8ff 44%, #3a006b 44%, #ff57d6 100%)',
+ backgroundClip: 'text',
+ fontSize: '2em',
+ color: 'transparent',
+ filter: 'drop-shadow(0 0 .05rem black)',
+ textTransform: 'uppercase',
+ whiteSpace: 'nowrap',
+ WebkitBackgroundClip: 'text',
+ WebkitTextStroke: '0.03em rgba(255, 255, 255, 0.6)',
+ },
+};
diff --git a/client/src/components/Creatify/FontFaces/Neon.js b/client/src/components/Creatify/FontFaces/Neon.js
new file mode 100644
index 00000000..16d0eb20
--- /dev/null
+++ b/client/src/components/Creatify/FontFaces/Neon.js
@@ -0,0 +1,21 @@
+module.exports = {
+ container: {},
+ editorStyle: {
+ fontFamily: 'Helvetica, Arial',
+ fontWeight: 'bold',
+ fontSize: '2em',
+ letterSpacing: '.1em',
+ },
+ text: {
+ color: '#fff',
+ fontFamily: 'Helvetica, Arial',
+ fontSize: '2rem',
+ fontWeight: 'bold',
+ letterSpacing: '.1em',
+ textShadow: '0 0 0.05em #fff, 0 0 0.1em #fff, 0 0 0.2em #fff, 0 0 .2em #ff1de2, 0 0 .4em #ff26e3, 0 0 .5em #ff00de, 0 0 1em #ff61eb, 0 0 1.5em #ff7cee',
+ },
+ previewOverrides: {
+ fontSize: '1.8em',
+ padding: '0 1rem 0 1rem',
+ },
+};
diff --git a/client/src/components/Creatify/FontFaces/OldBlue.js b/client/src/components/Creatify/FontFaces/OldBlue.js
new file mode 100644
index 00000000..99448d87
--- /dev/null
+++ b/client/src/components/Creatify/FontFaces/OldBlue.js
@@ -0,0 +1,39 @@
+import React from 'react';
+
+const charToFullWidth = char => {
+ const c = char.charCodeAt( 0 )
+ return c >= 33 && c <= 126
+ ? String.fromCharCode( ( c - 33 ) + 65281 )
+ : char
+}
+
+const stringToFullWidth =
+
+module.exports = {
+ container: {},
+ editorStyle: {},
+ text: {
+ fontFamily: 'Segoe UI,Helvetica,Arial',
+ },
+ previewOverrides: {
+ height: '2.6rem',
+ },
+ textRender: (text) => {
+ const id = `curve-${text.replace(/[^A-Za-z0-9]/g, '')}-oceanwave`
+ return (
+
+ );
+ },
+};
diff --git a/client/src/components/Creatify/FontFaces/RetroRainbow.js b/client/src/components/Creatify/FontFaces/RetroRainbow.js
new file mode 100644
index 00000000..833c9671
--- /dev/null
+++ b/client/src/components/Creatify/FontFaces/RetroRainbow.js
@@ -0,0 +1,21 @@
+module.exports = {
+ container: {},
+ editorStyle: {
+ fontFamily: 'Arial, sans-serif',
+ fontWeight: 'bold',
+ fontSize: '1.2em',
+ transform: 'scale(1, 1.5)',
+ },
+ text: {
+ fontFamily: 'Arial, sans-serif',
+ fontWeight: 'bold',
+ backgroundImage: 'linear-gradient(to right, #b306a9, #ef2667, #f42e2c, #ffa509, #fdfc00, #55ac2f, #0b13fd, #a804af)',
+ backgroundClip: 'text',
+ fontSize: '1.2em',
+ transform: 'scale(1, 1.5)',
+ color: 'transparent',
+ paddingBottom: '.25em',
+ paddingTop: '.1em',
+ WebkitBackgroundClip: 'text',
+ },
+};
diff --git a/client/src/components/Creatify/FontFaces/TheSpecial.js b/client/src/components/Creatify/FontFaces/TheSpecial.js
new file mode 100644
index 00000000..d7e5087a
--- /dev/null
+++ b/client/src/components/Creatify/FontFaces/TheSpecial.js
@@ -0,0 +1,41 @@
+import React from 'react';
+
+module.exports = {
+ container: {},
+ editorStyle: {
+ fontFamily: 'Arial, sans-serif',
+ fontWeight: 'bold',
+ fontSize: '1.4em',
+ },
+ text: {
+ fontFamily: 'Arial, sans-serif',
+ fontWeight: 'bold',
+ backgroundImage: 'linear-gradient(to right, #b306a9, #ef2667, #f42e2c, #ffa509, #fdfc00, #55ac2f, #0b13fd, #a804af)',
+ backgroundClip: 'text',
+ fontSize: '1.4em',
+ color: 'transparent',
+ paddingBottom: '.25em',
+ paddingTop: '.1em',
+ WebkitBackgroundClip: 'text',
+ },
+ textRender: (text) => {
+ text = text
+ .replace(/love [^\s.!$]+/g, 'love LBRY')
+ .replace(/LBRY/g, 'amazing LBRY')
+ .replace(/julie/gi, 'super Julie')
+ .replace(/tom/gi, 'amazing Tom')
+ .replace(/(btc|bch)/gi, 'LBC')
+ .replace(/\w+ is \w+/gi, 'LBRY is amazing');
+
+ return text.split(/chris[\d\w]*/gi).reduce((result, value, index) => {
+ if(index !== 0) {
+ result.push(
);
+ }
+ result.push({value})
+
+ return result;
+ }, [])
+ },
+};
+
+const THE_FACE = 'data:image/png;base64,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';
diff --git a/client/src/components/Creatify/FontFaces/VaporWave.js b/client/src/components/Creatify/FontFaces/VaporWave.js
new file mode 100644
index 00000000..7d2f0575
--- /dev/null
+++ b/client/src/components/Creatify/FontFaces/VaporWave.js
@@ -0,0 +1,43 @@
+import React from 'react';
+
+const charToFullWidth = char => {
+ const c = char.charCodeAt( 0 )
+ return c >= 33 && c <= 126
+ ? String.fromCharCode( ( c - 33 ) + 65281 )
+ : char
+}
+
+const stringToFullWidth =
+
+module.exports = {
+ container: {},
+ editorStyle: {},
+ text: {
+ fontFamily: 'Segoe UI,Helvetica,Arial',
+ },
+ previewOverrides: {
+ transform: 'rotate(39deg)',
+ height: '7rem',
+ paddingLeft: '2rem',
+ margin: '-2rem 0',
+ },
+ textRender: (text) => {
+ const formattedText = text.toLowerCase().split('').map((char) => {
+ const c = char.charCodeAt( 0 )
+ return (c >= 33 && c <= 126) ? String.fromCharCode(c + 65248) : char
+ }).join('');
+
+ // TODO: Inline the path
+ const id = `curve-${text.replace(/[^A-Za-z0-9]/g, '')}-oceanwave`
+ return (
+
+ );
+ },
+};
diff --git a/client/src/components/Creatify/RichDraggable/index.js b/client/src/components/Creatify/RichDraggable/index.js
new file mode 100644
index 00000000..c0e9e462
--- /dev/null
+++ b/client/src/components/Creatify/RichDraggable/index.js
@@ -0,0 +1,60 @@
+import React, { Component } from 'react';
+import Draggable from 'react-draggable';
+
+let body;
+try {
+ body = document.body;
+} catch(e) {}
+
+export default class RichDraggable extends Component {
+ constructor(props) {
+ super(props);
+
+ this.contents = React.createRef();
+ this.state = {
+ height: 0,
+ width: 0,
+ };
+ }
+
+ componentDidMount() {
+ const height = this.contents.current.offsetHeight;
+ const width = this.contents.current.offsetWidth;
+
+ this.setState({
+ height,
+ width,
+ });
+ }
+
+ render() {
+ const me = this;
+
+ const {
+ props,
+ state,
+ } = me;
+
+ const {
+ height: bottom,
+ width: right,
+ } = props.bounds;
+
+ const bounds = {
+ //top: 0,
+ //left: 0,
+ right: right - state.width,
+ bottom: bottom - state.height,
+ };
+
+ return (
+
+
+
+ );
+ }
+};
diff --git a/client/src/components/Creatify/index.js b/client/src/components/Creatify/index.js
new file mode 100644
index 00000000..91c77c9e
--- /dev/null
+++ b/client/src/components/Creatify/index.js
@@ -0,0 +1,236 @@
+import { library } from '@fortawesome/fontawesome-svg-core'
+import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
+
+import React, { Component } from 'react';
+import Select from 'react-select'
+
+import RichDraggable from './RichDraggable';
+import EditableFontface, { PRESETS as FontPresets } from './EditableFontface';
+
+import {
+ faFont,
+ faMinusCircle,
+ faPlusCircle,
+} from '@fortawesome/free-solid-svg-icons';
+
+const getRasterizedCanvas = (contents, width, height) => {
+ return new Promise((resolve) => {
+ // Parse to xHTML for SVG/foreignObject rendering
+ contents = new XMLSerializer().serializeToString(
+ new DOMParser().parseFromString(contents, 'text/html')
+ );
+
+ // Resolves a bug in Chrome where it renders correctly, but
+ // replaces the inline styles with an invalid `background-clip`.
+ if(/Chrome/.test(navigator.userAgent)) {
+ contents = contents.replace(/background\-clip:(\s*text\s*)[;$]/g,
+ (match, group) => (`-webkit-background-clip:text;${match}`)
+ );
+ }
+
+ // Attempt to match font kerning with the DOM.
+ const kerningAndPadding = '';
+ const svgContents = ``;
+
+ const pixelRatio = 2;
+
+ let img = document.createElement('img');
+ let canvas = document.createElement('canvas');
+
+ img.height = canvas.height = height * pixelRatio;
+ img.width = canvas.width = width * pixelRatio;
+ canvas.style.height = `${height}px`;
+ canvas.style.width = `${width}px`;
+
+ let shadowNode = document.createElement('div');
+ Object.assign(shadowNode.style, {
+ height: 0,
+ overflow: 'hidden',
+ width: 0,
+ });
+ document.body.appendChild(shadowNode);
+
+ shadowNode.appendChild(img);
+
+ var svg64 = btoa(unescape(encodeURIComponent(svgContents)));
+ var b64Start = 'data:image/svg+xml;base64,';
+ var image64 = b64Start + svg64;
+ img.addEventListener('load', () => {
+ window.requestAnimationFrame(() => {
+ // We still can't trust Firefox's %$%&* engine, add another 5ms timeout
+ // `background-clip: text` is very broken and does not always render in time.
+ setTimeout(() => {
+ let context = canvas.getContext('2d', { alpha: false });
+ context.clearRect(0, 0, canvas.width, canvas.height);
+ context.fillStyle = 'white';
+ context.imageSmoothingEnabled = false;
+ context.scale(pixelRatio, pixelRatio);
+ context.fillRect(0, 0, canvas.width, canvas.height);
+ context.drawImage(img, 0, 0);
+
+ document.body.removeChild(shadowNode);
+
+ resolve(canvas);
+ }, 10);
+ });
+ });
+ img.src = image64;
+ });
+};
+
+export default class Creatify extends Component {
+ constructor(props) {
+ super(props);
+
+ const fontKeys = Object.keys(FontPresets);
+
+ this.contents = React.createRef();
+
+ const fontOptions = fontKeys.map(
+ (fontName) => (
+ {
+ value: fontName,
+ label: (
+
+
+
+ ),
+ fontName,
+ }
+ )
+ );
+
+ this.state = {
+ activeElement: false,
+ bounds: {},
+ fontName: fontKeys[0],
+ elements: [],
+ fontOptions,
+ };
+ }
+
+ componentDidMount() {
+ // TODO: Fix bounds
+ /*
+ const bounds = this.contents.current.getBoundingClientRect();
+
+ this.setState({
+ bounds,
+ });
+
+ console.log({
+ bounds
+ })
+ */
+ }
+
+ setActiveElement(activeElement) {
+ this.setState({ activeElement });
+ }
+
+ addElement() {
+ const {
+ state
+ } = this;
+
+ const newElementKey = `element-${state.elements.length}-${Date.now()}`;
+
+ const newElement = (
+ this.setActiveElement(newElement)}>
+
+
+ );
+
+ this.setState({
+ elements: [...state.elements, newElement],
+ activeElement: newElement,
+ });
+ }
+
+ removeElement() {
+ const {
+ state
+ } = this;
+
+ const activeElementIndex = state.elements.indexOf(state.activeElement);
+
+ if(state.elements.length === 0 || activeElementIndex === -1) {
+ return;
+ }
+
+ const elements = [...state.elements];
+ elements.splice(activeElementIndex, 1)
+
+ this.setState({
+ activeElement: false,
+ elements,
+ });
+ }
+
+ async onSave() {
+ const renderedCanvas = await this.renderContentsToCanvas();
+
+ if(this.props.onSave) {
+ this.props.onSave(renderedCanvas);
+ }
+ }
+
+ async renderContentsToCanvas() {
+ const me = this;
+
+ const contentsElement = me.contents.current;
+ let contents = contentsElement.outerHTML;
+
+ // Cheap border/handles removal
+ contents = `` + contents;
+
+ const contentsWidth = contentsElement.offsetWidth;
+ const contentsHeight = contentsElement.offsetHeight;
+
+ // Fix the dimensions, fixes when flex is used.
+ contents = `${contents}
`;
+
+ return await getRasterizedCanvas(contents, contentsWidth, contentsHeight);
+ }
+
+ render() {
+ const me = this;
+ const {
+ props,
+ state,
+ } = this;
+
+ // TODO: Abstract into separate package & use CSS Modules.
+ const spacerCss = { width: '.3em' };
+ return (
+
+
+
this.addElement()} />
+
+ this.removeElement()} />
+
+
+
+
+ this.onSave()} style={{ alignItems: 'center', alignSelf: 'stretch', border: '1px solid #fff', borderRadius: '4px', color: '#fff', display: 'flex', padding: '0 0.6em' }}>
+ Save
+
+
+
+ {state.elements}
+ {props.children}
+
+
+ );
+ }
+
+ setFont(fontName) {
+ this.setState({
+ fontName,
+ });
+ }
+};
diff --git a/client/src/containers/Dropzone/view.jsx b/client/src/containers/Dropzone/view.jsx
index b524b014..f7f05a95 100644
--- a/client/src/containers/Dropzone/view.jsx
+++ b/client/src/containers/Dropzone/view.jsx
@@ -1,17 +1,34 @@
import React from 'react';
+
import { validateFile } from '../../utils/file';
+import Creatify from '@components/Creatify';
import DropzonePreviewImage from '@components/DropzonePreviewImage';
import DropzoneDropItDisplay from '@components/DropzoneDropItDisplay';
import DropzoneInstructionsDisplay from '@components/DropzoneInstructionsDisplay';
+import { library } from '@fortawesome/fontawesome-svg-core'
+import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
+import { faEdit } from '@fortawesome/free-solid-svg-icons';
+
class Dropzone extends React.Component {
constructor (props) {
super(props);
+
this.state = {
- dragOver : false,
- mouseOver : false,
- dimPreview: false,
+ dragOver : false,
+ mouseOver : false,
+ dimPreview : false,
+ filePreview: null,
+ memeify : false,
};
+
+ if(props.file) {
+ // No side effects allowed with `getDerivedStateFromProps`, so
+ // we must use `componentDidUpdate` and `constructor` routines.
+ // Note: `FileReader` has an `onloadend` side-effect
+ this.updateFilePreview();
+ }
+
this.handleDrop = this.handleDrop.bind(this);
this.handleDragOver = this.handleDragOver.bind(this);
this.handleDragEnd = this.handleDragEnd.bind(this);
@@ -23,6 +40,25 @@ class Dropzone extends React.Component {
this.handleFileInput = this.handleFileInput.bind(this);
this.chooseFile = this.chooseFile.bind(this);
}
+
+ componentDidUpdate(prevProps) {
+ if(prevProps.file !== this.props.file) {
+ this.updateFilePreview();
+ }
+ }
+
+ updateFilePreview() {
+ if (this.props.file) {
+ const fileReader = new FileReader();
+ fileReader.readAsDataURL(this.props.file);
+ fileReader.onloadend = () => {
+ this.setState({
+ filePreview: fileReader.result
+ });
+ };
+ }
+ }
+
handleDrop (event) {
event.preventDefault();
this.setState({dragOver: false});
@@ -35,9 +71,11 @@ class Dropzone extends React.Component {
}
}
}
+
handleDragOver (event) {
event.preventDefault();
}
+
handleDragEnd (event) {
var dt = event.dataTransfer;
if (dt.items) {
@@ -48,27 +86,34 @@ class Dropzone extends React.Component {
event.dataTransfer.clearData();
}
}
+
handleDragEnter () {
this.setState({dragOver: true, dimPreview: true});
}
+
handleDragLeave () {
this.setState({dragOver: false, dimPreview: false});
}
+
handleMouseEnter () {
this.setState({mouseOver: true, dimPreview: true});
}
+
handleMouseLeave () {
this.setState({mouseOver: false, dimPreview: false});
}
+
handleClick (event) {
event.preventDefault();
document.getElementById('file_input').click();
}
+
handleFileInput (event) {
event.preventDefault();
const fileList = event.target.files;
this.chooseFile(fileList[0]);
}
+
chooseFile (file) {
if (file) {
try {
@@ -80,15 +125,72 @@ class Dropzone extends React.Component {
this.props.selectFile(file);
}
}
+
+ selectFileFromCanvas (canvas) {
+ const destinationFormat = 'image/jpeg';
+
+ canvas.toBlob((blob) => {
+ const file = new File([blob], `memeify-${Math.random().toString(36).substring(7)}.jpg`, {
+ type: destinationFormat,
+ });
+
+ this.props.selectFile(file);
+
+ // TODO: Add ability to reset.
+ this.setState({
+ memeify: false,
+ });
+ }, destinationFormat, 0.95);
+ }
+
render () {
- const { dragOver, mouseOver, dimPreview } = this.state;
+ const { dragOver, mouseOver, dimPreview, filePreview, memeify } = this.state;
const { file, thumbnail, fileError, isUpdate, sourceUrl, fileExt } = this.props;
+
+ const hasContent = !!(file || isUpdate);
+
+ const dropZoneHooks = file ? {} : {
+ onDrop: this.handleDrop,
+ onDragOver: this.handleDragOver,
+ onDragEnd: this.handleDragEnd,
+ onDragEnter: this.handleDragEnter,
+ onDragLeave: this.handleDragLeave,
+ onMouseEnter: this.handleMouseEnter,
+ onMouseLeave: this.handleMouseLeave,
+ onClick: this.handleClick,
+ };
+
+ const dropZonePreviewProps = file ? {
+ dimPreview,
+ file,
+ thumbnail,
+ } : {
+ dimPreview: true,
+ isUpdate: true,
+ sourceUrl,
+ };
+
+ const memeifyContent = memeify && file && filePreview ? (
+ this.selectFileFromCanvas(canvas)}>
+
+
![]({filePreview})
+
+
+ ) : null;
+
+ const dropZoneClassName = 'dropzone' + (dragOver ? ' dropzone--drag-over' : '');
+
return (
{isUpdate && fileExt === 'mp4' ? (
Video updates are currently disabled. This feature will be available soon. You can edit metadata.
) : (
-
+
+ { hasContent && !memeify && fileExt !== 'mp4' && (
+
this.setState({ memeify: !memeify })}>
+ Memeify
+
+ )}
-
- {file || isUpdate ? (
-
- {file ? (
-
- ) : (
-
- )}
+
+ {hasContent ? (
+
+
{ dragOver ? : null }
{ mouseOver ? (
@@ -133,15 +214,15 @@ class Dropzone extends React.Component {
message={fileExt === 'mp4' ? 'Drag & drop new thumbnail' : null}
/>
) : null }
+ {memeifyContent}
) : (
dragOver ?
: (
-
+
)
)}
+ {memeifyContent ?
{`Don't forget to save before you publish.`}
: null}
)}
diff --git a/package.json b/package.json
index 4177efb1..8f066fae 100644
--- a/package.json
+++ b/package.json
@@ -46,6 +46,9 @@
},
"homepage": "https://github.com/lbryio/spee.ch#readme",
"dependencies": {
+ "@fortawesome/fontawesome-svg-core": "^1.2.8",
+ "@fortawesome/free-solid-svg-icons": "^5.5.0",
+ "@fortawesome/react-fontawesome": "^0.1.3",
"axios": "^0.18.0",
"bcrypt": "^2.0.1",
"body-parser": "^1.18.3",
@@ -67,12 +70,14 @@
"passport-local": "^1.0.0",
"prop-types": "^15.6.2",
"react": "^16.4.2",
- "react-feather": "^1.1.4",
"react-dom": "^16.6.1",
+ "react-draggable": "^3.0.5",
+ "react-feather": "^1.1.4",
"react-ga": "^2.5.3",
"react-helmet": "^5.2.0",
"react-redux": "^5.1.1",
"react-router-dom": "^4.3.1",
+ "react-select": "^2.1.1",
"redux": "^4.0.1",
"redux-saga": "^0.16.2",
"sequelize": "^4.41.1",