added creation of three thumbnails
This commit is contained in:
parent
cb871cae36
commit
b5d723fcb0
11 changed files with 200 additions and 69 deletions
|
@ -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',
|
||||
|
|
|
@ -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,
|
||||
};
|
||||
};
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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,
|
||||
};
|
||||
};
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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">
|
||||
|
|
|
@ -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));
|
||||
},
|
||||
};
|
||||
};
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
|
|
|
@ -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,
|
||||
});
|
||||
|
|
|
@ -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
12
react/reducers/site.js
Normal 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;
|
||||
}
|
||||
}
|
Loading…
Reference in a new issue