diff --git a/config/speechConfig.js.example b/config/speechConfig.js.example index 7abd197c..b1eebc56 100644 --- a/config/speechConfig.js.example +++ b/config/speechConfig.js.example @@ -23,8 +23,11 @@ module.exports = { uploadDirectory: null, // enter file path to where uploads/publishes should be stored }, site: { - name: 'Spee.ch', - host: 'https://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', @@ -32,7 +35,7 @@ module.exports = { defaultDescription: 'Open-source, decentralized image and video sharing.', }, 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 }, api: { diff --git a/react/actions/publish.js b/react/actions/publish.js index 603aca4e..72928fd4 100644 --- a/react/actions/publish.js +++ b/react/actions/publish.js @@ -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, + }; +}; diff --git a/react/constants/publish_action_types.js b/react/constants/publish_action_types.js index 229b570e..b36a67c6 100644 --- a/react/constants/publish_action_types.js +++ b/react/constants/publish_action_types.js @@ -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'; diff --git a/react/containers/Dropzone/index.js b/react/containers/Dropzone/index.js index 07cc66e0..26421d3d 100644 --- a/react/containers/Dropzone/index.js +++ b/react/containers/Dropzone/index.js @@ -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, }; }; diff --git a/react/containers/Dropzone/view.jsx b/react/containers/Dropzone/view.jsx index 90b7c2a4..55896f01 100644 --- a/react/containers/Dropzone/view.jsx +++ b/react/containers/Dropzone/view.jsx @@ -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); } diff --git a/react/containers/PublishForm/view.jsx b/react/containers/PublishForm/view.jsx index a58ddd91..8a24001c 100644 --- a/react/containers/PublishForm/view.jsx +++ b/react/containers/PublishForm/view.jsx @@ -138,11 +138,13 @@ class PublishForm extends React.Component {
+ {/* left column */}
+ {/* right column */}
diff --git a/react/containers/PublishThumbnailInput/index.js b/react/containers/PublishThumbnailInput/index.js index babc1303..6614ed7a 100644 --- a/react/containers/PublishThumbnailInput/index.js +++ b/react/containers/PublishThumbnailInput/index.js @@ -1,17 +1,32 @@ -import {connect} from 'react-redux'; -import {updateMetadata} from 'actions/publish'; +import { connect } from 'react-redux'; +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)); }, }; }; diff --git a/react/containers/PublishThumbnailInput/view.jsx b/react/containers/PublishThumbnailInput/view.jsx index 2ddddb6e..05096ebc 100644 --- a/react/containers/PublishThumbnailInput/view.jsx +++ b/react/containers/PublishThumbnailInput/view.jsx @@ -1,79 +1,122 @@ import React from 'react'; +const ThumbnailPreview = ({dataUrl}) => { + const thumbnailPreviewStyle = { + width : '30%', + padding: '1%', + display: 'inline-block', + } + return ( +
+ { dataUrl ? ( + image preview here + ) : ( +

loading...

+ ) + } +
+ ); +} + class PublishThumbnailInput extends React.Component { constructor (props) { super(props); this.state = { - videoPreviewSrc: null, - thumbnailError : null, - thumbnailInput : '', + error: 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); } - 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}); + 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 => { + this.setState({error: error.message}); + }); } - urlIsAnImage (url) { - return (url.match(/\.(jpeg|jpg|gif|png)$/) != null); - } - testImage (url) { + createThumbnail (file) { 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(); + 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(); }; - xhttp.send(); + fileReader.readAsArrayBuffer(file); }); } - 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}); - }) - .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'}); - }); - } else { - that.props.onThumbnailChange('thumbnail', null); - that.setState({thumbnailError: null}); - } + 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 (
-
- -
-
-

{this.state.thumbnailError}

- { - this.handleInput(event); - this.updateVideoThumb(event); - }} /> -
+ +
+

{this.state.error}

+ {this.props.potentialFiles.map((file, index) => )}
); diff --git a/react/reducers/index.js b/react/reducers/index.js index c80273f3..2fce4d7a 100644 --- a/react/reducers/index.js +++ b/react/reducers/index.js @@ -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, }); diff --git a/react/reducers/publish.js b/react/reducers/publish.js index 1195a37d..3bba5d21 100644 --- a/react/reducers/publish.js +++ b/react/reducers/publish.js @@ -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; } diff --git a/react/reducers/site.js b/react/reducers/site.js new file mode 100644 index 00000000..aa215467 --- /dev/null +++ b/react/reducers/site.js @@ -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; + } +}