diff options
Diffstat (limited to 'animism-align/frontend/views')
8 files changed, 427 insertions, 30 deletions
diff --git a/animism-align/frontend/views/media/components/media.form.js b/animism-align/frontend/views/media/components/media.form.js index 94968df..c82b384 100644 --- a/animism-align/frontend/views/media/components/media.form.js +++ b/animism-align/frontend/views/media/components/media.form.js @@ -84,7 +84,7 @@ export default class MediaForm extends Component { ...this.state.data, settings: { ...this.state.data.settings, - [name]: [value], + [name]: value, } } }) @@ -94,8 +94,8 @@ export default class MediaForm extends Component { e.preventDefault() const { isNew, onSubmit } = this.props const { data } = this.state - const requiredKeys = "title".split(" ") - const validKeys = "title".split(" ") + const requiredKeys = "author title date".split(" ") + const validKeys = "type tag url title author pre_title translated_title date source medium start_ts settings".split(" ") const validData = validKeys.reduce((a,b) => { a[b] = data[b]; return a }, {}) const errorFields = requiredKeys.filter(key => !validData[key]) if (errorFields.length) { @@ -116,12 +116,7 @@ export default class MediaForm extends Component { render() { const { isNew } = this.props const { title, submitTitle, errorFields, data } = this.state - /* - type: '', - tag: '', - url: '', - */ - console.log(data) + // console.log(data) return ( <div className='form'> <h1>{title}</h1> diff --git a/animism-align/frontend/views/media/components/media.formImage.js b/animism-align/frontend/views/media/components/media.formImage.js index b3d227e..c757d03 100644 --- a/animism-align/frontend/views/media/components/media.formImage.js +++ b/animism-align/frontend/views/media/components/media.formImage.js @@ -4,7 +4,9 @@ import { Link } from 'react-router-dom' import { session } from '../../../session' import { capitalize } from '../../../util' -import { TextInput, LabelDescription, Select, TextArea, Checkbox, SubmitButton, Loader } from '../../../common' +import { TextInput, LabelDescription, FileInputField, Select, TextArea, Checkbox, SubmitButton, Loader } from '../../../common' + +import { ImageSelection } from './media.formImageSelection' export default class MediaImageForm extends Component { state = { @@ -15,9 +17,7 @@ export default class MediaImageForm extends Component { this.handleSelect = this.handleSelect.bind(this) this.handleChange = this.handleChange.bind(this) this.handleSettingsChange = this.handleSettingsChange.bind(this) - } - - componentDidMount() { + this.handleUpload = this.handleUpload.bind(this) } handleChange(e) { @@ -33,11 +33,67 @@ export default class MediaImageForm extends Component { this.props.onSettingsChange(name, value) } + handleUpload(image) { + // upload fullsize + this.uploadFullSize(image) + .then(res => { + this.props.onSettingsChange('fullsize', data.res) + setTimeout(() => { + }) + }) + } + + uploadFullSize(image) { + actions.upload.upload({ + image, + tag: 'fullsize', + username: 'animism', + }).then(data => { + console.log(data.res) + return data.res + }) + } + + uploadThumbnail(image) { + actions.upload.upload({ + image, + tag: 'thumbnail', + username: 'animism', + }).then(data => { + console.log(data.res) + }) + } + + uploadCrop(image) { + actions.upload.upload({ + image, + tag: 'crop', + username: 'animism', + }).then(data => { + console.log(data.res) + this.props.onSelect('url', data.res.url) + }) + } + render() { const { data } = this.props console.log(data) return ( <div className='imageForm'> + {!data.url && + <FileInputField + title='Upload image' + onChange={this.handleUpload} + /> + } + {data.settings.fullsize && + <div> + <ImageSelection + url={data.settings.fullsize.url} + crop={data.settings.crop} + /> + </div> + } </div> ) } diff --git a/animism-align/frontend/views/media/components/media.formImageSelection.js b/animism-align/frontend/views/media/components/media.formImageSelection.js new file mode 100644 index 0000000..142525b --- /dev/null +++ b/animism-align/frontend/views/media/components/media.formImageSelection.js @@ -0,0 +1,213 @@ +import React, { Component } from 'react' +import { Link } from 'react-router-dom' +import { bindActionCreators } from 'redux' +import { connect } from 'react-redux' +import toBlob from 'data-uri-to-blob' + +import { clamp } from '../../../util' +import { Loader } from '../../../common' + +const defaultState = { + dragging: false, + draggingBox: false, + bounds: null, + mouseX: 0, + mouseY: 0, + box: { + x: 0, + y: 0, + w: 0, + h: 0, + } +} + +class ImageSelection extends Component { + state = { + ...defaultState + } + + constructor() { + super() + // bind these events in the constructor, so we can remove event listeners later + this.handleMouseDown = this.handleMouseDown.bind(this) + this.handleMouseDownOnBox = this.handleMouseDownOnBox.bind(this) + this.handleMouseMove = this.handleMouseMove.bind(this) + this.handleMouseUp = this.handleMouseUp.bind(this) + this.handleWindowResize = this.handleWindowResize.bind(this) + } + + componentDidMount() { + document.body.addEventListener('mousemove', this.handleMouseMove) + document.body.addEventListener('mouseup', this.handleMouseUp) + window.addEventListener('resize', this.handleWindowResize) + } + + componentDidUpdate(prevProps) { + if (this.state.bounds && this.props.url !== prevProps.url) { + this.setState({ + ...defaultState, + bounds: this.getBoundingClientRect(), + box: this.props.crop || defaultState.box, + }) + } + } + + componentWillUnmount() { + document.body.removeEventListener('mousemove', this.handleMouseMove) + document.body.removeEventListener('mouseup', this.handleMouseUp) + window.removeEventListener('resize', this.handleWindowResize) + } + + getBoundingClientRect() { + if (!this.imgRef) return null + const rect = this.imgRef.getBoundingClientRect() + const scrollTop = document.body.scrollTop || document.body.parentNode.scrollTop + const scrollLeft = document.body.scrollLeft || document.body.parentNode.scrollLeft + const bounds = { + top: rect.top + scrollTop, + left: rect.left + scrollLeft, + width: rect.width, + height: rect.height, + } + return bounds + } + + handleLoad() { + const bounds = this.getBoundingClientRect() + const box = this.props.crop || defaultState.box + this.setState({ bounds, box }) + } + + handleWindowResize() { + if (!this.imgRef) return + const bounds = this.getBoundingClientRect() + this.setState({ bounds }) + } + + handleMouseDown(e) { + e.preventDefault() + const bounds = this.getBoundingClientRect() + const mouseX = e.pageX + const mouseY = e.pageY + const x = (mouseX - bounds.left) / bounds.width + const y = (mouseY - bounds.top) / bounds.height + const w = 1 / bounds.width + const h = 1 / bounds.height + this.setState({ + dragging: true, + bounds, + mouseX, + mouseY, + box: { + x, y, w, h, + } + }) + } + + handleMouseDownOnBox(e) { + const bounds = this.getBoundingClientRect() + const mouseX = e.pageX + const mouseY = e.pageY + this.setState({ + draggingBox: true, + bounds, + mouseX, + mouseY, + initialBox: { + ...this.state.box + }, + box: { + ...this.state.box + } + }) + } + + handleMouseMove(e) { + const { + dragging, draggingBox, + bounds, mouseX, mouseY, initialBox, box + } = this.state + if (dragging) { + e.preventDefault() + let { x, y } = box + let dx = (e.pageX - mouseX) / bounds.width + let dy = (e.pageY - mouseY) / bounds.height + let w = clamp(dx, 0.0, 1.0 - x) + let h = clamp(dy, 0.0, 1.0 - y) + this.setState({ + box: { + x, y, w, h, + } + }) + } else if (draggingBox) { + e.preventDefault() + let { x, y, w, h } = initialBox + let dx = (e.pageX - mouseX) / bounds.width + let dy = (e.pageY - mouseY) / bounds.height + this.setState({ + box: { + x: clamp(x + dx, 0, 1.0 - w), + y: clamp(y + dy, 0, 1.0 - h), + w, + h, + } + }) + } + } + + handleMouseUp(e) { + const { onCrop } = this.props + const { dragging, draggingBox, bounds, box } = this.state + if (!dragging && !draggingBox) return + e.preventDefault() + const { x, y, w, h } = box + let url = window.location.pathname + this.setState({ + dragging: false, + draggingBox: false, + }) + if (w < 10 / bounds.width || h < 10 / bounds.height) { + this.setState({ box: { ...defaultState.box }}) + onCrop({}) + } else { + // pass the box dimensions up - do the search again + onCrop(box) + } + } + + render() { + const { url } = this.props + const { bounds, box } = this.state + const { x, y, w, h } = box + return ( + <div className="imageSelection"> + <img + src={url} + ref={ref => this.imgRef = ref} + onMouseDown={this.handleMouseDown} + onLoad={this.handleLoad.bind(this)} + crossOrigin='anonymous' + /> + {!!w && + <div + className="box" + style={{ + left: x * bounds.width, + top: y * bounds.height, + width: w * bounds.width, + height: h * bounds.height, + }} + onMouseDown={this.handleMouseDownOnBox} + /> + } + </div> + ) + } +} + +const boxToFixed = ({ x, y, w, h }) => ({ + x: x.toFixed(3), + y: y.toFixed(3), + w: w.toFixed(3), + h: h.toFixed(3), +}) diff --git a/animism-align/frontend/views/media/components/media.formVideo.js b/animism-align/frontend/views/media/components/media.formVideo.js index 16c1fbb..89954b9 100644 --- a/animism-align/frontend/views/media/components/media.formVideo.js +++ b/animism-align/frontend/views/media/components/media.formVideo.js @@ -1,11 +1,12 @@ import React, { Component } from 'react' import { Link } from 'react-router-dom' +import VimeoPlayer from '@u-wave/react-vimeo' -import { session } from '../../../session' import { capitalize } from '../../../util' - import { TextInput, LabelDescription, Select, TextArea, Checkbox, SubmitButton, Loader } from '../../../common' +import { getVimeoMetadata } from '../media.actions' + export default class MediaVideoForm extends Component { state = { } @@ -17,27 +18,93 @@ export default class MediaVideoForm extends Component { this.handleSettingsChange = this.handleSettingsChange.bind(this) } - componentDidMount() { - } - handleChange(e) { - const { name, value } = e.target - this.handleSelect(name, value) + let { name, value } = e.target + return this.handleSelect(name, value) } handleSelect(name, value) { - this.props.onSelect(name, value) + value = value.trim() + if (name === 'url') { + getVimeoMetadata(value) + .then(data => { + console.log('video metadata', data) + this.props.onChange(name, value) + setTimeout(() => { + this.props.onSettingsChange('video', { + thumbnail_url: data.thumbnail_url, + duration: data.duration, + video_id: data.video_id, + }) + }, 20) + }) + } else { + this.props.onChange(name, value) + } + } + + handleSettingsChange(e) { + let { name, value } = e.target + this.props.onSettingsChange(name, value) } - handleSettingsChange(name, value) { + handleSettingsSelect(name, value) { this.props.onSettingsChange(name, value) } render() { const { data } = this.props - console.log(data) return ( - <div className='imageForm'> + <div className='videoForm'> + <TextInput + title="Video URL" + name="url" + required + data={data} + onChange={this.handleChange} + autoComplete="off" + /> + + {data.url && + <div> + <LabelDescription className='video'> + <VimeoPlayer video={data.url} /> + </LabelDescription> + + {data.settings.video && data.settings.video.thumbnail && + <LabelDescription className='thumbnail'> + <img src={data.settings.video.thumbnail} /> + </LabelDescription> + } + + <TextInput + title="Start time" + name="video_start_time" + data={data.settings} + placeholder="0:00" + onChange={this.handleSettingsChange} + autoComplete="off" + /> + + <TextInput + title="End time" + name="video_end_time" + data={data.settings} + placeholder="0:00" + onChange={this.handleSettingsChange} + autoComplete="off" + /> + + <TextInput + title="Original duration" + name="original_duration" + data={data.settings} + placeholder="0:00" + onChange={this.handleSettingsChange} + autoComplete="off" + /> + </div> + } </div> ) } diff --git a/animism-align/frontend/views/media/containers/media.index.js b/animism-align/frontend/views/media/containers/media.index.js index 09ef6ca..7797fd7 100644 --- a/animism-align/frontend/views/media/containers/media.index.js +++ b/animism-align/frontend/views/media/containers/media.index.js @@ -79,19 +79,23 @@ class MediaIndex extends Component { } } +const thumbnailURL = data => { + if (data.type === 'video') return data.settings.video.thumbnail_url +} const MediaItem = ({ data }) => { // console.log(data) return ( <div className='cell'> <div className='img'> - <Link to={"/media/" + data.id + "/show/"}> - <img src={data.thumb_url} alt={data.title} /> + <Link to={"/media/" + data.id + "/edit/"}> + <img src={thumbnailURL(data)} alt={data.title} /> </Link> </div> <div className='meta center'> <div> - {data.title}<br /> - {data.author} + <i>{data.title}</i><br /> + {data.author}<br /> + {data.date} </div> </div> </div> diff --git a/animism-align/frontend/views/media/containers/media.new.js b/animism-align/frontend/views/media/containers/media.new.js index 88bf467..e740c0c 100644 --- a/animism-align/frontend/views/media/containers/media.new.js +++ b/animism-align/frontend/views/media/containers/media.new.js @@ -42,7 +42,7 @@ const mapStateToProps = state => ({ }) const mapDispatchToProps = dispatch => ({ - // searchActions: bindActionCreators({ ...searchActions }, dispatch), + // uploadActions: bindActionCreators({ ...uploadActions }, dispatch), }) export default connect(mapStateToProps, mapDispatchToProps)(MediaNew) diff --git a/animism-align/frontend/views/media/media.actions.js b/animism-align/frontend/views/media/media.actions.js new file mode 100644 index 0000000..e33746e --- /dev/null +++ b/animism-align/frontend/views/media/media.actions.js @@ -0,0 +1,10 @@ +import * as types from '../../types' +import { capitalize, api } from '../../util' + +export const getVimeoMetadata = url => { + return api(() => {}, types.vimeo, 'vimeo', 'https://vimeo.com/api/oembed.json', { url }) + .then(data => { + return data + }) + // const id = url.match(/\d+/i)[0]; +} diff --git a/animism-align/frontend/views/media/media.css b/animism-align/frontend/views/media/media.css index 2f3ca0d..251afd6 100644 --- a/animism-align/frontend/views/media/media.css +++ b/animism-align/frontend/views/media/media.css @@ -1,3 +1,55 @@ +.app > .media { + width: 100%; + height: calc(100% - 3.125rem); + overflow: scroll; +} + +/* new / edit media forms */ + .formContainer { padding-top: 1rem; -}
\ No newline at end of file +} + +.imageForm, +.videoForm { + padding: 1rem 1rem 0.5rem 1rem; + margin: 1rem 0; + position: relative; + left: -1rem; + border-radius: 10px; +} + +/* image form */ + +.imageForm { + background: #315; +} + +/* video form */ + +.videoForm { + background: #314; +} +.videoForm .thumbnail img { + max-height: 200px; +} + +/* image crop */ + +.imageSelection { + width: 30rem; + position: relative; +} +.imageSelection img { + display: block; + max-width: 100%; + max-height: 20rem; +} +.imageSelection img.loading { + opacity: 0.5; +} +.imageSelection .box { + position: absolute; + background: rgba(255,32,64,0.05); + border: 1px solid #f24; +} |
