added creation of three thumbnails

This commit is contained in:
bill bittner 2018-02-05 18:14:12 -08:00
parent cb871cae36
commit b5d723fcb0
11 changed files with 200 additions and 69 deletions

View file

@ -23,8 +23,11 @@ module.exports = {
uploadDirectory: null, // enter file path to where uploads/publishes should be stored uploadDirectory: null, // enter file path to where uploads/publishes should be stored
}, },
site: { site: {
name: 'Spee.ch', title: 'Spee.ch',
host: 'https://spee.ch', host : 'https://spee.ch',
},
publish: {
thumbnailChannel: '@channelName:channelId', // create a channel to use for thumbnail images
}, },
claim: { claim: {
defaultTitle : 'Spee.ch', defaultTitle : 'Spee.ch',

View file

@ -65,3 +65,27 @@ export function toggleMetadataInputs (value) {
value, value,
}; };
}; };
export function updateThumbnailClaim (claim, url) {
return {
type: actions.THUMBNAIL_CLAIM_UPDATE,
claim,
url,
};
};
export function updateThumbnailFileOptions (fileOne, fileTwo, fileThree) {
return {
type: actions.THUMBNAIL_FILES_UPDATE,
fileOne,
fileTwo,
fileThree,
};
};
export function updateThumbnailSelectedFile (file) {
return {
type: actions.THUMBNAIL_FILE_SELECT,
file,
};
};

View file

@ -7,3 +7,6 @@ export const PUBLISH_STATUS_UPDATE = 'PUBLISH_STATUS_UPDATE';
export const ERROR_UPDATE = 'ERROR_UPDATE'; export const ERROR_UPDATE = 'ERROR_UPDATE';
export const SELECTED_CHANNEL_UPDATE = 'SELECTED_CHANNEL_UPDATE'; export const SELECTED_CHANNEL_UPDATE = 'SELECTED_CHANNEL_UPDATE';
export const TOGGLE_METADATA_INPUTS = 'TOGGLE_METADATA_INPUTS'; export const TOGGLE_METADATA_INPUTS = 'TOGGLE_METADATA_INPUTS';
export const THUMBNAIL_CLAIM_UPDATE = 'THUMBNAIL_CLAIM_UPDATE';
export const THUMBNAIL_FILES_UPDATE = 'THUMBNAIL_FILES_UPDATE';
export const THUMBNAIL_FILE_SELECT = 'THUMBNAIL_FILE_SELECT';

View file

@ -5,7 +5,7 @@ import View from './view';
const mapStateToProps = ({ publish }) => { const mapStateToProps = ({ publish }) => {
return { return {
file : publish.file, file : publish.file,
thumbnail: publish.metadata.thumbnail, thumbnail: publish.thumbnail.selectedFile,
fileError: publish.error.file, fileError: publish.error.file,
}; };
}; };

View file

