added creation of three thumbnails
This commit is contained in:
parent
cb871cae36
commit
b5d723fcb0
11 changed files with 200 additions and 69 deletions
|
@ -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',
|
||||||
|
@ -32,7 +35,7 @@ module.exports = {
|
||||||
defaultDescription: 'Open-source, decentralized image and video sharing.',
|
defaultDescription: 'Open-source, decentralized image and video sharing.',
|
||||||
},
|
},
|
||||||
testing: {
|
testing: {
|
||||||
testChannel : '@testpublishchannel', // a channel to make test publishes in
|
testChannel : '@testpublishchannel', // a channel to make test publishes in
|
||||||
testChannelPassword: 'password', // password for the test channel
|
testChannelPassword: 'password', // password for the test channel
|
||||||
},
|
},
|
||||||
api: {
|
api: {
|
||||||
|
|
|
@ -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,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
|
@ -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';
|
||||||
|
|
|
@ -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,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
|
@ -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">
|
||||||
|
|
|
@ -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));
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
|
@ -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 : '',
|
}
|
||||||
|
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);
|
||||||
}
|
}
|
||||||
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) {
|
createThreePotentialThumbnails () {
|
||||||
const value = event.target.value;
|
const videoFile = this.props.publishFile;
|
||||||
this.setState({thumbnailInput: value});
|
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 => {
|
||||||
|
this.setState({error: error.message});
|
||||||
|
});
|
||||||
}
|
}
|
||||||
urlIsAnImage (url) {
|
createThumbnail (file) {
|
||||||
return (url.match(/\.(jpeg|jpg|gif|png)$/) != null);
|
|
||||||
}
|
|
||||||
testImage (url) {
|
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
const xhttp = new XMLHttpRequest();
|
console.log('creating a thumbnail');
|
||||||
xhttp.open('HEAD', url, true);
|
var fileReader = new FileReader();
|
||||||
xhttp.onreadystatechange = () => {
|
fileReader.onload = () => {
|
||||||
if (xhttp.readyState === 4) {
|
console.log('thumbnail loaded');
|
||||||
if (xhttp.status === 200) {
|
const blob = new Blob([fileReader.result], {type: file.type});
|
||||||
resolve();
|
const url = URL.createObjectURL(blob);
|
||||||
} else {
|
let video = document.createElement('video');
|
||||||
reject();
|
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();
|
||||||
};
|
};
|
||||||
xhttp.send();
|
fileReader.readAsArrayBuffer(file);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
updateVideoThumb (event) {
|
selectVideoThumb (dataUrl) {
|
||||||
const imageUrl = event.target.value;
|
// update this.props.selectedFile
|
||||||
const that = this;
|
this.props.onThumbnailFileSelect(dataUrl);
|
||||||
if (this.urlIsAnImage(imageUrl)) {
|
}
|
||||||
this.testImage(imageUrl, 3000)
|
setPossibleThumbnailFiles (fileOne, fileTwo, fileThree) {
|
||||||
.then(() => {
|
console.log('updating thumbnail file options');
|
||||||
console.log('thumbnail is a valid image');
|
this.props.onThumbnailFileOptionsChange(fileOne, fileTwo, fileThree);
|
||||||
that.props.onThumbnailChange('thumbnail', imageUrl);
|
}
|
||||||
that.setState({thumbnailError: null});
|
setClaimAndThumbnail (claim) {
|
||||||
})
|
// set thumbnail claim based on publish claim name
|
||||||
.catch(error => {
|
const url = `${this.props.host}/${this.props.channel}/${claim}.jpeg`;
|
||||||
console.log('encountered an error loading thumbnail image url:', error);
|
this.props.onThumbnailClaimChange(claim, url);
|
||||||
that.props.onThumbnailChange('thumbnail', null);
|
|
||||||
that.setState({thumbnailError: 'That is an invalid image url'});
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
that.props.onThumbnailChange('thumbnail', null);
|
|
||||||
that.setState({thumbnailError: null});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
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><div className="column column--6 column--sml-10">
|
<p className="info-message-placeholder info-message--failure">{this.state.error}</p>
|
||||||
<div className="input-text--primary">
|
{this.props.potentialFiles.map((file, index) => <ThumbnailPreview dataUrl={file} key={index}/>)}
|
||||||
<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>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
|
@ -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,
|
||||||
});
|
});
|
||||||
|
|
|
@ -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
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