diff options
Diffstat (limited to 'frontend/app/views/graph')
| -rw-r--r-- | frontend/app/views/graph/components/audio.list.js | 148 | ||||
| -rw-r--r-- | frontend/app/views/graph/components/graph.header.js | 4 | ||||
| -rw-r--r-- | frontend/app/views/graph/components/page.edit.js | 1 | ||||
| -rw-r--r-- | frontend/app/views/graph/components/page.form.js | 63 | ||||
| -rw-r--r-- | frontend/app/views/graph/graph.actions.js | 15 | ||||
| -rw-r--r-- | frontend/app/views/graph/graph.container.js | 2 | ||||
| -rw-r--r-- | frontend/app/views/graph/graph.css | 111 | ||||
| -rw-r--r-- | frontend/app/views/graph/graph.reducer.js | 45 |
8 files changed, 375 insertions, 14 deletions
diff --git a/frontend/app/views/graph/components/audio.list.js b/frontend/app/views/graph/components/audio.list.js new file mode 100644 index 0000000..011ab08 --- /dev/null +++ b/frontend/app/views/graph/components/audio.list.js @@ -0,0 +1,148 @@ +import React, { Component } from 'react' +import { Link } from 'react-router-dom' +import { connect } from 'react-redux' + +import { history } from 'app/store' +import { unslugify } from 'app/utils' +import actions from 'app/actions' + +class AudioList extends Component { + state = { + playing: false, + play_id: -1, + } + + constructor(props) { + super(props) + this.toggleAudio = this.toggleAudio.bind(this) + this.upload = this.upload.bind(this) + this.audioDidEnd = this.audioDidEnd.bind(this) + } + + componentDidMount() { + this.audioElement = document.createElement('audio') + this.audioElement.addEventListener('ended', this.audioDidEnd) + } + + componentWillUnmount() { + this.audioElement.removeEventListener('ended', this.audioDidEnd) + this.audioElement.pause() + this.audioElement = null + } + + audioDidEnd() { + this.setState({ playing: false }) + } + + upload(e) { + e.preventDefault() + document.body.className = '' + const files = e.dataTransfer ? e.dataTransfer.files : e.target.files + let i + if (!files.length) return + Array.from(files).forEach(file => this.uploadTaggedFile(file, 'audio', file.filename)) + } + + uploadTaggedFile(file, tag, fn) { + return new Promise((resolve, reject) => { + this.setState({ status: "Uploading " + tag + "..." }) + const uploadData = { + tag, + file, + __file_filename: fn, + graph_id: this.props.graph.id, + username: 'swimmer', + } + // console.log(uploadData) + return actions.upload.upload(uploadData).then(data => { + // console.log(data) + resolve({ + ...data.res, + }) + }) + }) + } + + destroyFile(upload) { + return new Promise((resolve, reject) => { + actions.upload.destroy(upload) + .then(() => { + console.log('Destroy successful') + resolve() + }) + .catch(() => { + console.log('Error deleting the file') + reject() + }) + }) + } + + toggleAudio(upload) { + console.log(upload) + let playing = false + if (this.state.play_id === upload.id && this.state.playing) { + this.audioElement.pause() + } else { + this.audioElement.src = upload.url + this.audioElement.currentTime = 0 + this.audioElement.play() + playing = true + } + this.setState({ + playing, + play_id: upload.id, + }) + } + + render() { + const { playing, play_id } = this.state + const { graph } = this.props + // console.log(graph.uploads) + return ( + <div className='box audioList'> + <div className="uploadButton"> + <button> + <span> + {"Upload an audio file"} + </span> + </button> + <input + type="file" + accept="audio/mp3" + onChange={this.upload} + required={this.props.required} + /> + </div> + {graph.uploads.map(upload => ( + <div className='audioItem' key={upload.id} onClick={() => this.toggleAudio(upload)} > + <img + className='playButton' + src={ + (playing && play_id === upload.id) + ? "/static/img/icons_pause_white.svg" + : "/static/img/icons_play_white.svg" + } + /> + <div className='title'> + <div>{unslugify(upload.fn)}</div> + </div> + </div> + ))} + </div> + ) + } +} + +const mapStateToProps = state => ({ + graph: state.graph.show.res, +}) + +const mapDispatchToProps = dispatch => ({ +}) + +export default connect(mapStateToProps, mapDispatchToProps)(AudioList) + + +/* + - upload new audio file + */
\ No newline at end of file diff --git a/frontend/app/views/graph/components/graph.header.js b/frontend/app/views/graph/components/graph.header.js index 46ad962..0766580 100644 --- a/frontend/app/views/graph/components/graph.header.js +++ b/frontend/app/views/graph/components/graph.header.js @@ -9,10 +9,12 @@ function GraphHeader(props) { return ( <header> <div> - <Link to="/" className="logo"><b>{props.site.siteTitle}</b></Link> + <Link to="/" className="logo arrow">{"◁ "}</Link> + <b>{props.site.siteTitle}</b> </div> <div> <button onClick={() => props.graphActions.toggleAddPageForm()}>+ Add page</button> + <button onClick={() => props.graphActions.toggleAudioList()}>+ Audio</button> </div> </header> ) diff --git a/frontend/app/views/graph/components/page.edit.js b/frontend/app/views/graph/components/page.edit.js index 4025726..16a7eef 100644 --- a/frontend/app/views/graph/components/page.edit.js +++ b/frontend/app/views/graph/components/page.edit.js @@ -45,6 +45,7 @@ class PageEdit extends Component { return ( <PageForm data={show.res} + actions={{ graph: this.props.graphActions }} graph={this.props.graph.show.res} onSubmit={this.handleSubmit.bind(this)} /> diff --git a/frontend/app/views/graph/components/page.form.js b/frontend/app/views/graph/components/page.form.js index 8fc00b0..a060698 100644 --- a/frontend/app/views/graph/components/page.form.js +++ b/frontend/app/views/graph/components/page.form.js @@ -2,8 +2,11 @@ import React, { Component } from 'react' import { Link } from 'react-router-dom' import { session } from 'app/session' +import actions from 'app/actions' +import { history } from 'app/store' -import { TextInput, ColorInput, LabelDescription, TextArea, Checkbox, SubmitButton, Loader } from 'app/common' +import { TextInput, ColorInput, Checkbox, LabelDescription, TextArea, SubmitButton, Loader } from 'app/common' +import AudioSelect from 'app/views/audio/components/audio.select' const newPage = (data) => ({ path: '', @@ -14,6 +17,8 @@ const newPage = (data) => ({ x: 0.05, y: 0.05, background_color: '#000000', + background_audio_id: 0, + restart_audio: false, }, ...data, }) @@ -26,6 +31,16 @@ export default class PageForm extends Component { errorFields: new Set([]), } + constructor(props){ + super(props) + this.handleChange = this.handleChange.bind(this) + this.handleSelect = this.handleSelect.bind(this) + this.handleSettingsChange = this.handleSettingsChange.bind(this) + this.handleSettingsSelect = this.handleSettingsSelect.bind(this) + this.handleSubmit = this.handleSubmit.bind(this) + this.handleDelete = this.handleDelete.bind(this) + } + componentDidMount() { const { graph, data, isNew } = this.props const title = isNew ? 'new page' : 'editing ' + data.title @@ -76,6 +91,10 @@ export default class PageForm extends Component { handleSettingsChange(e) { const { name, value } = e.target + this.handleSettingsSelect(name, value) + } + + handleSettingsSelect(name, value) { this.setState({ data: { ...this.state.data, @@ -110,11 +129,17 @@ export default class PageForm extends Component { } } - handleDelete() { + handleDelete(e) { + e && e.preventDefault() + e && e.stopPropagation() const { data } = this.state console.log(data) if (confirm('Really delete this page?')) { - actions.page.delete(page_id) + actions.page.destroy(data) + .then(() => { + this.props.actions.graph.hideEditPageForm() + history.goBack() + }) } } @@ -124,14 +149,14 @@ export default class PageForm extends Component { return ( <div className='box'> <h1>{title}</h1> - <form onSubmit={this.handleSubmit.bind(this)}> + <form onSubmit={this.handleSubmit}> <TextInput title="Path" name="path" required data={data} error={errorFields.has('path')} - onChange={this.handleChange.bind(this)} + onChange={this.handleChange} autoComplete="off" /> <LabelDescription> @@ -143,32 +168,48 @@ export default class PageForm extends Component { required data={data} error={errorFields.has('title')} - onChange={this.handleChange.bind(this)} + onChange={this.handleChange} autoComplete="off" /> <ColorInput - title='BG' + title='BG Color' name='background_color' data={data.settings} - onChange={this.handleSettingsChange.bind(this)} + onChange={this.handleSettingsChange} autoComplete="off" /> <TextArea title="Description" name="description" data={data} - onChange={this.handleChange.bind(this)} + onChange={this.handleChange} + /> + + <AudioSelect + title="Background Audio" + name="background_audio_id" + selected={data.settings.background_audio_id} + onChange={this.handleSettingsSelect} /> + + <Checkbox + label="Restart audio on load" + name="restart_audio" + checked={data.settings.restart_audio} + onChange={this.handleSettingsSelect} + autoComplete="off" + /> + <div className='row buttons'> <SubmitButton title={submitTitle} - onClick={this.handleSubmit.bind(this)} + onClick={this.handleSubmit} /> {!isNew && <SubmitButton title={'Delete'} className='destroy' - onClick={this.handleDelete.bind(this)} + onClick={this.handleDelete} /> } </div> diff --git a/frontend/app/views/graph/graph.actions.js b/frontend/app/views/graph/graph.actions.js index a24ccc2..4185386 100644 --- a/frontend/app/views/graph/graph.actions.js +++ b/frontend/app/views/graph/graph.actions.js @@ -1,4 +1,5 @@ import * as types from 'app/types' +import { api } from 'app/utils' import actions from 'app/actions' export const showAddPageForm = () => dispatch => { @@ -25,6 +26,10 @@ export const toggleEditPageForm = () => dispatch => { dispatch({ type: types.graph.toggle_edit_page_form }) } +export const toggleAudioList = () => dispatch => { + dispatch({ type: types.graph.toggle_audio_list }) +} + export const updateGraphPage = page => dispatch => { dispatch({ type: types.graph.update_graph_page, page }) } @@ -34,4 +39,12 @@ export const setHomePageId = (graph, page) => dispatch => { delete updated_graph.pages updated_graph.home_page_id = page.id actions.graph.update(updated_graph) -}
\ No newline at end of file +} + +export const viewPage = (graph, page) => dispatch => { + api(dispatch, types.api, 'export', `/api/v1/graph/export/${graph.path}`) + .then(result => { + console.log(result) + window.open(`${process.env.EXPORT_HOST}/${graph.path}/${page.path}`) + }) +} diff --git a/frontend/app/views/graph/graph.container.js b/frontend/app/views/graph/graph.container.js index 9e354fc..34c3d9d 100644 --- a/frontend/app/views/graph/graph.container.js +++ b/frontend/app/views/graph/graph.container.js @@ -15,6 +15,7 @@ import PageEdit from './components/page.edit' import GraphHeader from './components/graph.header' import GraphEditor from './components/graph.editor' +import AudioList from './components/audio.list' class GraphContainer extends Component { componentDidMount() { @@ -63,6 +64,7 @@ class GraphContainer extends Component { <div className='sidebar'> {this.props.graph.editor.addingPage && <PageNew />} {this.props.graph.editor.editingPage && <PageEdit />} + {this.props.graph.editor.showingAudio && <AudioList />} </div> </div> </div> diff --git a/frontend/app/views/graph/graph.css b/frontend/app/views/graph/graph.css index 389a55d..171bb38 100644 --- a/frontend/app/views/graph/graph.css +++ b/frontend/app/views/graph/graph.css @@ -29,6 +29,10 @@ max-height: 100%; z-index: 20; } +.sidebar.left { + right: auto; + left: 0; +} .box { width: 15rem; padding: 0.5rem; @@ -65,6 +69,9 @@ justify-content: flex-start; align-items: center; } +.box form label.checkbox.short span { + padding: 0.125rem 0; +} .box form input[type="checkbox"] { margin-left: 0rem; } @@ -100,6 +107,9 @@ padding: 0.25rem; margin-right: 0; } +.box .single .select { + width: 9.25rem; +} .box .selects label { flex-direction: row; width: 6.5rem; @@ -110,6 +120,18 @@ padding: 0.25rem; } +.box form .single label span { + min-width: auto; + width: 4.25rem; + padding: 0.25rem 0; +} +.box .single label { + flex-direction: row; + width: 100%; + margin-right: 0.5px; + min-width: auto; +} + .box form .pair label span { min-width: 3rem; padding: 0.25rem 0; @@ -123,11 +145,45 @@ .box .pair input[type=text] { width: 3rem; } + +.box form .single_text label span { + min-width: auto; + width: 6rem; + padding: 0.25rem 0; +} +.box .single_text input[type=text] { + width: 7rem; +} +.box .single_text label { + flex-direction: row; + justify-content: space-between; + width: 100%; + margin-right: 0.5px; + min-width: auto; +} + .box .position { font-size: smaller; margin-bottom: 0.25rem; } +button.box_corner { + position: absolute; + top: 1.25rem; right: 1.25rem; + padding: 0.5rem; + background: transparent; + border: 0; + border-radius: 4px; + transform: scaleX(-1); +} +button.box_corner:hover { + color: #fff; + background: rgba(64,64,128,0.5); +} +.sidebar.left button.box_corner { + transform: scaleX(1); +} + .box .slider { display: flex; flex-direction: row; @@ -146,6 +202,59 @@ width: 5.5rem; } +/* Upload area */ + +.box .uploadButton { + position: relative; + display: flex; + justify-content: center; + align-items: center; + margin-top: 0.5rem; + margin-bottom: 0.5rem; +} +.uploadButton input[type=file] { + position: absolute; + top: 0; left: 0; + width: 100%; height: 100%; +} +.audioList .audioItem { + display: flex; + justify-content: flex-start; + align-items: center; + cursor: pointer; + padding: 0.125rem 0; +} +.audioList .playButton { + background: transparent; + border: 0; + width: 1.5rem; + height: 1.5rem; + margin-right: 0.5rem; + opacity: 0.8; +} +.audioList .title { + display: flex; + justify-content: flex-start; + align-items: center; + overflow: hidden; + flex: 1; +} +.audioList .title div { + overflow: hidden; + text-overflow: ellipsis; + white-space: pre; + width: 100%; +} +.audioList .audioItem:hover { + background: rgba(255,255,255,0.2); +} +.audioList .audioItem:hover .title { + color: #fff; +} +.audioList .audioItem:hover .playButton { + opacity: 1.0; +} + /* Graph handles */ .handle { @@ -156,7 +265,7 @@ white-space: nowrap; overflow: hidden; text-overflow: ellipsis; - max-width: 8rem; + max-width: 12rem; user-select: none; cursor: arrow; } diff --git a/frontend/app/views/graph/graph.reducer.js b/frontend/app/views/graph/graph.reducer.js index 6be5089..725c256 100644 --- a/frontend/app/views/graph/graph.reducer.js +++ b/frontend/app/views/graph/graph.reducer.js @@ -7,6 +7,8 @@ const initialState = crudState('graph', { editor: { addingPage: false, editingPage: false, + showingAudio: false, + building: false, }, options: { } @@ -36,6 +38,19 @@ export default function graphReducer(state = initialState, action) { } } + case types.upload.upload_complete: + console.log(action) + return { + ...state, + show: { + ...state.show, + res: { + ...state.show.res, + uploads: state.show.res.uploads.concat(action.data.res) + } + } + } + case types.graph.show_add_page_form: return { ...state, @@ -43,6 +58,7 @@ export default function graphReducer(state = initialState, action) { ...state.editor, addingPage: true, editingPage: false, + showingAudio: false, } } @@ -52,6 +68,7 @@ export default function graphReducer(state = initialState, action) { editor: { ...state.editor, addingPage: false, + showingAudio: false, } } @@ -62,6 +79,7 @@ export default function graphReducer(state = initialState, action) { ...state.editor, addingPage: !state.editor.addingPage, editingPage: false, + showingAudio: false, } } @@ -72,6 +90,7 @@ export default function graphReducer(state = initialState, action) { ...state.editor, addingPage: false, editingPage: true, + showingAudio: false, } } @@ -81,6 +100,7 @@ export default function graphReducer(state = initialState, action) { editor: { ...state.editor, editingPage: false, + showingAudio: false, } } @@ -91,9 +111,34 @@ export default function graphReducer(state = initialState, action) { ...state.editor, addingPage: false, editingPage: !state.editor.editingPage, + showingAudio: false, } } + case types.graph.toggle_audio_list: + return { + ...state, + editor: { + ...state.editor, + addingPage: false, + editingPage: false, + showingAudio: !state.editor.showingAudio, + } + } + + case types.api.loading: + if (action.tag !== 'view' && action.tag !== 'export') { + return state + } + return { ...state, editor: { ...state.editor, building: action.tag } } + + case types.api.loaded: + case types.api.error: + if (action.tag !== 'view' && action.tag !== 'export') { + return state + } + return { ...state, editor: { ...state.editor, building: null } } + default: return state } |