@ -28,7 +28,7 @@ class Dropzone extends React.Component {
const dt = event.dataTransfer; const dt = event.dataTransfer;
console.log('dt', dt); console.log('dt', dt);
if (dt.items) { if (dt.items) {
if (dt.items[0].kind == 'file') { if (dt.items[0].kind === 'file') {
const droppedFile = dt.items[0].getAsFile(); const droppedFile = dt.items[0].getAsFile();
this.selectFile(droppedFile); this.selectFile(droppedFile);
} }

View file

@ -138,11 +138,13 @@ class PublishForm extends React.Component {
<div className="column column--10"> <div className="column column--10">
<PublishTitleInput /> <PublishTitleInput />
</div> </div>
{/* left column */}
<div className="column column--5 column--sml-10" > <div className="column column--5 column--sml-10" >
<div className="row row--padded"> <div className="row row--padded">
<Dropzone /> <Dropzone />
</div> </div>
</div> </div>
{/* right column */}
<div className="column column--5 column--sml-10 align-content-top"> <div className="column column--5 column--sml-10 align-content-top">
<div id="publish-active-area" className="row row--padded"> <div id="publish-active-area" className="row row--padded">
<div className="row row--padded row--no-top row--wide"> <div className="row row--padded row--no-top row--wide">

View file

@ -1,17 +1,32 @@
import {connect} from 'react-redux'; import { connect } from 'react-redux';
import {updateMetadata} from 'actions/publish'; import { updateThumbnailClaim, updateThumbnailFileOptions, updateThumbnailSelectedFile } from 'actions/publish';
import View from './view'; import View from './view';
const mapStateToProps = ({ publish }) => { const mapStateToProps = ({ publish, site }) => {
return { return {
thumbnail: publish.metadata.thumbnail, host : site.host,
// file props
publishFile : publish.file,
publishClaim : publish.claim,
// channel props
channel : publish.thumbnail.channel,
claim : publish.thumbnail.claim,
url : publish.thumbnail.url,
potentialFiles: publish.thumbnail.potentialFiles,
selectedFile : publish.thumbnail.selectedFile,
}; };
}; };
const mapDispatchToProps = dispatch => { const mapDispatchToProps = dispatch => {
return { return {
onThumbnailChange: (name, value) => { onThumbnailClaimChange: (claim, url) => {
dispatch(updateMetadata(name, value)); dispatch(updateThumbnailClaim(claim, url));
},
onThumbnailFileOptionsChange: (fileOne, fileTwo, fileThree) => {
dispatch(updateThumbnailFileOptions(fileOne, fileTwo, fileThree));
},
onThumbnailFileSelect: (file) => {
dispatch(updateThumbnailSelectedFile(file));
}, },
}; };
}; };

View file

@ -1,79 +1,122 @@
import React from 'react'; import React from 'react';
const ThumbnailPreview = ({dataUrl}) => {
const thumbnailPreviewStyle = {
width : '30%',
padding: '1%',
display: 'inline-block',
}
return (
<div style={thumbnailPreviewStyle}>
{ dataUrl ? (
<img style={{width: '100%'}} src={dataUrl} alt='image preview here' />
) : (
<p>loading...</p>
)
}
</div>
);
}
class PublishThumbnailInput extends React.Component { class PublishThumbnailInput extends React.Component {
constructor (props) { constructor (props) {
super(props); super(props);
this.state = { this.state = {
videoPreviewSrc: null, error: null,
thumbnailError : null,
thumbnailInput : '',
}
this.handleInput = this.handleInput.bind(this);
this.urlIsAnImage = this.urlIsAnImage.bind(this);
this.testImage = this.testImage.bind(this);
this.updateVideoThumb = this.updateVideoThumb.bind(this);
}
handleInput (event) {
const value = event.target.value;
this.setState({thumbnailInput: value});
}
urlIsAnImage (url) {
return (url.match(/\.(jpeg|jpg|gif|png)$/) != null);
}
testImage (url) {
return new Promise((resolve, reject) => {
const xhttp = new XMLHttpRequest();
xhttp.open('HEAD', url, true);
xhttp.onreadystatechange = () => {
if (xhttp.readyState === 4) {
if (xhttp.status === 200) {
resolve();
} else {
reject();
}
}
}; };
xhttp.send();
});
} }
updateVideoThumb (event) { componentDidMount () {
const imageUrl = event.target.value; this.setClaimAndThumbnail(this.props.publishClaim);
const that = this; this.createThreePotentialThumbnails();
if (this.urlIsAnImage(imageUrl)) { }
this.testImage(imageUrl, 3000) componentWillReceiveProps (nextProps) {
.then(() => { if (nextProps.publishFile !== this.publishFile) {
console.log('thumbnail is a valid image'); // this.createThreePotentialThumbnails();
that.props.onThumbnailChange('thumbnail', imageUrl); }
that.setState({thumbnailError: null}); if (nextProps.publishClaim !== this.props.publishClaim) {
console.log(nextProps.publishClaim, this.props.publishClaim);
this.setClaimAndThumbnail(nextProps.publishClaim);
}
}
createThreePotentialThumbnails () {
const videoFile = this.props.publishFile;
console.log('video file', videoFile);
Promise.all([this.createThumbnail(videoFile), this.createThumbnail(videoFile), this.createThumbnail(videoFile)])
.then(([thumbOne, thumbTwo, thumbThree]) => {
// set the potential thumbnails
console.log([thumbOne, thumbTwo, thumbThree]);
this.selectVideoThumb(thumbOne);
this.setPossibleThumbnailFiles(thumbOne, thumbTwo, thumbThree);
}) })
.catch(error => { .catch(error => {
console.log('encountered an error loading thumbnail image url:', error); this.setState({error: error.message});
that.props.onThumbnailChange('thumbnail', null);
that.setState({thumbnailError: 'That is an invalid image url'});
}); });
} else {
that.props.onThumbnailChange('thumbnail', null);
that.setState({thumbnailError: null});
} }
createThumbnail (file) {
return new Promise((resolve, reject) => {
console.log('creating a thumbnail');
var fileReader = new FileReader();
fileReader.onload = () => {
console.log('thumbnail loaded');
const blob = new Blob([fileReader.result], {type: file.type});
const url = URL.createObjectURL(blob);
let video = document.createElement('video');
const timeupdate = () => {
if (snapImage()) {
video.removeEventListener('timeupdate', timeupdate);
video.pause();
}
};
const snapImage = () => {
let canvas = document.createElement('canvas');
canvas.width = video.videoWidth;
canvas.height = video.videoHeight;
canvas.getContext('2d').drawImage(video, 0, 0, canvas.width, canvas.height);
const imageDataUrl = canvas.toDataURL();
// console.log('imageDataUrl', imageDataUrl);
const success = imageDataUrl.length > 1000;
if (success) { // do something with the image
return resolve(imageDataUrl);
// URL.revokeObjectURL(url);
}
reject(success);
};
video.addEventListener('loadeddata', () => {
if (snapImage()) {
video.removeEventListener('timeupdate', timeupdate);
}
});
video.addEventListener('timeupdate', timeupdate);
video.preload = 'metadata';
video.src = url;
// Load video in Safari / IE11
video.muted = true;
video.playsInline = true;
video.play();
};
fileReader.readAsArrayBuffer(file);
});
}
selectVideoThumb (dataUrl) {
// update this.props.selectedFile
this.props.onThumbnailFileSelect(dataUrl);
}
setPossibleThumbnailFiles (fileOne, fileTwo, fileThree) {
console.log('updating thumbnail file options');
this.props.onThumbnailFileOptionsChange(fileOne, fileTwo, fileThree);
}
setClaimAndThumbnail (claim) {
// set thumbnail claim based on publish claim name
const url = `${this.props.host}/${this.props.channel}/${claim}.jpeg`;
this.props.onThumbnailClaimChange(claim, url);
} }
render () { render () {
return ( return (
<div> <div>
<div className="column column--3 column--sml-10">
<label className="label">Thumbnail:</label> <label className="label">Thumbnail:</label>
</div><div className="column column--6 column--sml-10"> <div>
<div className="input-text--primary"> <p className="info-message-placeholder info-message--failure">{this.state.error}</p>
<p className="info-message-placeholder info-message--failure">{this.state.thumbnailError}</p> {this.props.potentialFiles.map((file, index) => <ThumbnailPreview dataUrl={file} key={index}/>)}
<input
type="text" id="claim-thumbnail-input"
className="input-text input-text--full-width"
placeholder="https://spee.ch/xyz/example.jpg"
value={this.state.thumbnailInput}
onChange={ (event) => {
this.handleInput(event);
this.updateVideoThumb(event);
}} />
</div>
</div> </div>
</div> </div>
); );

View file

@ -2,9 +2,11 @@ import { combineReducers } from 'redux';
import PublishReducer from 'reducers/publish'; import PublishReducer from 'reducers/publish';
import ChannelReducer from 'reducers/channel'; import ChannelReducer from 'reducers/channel';
import ShowReducer from 'reducers/show'; import ShowReducer from 'reducers/show';
import SiteReducer from 'reducers/site';
export default combineReducers({ export default combineReducers({
channel: ChannelReducer, channel: ChannelReducer,
publish: PublishReducer, publish: PublishReducer,
show : ShowReducer, show : ShowReducer,
site : SiteReducer,
}); });

View file

@ -1,5 +1,6 @@
import * as actions from 'constants/publish_action_types'; import * as actions from 'constants/publish_action_types';
import { LOGIN } from 'constants/publish_channel_select_states'; import { LOGIN } from 'constants/publish_channel_select_states';
const { publish } = require('../../config/speechConfig.js');
const initialState = { const initialState = {
publishInChannel : false, publishInChannel : false,
@ -24,6 +25,13 @@ const initialState = {
license : '', license : '',
nsfw : false, nsfw : false,
}, },
thumbnail: {
channel : publish.thumbnailChannel,
claim : null,
url : null,
potentialFiles: [], // should be named 'thumbnailFiles' or something
selectedFile : null,
},
}; };
/* /*
@ -73,6 +81,25 @@ export default function (state = initialState, action) {
return Object.assign({}, state, { return Object.assign({}, state, {
showMetadataInputs: action.value, showMetadataInputs: action.value,
}); });
case actions.THUMBNAIL_CLAIM_UPDATE:
return Object.assign({}, state, {
thumbnail: Object.assign({}, state.thumbnail, {
claim: action.claim,
url : action.url,
}),
});
case actions.THUMBNAIL_FILES_UPDATE:
return Object.assign({}, state, {
thumbnail: Object.assign({}, state.thumbnail, {
potentialFiles: [action.fileOne, action.fileTwo, action.fileThree],
}),
});
case actions.THUMBNAIL_FILE_SELECT:
return Object.assign({}, state, {
thumbnail: Object.assign({}, state.thumbnail, {
selectedFile: action.file,
}),
});
default: default:
return state; return state;
} }

12
react/reducers/site.js Normal file
View file

@ -0,0 +1,12 @@
const { site } = require('../../config/speechConfig.js');
const initialState = {
host: site.host,
};
export default function (state = initialState, action) {
switch (action.type) {
default:
return state;
}
}