summaryrefslogtreecommitdiff
path: root/frontend/app/views/graph
diff options
context:
space:
mode:
Diffstat (limited to 'frontend/app/views/graph')
-rw-r--r--frontend/app/views/graph/components/audio.list.js148
-rw-r--r--frontend/app/views/graph/components/graph.header.js4
-rw-r--r--frontend/app/views/graph/components/page.edit.js1
-rw-r--r--frontend/app/views/graph/components/page.form.js63
-rw-r--r--frontend/app/views/graph/graph.actions.js15
-rw-r--r--frontend/app/views/graph/graph.container.js2
-rw-r--r--frontend/app/views/graph/graph.css111
-rw-r--r--frontend/app/views/graph/graph.reducer.js45
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
}