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,9 +23,12 @@ module.exports = {
uploadDirectory: null, // enter file path to where uploads/publishes should be stored
},
site: {
name: 'Spee.ch',
title: 'Spee.ch',
host : 'https://spee.ch',
},
publish: {
thumbnailChannel: '@channelName:channelId', // create a channel to use for thumbnail images
},
claim: {
defaultTitle : 'Spee.ch',
defaultThumbnail : 'https://spee.ch/assets/img/video_thumb_default.png',

View file

@ -65,3 +65,27 @@ export function toggleMetadataInputs (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 SELECTED_CHANNEL_UPDATE = 'SELECTED_CHANNEL_UPDATE';
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 }) => {
return {
file : publish.file,
thumbnail: publish.metadata.thumbnail,
thumbnail: publish.thumbnail.selectedFile,
fileError: publish.error.file,
};
};

View file

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

View file

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

View file

@ -1,17 +1,32 @@
import { connect } from 'react-redux';
import {updateMetadata} from 'actions/publish';
import { updateThumbnailClaim, updateThumbnailFileOptions, updateThumbnailSelectedFile } from 'actions/publish';
import View from './view';
const mapStateToProps = ({ publish }) => {
const mapStateToProps = ({ publish, site }) => {
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 => {
return {
onThumbnailChange: (name, value) => {
dispatch(updateMetadata(name, value));
onThumbnailClaimChange: (claim, url) => {
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';
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 {
constructor (props) {
super(props);
this.state = {
videoPreviewSrc: 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();
}
}
error: null,
};
xhttp.send();
});
}
updateVideoThumb (event) {
const imageUrl = event.target.value;
const that = this;
if (this.urlIsAnImage(imageUrl)) {
this.testImage(imageUrl, 3000)
.then(() => {
console.log('thumbnail is a valid image');
that.props.onThumbnailChange('thumbnail', imageUrl);
that.setState({thumbnailError: null});
componentDidMount () {
this.setClaimAndThumbnail(this.props.publishClaim);
this.createThreePotentialThumbnails();
}
componentWillReceiveProps (nextProps) {
if (nextProps.publishFile !== this.publishFile) {
// this.createThreePotentialThumbnails();
}
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 => {
console.log('encountered an error loading thumbnail image url:', error);
that.props.onThumbnailChange('thumbnail', null);
that.setState({thumbnailError: 'That is an invalid image url'});
this.setState({error: error.message});
});
} 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 () {
return (
<div>
<div className="column column--3 column--sml-10">
<label className="label">Thumbnail:</label>
</div><div className="column column--6 column--sml-10">
<div className="input-text--primary">
<p className="info-message-placeholder info-message--failure">{this.state.thumbnailError}</p>
<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>
<p className="info-message-placeholder info-message--failure">{this.state.error}</p>
{this.props.potentialFiles.map((file, index) => <ThumbnailPreview dataUrl={file} key={index}/>)}
</div>
</div>
);

View file

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

View file

@ -1,5 +1,6 @@
import * as actions from 'constants/publish_action_types';
import { LOGIN } from 'constants/publish_channel_select_states';
const { publish } = require('../../config/speechConfig.js');
const initialState = {
publishInChannel : false,
@ -24,6 +25,13 @@ const initialState = {
license : '',
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, {
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:
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;
}
}