summaryrefslogtreecommitdiff
path: root/frontend
diff options
context:
space:
mode:
Diffstat (limited to 'frontend')
-rw-r--r--frontend/app/actions.js4
-rw-r--r--frontend/app/api/crud.upload.js14
-rw-r--r--frontend/app/common/app.css24
-rw-r--r--frontend/app/common/form.component.js73
-rw-r--r--frontend/app/common/slider.component.js4
-rw-r--r--frontend/app/site/viewer/viewer.container.js96
-rw-r--r--frontend/app/types.js3
-rw-r--r--frontend/app/utils/index.js43
-rw-r--r--frontend/app/views/audio/components/audio.select.js58
-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
-rw-r--r--frontend/app/views/index/containers/graph.index.js1
-rw-r--r--frontend/app/views/page/components/page.editor.js32
-rw-r--r--frontend/app/views/page/components/page.header.js20
-rw-r--r--frontend/app/views/page/components/tile.handle.js146
-rw-r--r--frontend/app/views/page/cursors.css7
-rw-r--r--frontend/app/views/page/page.actions.js24
-rw-r--r--frontend/app/views/page/page.container.js13
-rw-r--r--frontend/app/views/page/page.css9
-rw-r--r--frontend/app/views/page/page.reducer.js29
-rw-r--r--frontend/app/views/tile/components/tile.edit.js (renamed from frontend/app/views/page/components/tile.edit.js)4
-rw-r--r--frontend/app/views/tile/components/tile.form.js (renamed from frontend/app/views/page/components/tile.form.js)388
-rw-r--r--frontend/app/views/tile/components/tile.handle.js8
-rw-r--r--frontend/app/views/tile/components/tile.list.js (renamed from frontend/app/views/page/components/tile.list.js)12
-rw-r--r--frontend/app/views/tile/components/tile.new.js (renamed from frontend/app/views/page/components/tile.new.js)1
-rw-r--r--frontend/app/views/tile/handles/index.js14
-rw-r--r--frontend/app/views/tile/handles/tile.image.js56
-rw-r--r--frontend/app/views/tile/handles/tile.link.js33
-rw-r--r--frontend/app/views/tile/handles/tile.script.js36
-rw-r--r--frontend/app/views/tile/handles/tile.text.js44
-rw-r--r--frontend/app/views/tile/handles/tile.video.js108
-rw-r--r--frontend/app/views/tile/tile.utils.js80
-rw-r--r--frontend/site/actions.js (renamed from frontend/app/site/actions.js)4
-rw-r--r--frontend/site/app.js (renamed from frontend/app/site/app.js)14
-rw-r--r--frontend/site/audio/audio.player.js141
-rw-r--r--frontend/site/audio/audio.reducer.js18
-rw-r--r--frontend/site/index.js (renamed from frontend/app/site/index.js)6
-rw-r--r--frontend/site/site.css20
-rw-r--r--frontend/site/site/site.actions.js (renamed from frontend/app/site/site/site.actions.js)10
-rw-r--r--frontend/site/site/site.reducer.js (renamed from frontend/app/site/site/site.reducer.js)11
-rw-r--r--frontend/site/store.js (renamed from frontend/app/site/store.js)4
-rw-r--r--frontend/site/types.js (renamed from frontend/app/site/types.js)4
-rw-r--r--frontend/site/viewer/viewer.container.js223
49 files changed, 1874 insertions, 354 deletions
diff --git a/frontend/app/actions.js b/frontend/app/actions.js
index 0fba6d1..6cfa470 100644
--- a/frontend/app/actions.js
+++ b/frontend/app/actions.js
@@ -1,5 +1,5 @@
import { bindActionCreators } from 'redux'
-import { actions as crudActions } from './api'
+import { actions as crudActions } from 'app/api'
import * as siteActions from 'app/views/site/site.actions'
@@ -15,4 +15,4 @@ export default
.concat([
// ['socket', socketActions],
])
- .reduce((a,b) => (a[b[0]] = b[1])&&a,{}) \ No newline at end of file
+ .reduce((a,b) => (a[b[0]] = b[1])&&a,{})
diff --git a/frontend/app/api/crud.upload.js b/frontend/app/api/crud.upload.js
index 8c1b265..2837dd4 100644
--- a/frontend/app/api/crud.upload.js
+++ b/frontend/app/api/crud.upload.js
@@ -1,4 +1,5 @@
import { as_type } from 'app/api/crud.types'
+import { session } from 'app/session'
export function crud_upload(type, data, dispatch) {
return new Promise( (resolve, reject) => {
@@ -6,9 +7,17 @@ export function crud_upload(type, data, dispatch) {
const { id } = data
const fd = new FormData()
+ if (!data.tag) {
+ data.tag = 'misc'
+ }
Object.keys(data).forEach(key => {
- if (key !== 'id') {
+ if (key.indexOf('__') !== -1) return
+ if (key === 'id') return
+ const fn_key = `__${key}_filename`
+ if (fn_key in data) {
+ fd.append(key, data[key], data[fn_key])
+ } else {
fd.append(key, data[key])
}
})
@@ -23,12 +32,11 @@ export function crud_upload(type, data, dispatch) {
xhr.addEventListener("error", uploadFailed, false)
xhr.addEventListener("abort", uploadCancelled, false)
xhr.open("POST", url)
+ xhr.setRequestHeader("Authorization", "Bearer " + session.get("access_token"))
xhr.send(fd)
dispatch && dispatch({ type: as_type(type, 'upload_loading')})
- let complete = false
-
function uploadProgress (e) {
if (e.lengthComputable) {
const percent = Math.round(e.loaded * 100 / e.total) || 0
diff --git a/frontend/app/common/app.css b/frontend/app/common/app.css
index d9f9946..486e5fa 100644
--- a/frontend/app/common/app.css
+++ b/frontend/app/common/app.css
@@ -116,6 +116,20 @@ header > div > button:hover {
border-color: #fff;
color: #fff;
}
+
+header .building {
+ display: flex;
+ flex-direction: row;
+ align-items: center;
+ justify-content: flex-start;
+ margin-left: 1rem;
+ color: #888;
+}
+header .building .loader {
+ transform: scale(0.75);
+ margin-right: 0.5rem;
+}
+
header .vcat-btn {
font-size: 0.875rem;
padding-left: 0.5rem;
@@ -147,6 +161,16 @@ header a:active {
header a.navbar-brand {
font-size: .8rem;
}
+header .arrow {
+ padding: 0.5rem 0.5rem 0.5rem 0.5rem;
+ margin-left: -0.5rem;
+ margin-right: 0.25rem;
+ transition: background 0.2s;
+ border-radius: 4px;
+}
+header .arrow:hover {
+ background: rgba(0,0,255,0.5);
+}
header .username {
cursor: pointer;
diff --git a/frontend/app/common/form.component.js b/frontend/app/common/form.component.js
index cf3e466..de1020a 100644
--- a/frontend/app/common/form.component.js
+++ b/frontend/app/common/form.component.js
@@ -23,21 +23,60 @@ export const LabelDescription = props => (
</label>
)
-export const NumberInput = props => (
- <label className={props.error ? 'error' : 'text'}>
- <span>{props.title}</span>
- <input
- type="number"
- required={props.required}
- onChange={props.onChange}
- name={props.name}
- value={props.data[props.name]}
- min={props.min}
- max={props.max}
- step={props.step || 1}
- />
- </label>
-)
+export class NumberInput extends Component {
+ constructor(props) {
+ super(props)
+ this.handleKeyDown = this.handleKeyDown.bind(this)
+ }
+ handleKeyDown(e) {
+ const { min, max, step, data, name, onChange } = this.props
+ const value = data[name]
+ // console.log(e.keyCode)
+ switch (e.keyCode) {
+ case 38: // up
+ if (e.shiftKey) {
+ e.preventDefault()
+ onChange({
+ target: {
+ name,
+ value: Math.min(max, parseFloat(value) + ((step || 1) * 10))
+ }
+ })
+ }
+ break
+ case 40: // down
+ if (e.shiftKey) {
+ e.preventDefault()
+ onChange({
+ target: {
+ name,
+ value: Math.max(min, parseFloat(value) - ((step || 1) * 10))
+ }
+ })
+ }
+ break
+ }
+ }
+ render() {
+ const { props } = this
+ return (
+ <label className={props.error ? 'error' : 'text'}>
+ <span>{props.title}</span>
+ <input
+ type="number"
+ required={props.required}
+ onKeyDown={this.handleKeyDown}
+ onChange={props.onChange}
+ name={props.name}
+ value={props.data[props.name]}
+ min={props.min}
+ max={props.max}
+ step={props.step || 1}
+ />
+ </label>
+ )
+ }
+}
export const ColorInput = props => (
<label className={props.error ? 'error color' : 'text color'}>
@@ -71,12 +110,12 @@ export const TextArea = props => (
)
export const Checkbox = props => (
- <label className="checkbox">
+ <label className={props.className ? props.className + " checkbox" : "checkbox"}>
<input
type="checkbox"
name={props.name}
value={1}
- checked={props.checked}
+ checked={!!props.checked}
onChange={(e) => props.onChange(props.name, e.target.checked)}
/>
<span>{props.label}</span>
diff --git a/frontend/app/common/slider.component.js b/frontend/app/common/slider.component.js
index 9d96b1e..d19ab9b 100644
--- a/frontend/app/common/slider.component.js
+++ b/frontend/app/common/slider.component.js
@@ -53,7 +53,7 @@ export default class Slider extends Component {
}
}
handleKeyDown(e) {
- console.log(e.keyCode)
+ // console.log(e.keyCode)
}
handleRange(e){
let { value: new_value } = e.target
@@ -90,7 +90,7 @@ export default class Slider extends Component {
step = 1
value = this.props.options.indexOf(value)
} else {
- step = (this.props.max - this.props.min) / 100
+ step = this.props.step || (this.props.max - this.props.min) / 100
text_value = parseFloat(value).toFixed(2)
}
return (
diff --git a/frontend/app/site/viewer/viewer.container.js b/frontend/app/site/viewer/viewer.container.js
deleted file mode 100644
index 42ce6c2..0000000
--- a/frontend/app/site/viewer/viewer.container.js
+++ /dev/null
@@ -1,96 +0,0 @@
-import React, { Component } from 'react'
-import { Route } from 'react-router-dom'
-import { bindActionCreators } from 'redux'
-import { connect } from 'react-redux'
-
-import actions from '../actions'
-import { Loader } from 'app/common/loader.component'
-import TileHandle from 'app/views/page/components/tile.handle'
-
-import '../../views/page/page.css'
-
-class ViewerContainer extends Component {
- state = {
- page: {},
- }
-
- constructor(props) {
- super(props)
- this.pageRef = React.createRef()
- this.handleMouseDown = this.handleMouseDown.bind(this)
- }
-
- componentDidUpdate(prevProps) {
- // console.log('didUpdate', this.props.graph !== prevProps.graph, this.props.location.pathname !== prevProps.location.pathname)
- if (this.props.graph !== prevProps.graph || this.props.location.pathname !== prevProps.location.pathname) {
- this.load()
- }
- }
-
- load() {
- const { graph_name, page_name } = this.props.match.params
- const page_path = ["", graph_name, page_name].join('/')
- const { pages, home_page } = this.props.graph
- const page = pages[page_path]
- if (!page) {
- // console.log('-> home page')
- console.log(page_path)
- const { home_page } = this.props.graph
- this.setState({ page: pages[home_page] })
- } else {
- // console.log(page)
- console.log(page_path)
- this.setState({ page })
- }
- }
-
- handleMouseDown(e, tile) {
- // console.log(tile)
- }
-
- render() {
- const { page } = this.state
- if (this.props.graph.loading || !page.id) {
- return (
- <div>
- <div className='body'>
- <div className='page loading'>
- <Loader />
- </div>
- </div>
- </div>
- )
- }
- const { settings } = page
- const pageStyle = { backgroundColor: settings ? settings.background_color : '#000000' }
- // console.log(page)
- return (
- <div className='body'>
- <div className='page' ref={this.pageRef} style={pageStyle}>
- {page.tiles.map(tile => {
- return (
- <TileHandle
- viewing
- key={tile.id}
- tile={tile}
- bounds={this.state.bounds}
- onMouseDown={e => this.handleMouseDown(e, tile)}
- onDoubleClick={e => {}}
- />
- )
- })}
- </div>
- </div>
- )
- }
-}
-
-const mapStateToProps = state => ({
- site: state.site,
- graph: state.site.graph,
-})
-
-const mapDispatchToProps = dispatch => ({
-})
-
-export default connect(mapStateToProps, mapDispatchToProps)(ViewerContainer)
diff --git a/frontend/app/types.js b/frontend/app/types.js
index 7120a91..f0f1e27 100644
--- a/frontend/app/types.js
+++ b/frontend/app/types.js
@@ -6,6 +6,7 @@ export const graph = crud_type('graph', [
'show_add_page_form', 'hide_add_page_form', 'toggle_add_page_form',
'show_edit_page_form', 'hide_edit_page_form', 'toggle_edit_page_form',
'update_graph_page',
+ 'toggle_audio_list',
])
export const page = crud_type('page', [
@@ -14,6 +15,8 @@ export const page = crud_type('page', [
'update_page_tile',
'set_tile_sort_order', 'update_tile_sort_order',
'show_tile_list', 'hide_tile_list', 'toggle_tile_list',
+ 'toggle_popups', 'load_popups',
+ 'toggle_sidebar_side',
])
export const tile = crud_type('tile', [
diff --git a/frontend/app/utils/index.js b/frontend/app/utils/index.js
index bb5e01d..d67d89a 100644
--- a/frontend/app/utils/index.js
+++ b/frontend/app/utils/index.js
@@ -8,6 +8,7 @@ export const formatDateTime = dateStr => format(new Date(dateStr), 'd MMM yyyy H
export const formatDate = dateStr => format(new Date(dateStr), 'd MMM yyyy')
export const formatTime = dateStr => format(new Date(dateStr), 'H:mm')
export const formatAge = dateStr => formatDistance(new Date(), new Date(dateStr)) + ' ago.'
+export const unslugify = fn => fn.replace(/-/g, ' ').replace(/_/g, ' ').replace('.mp3', '')
/* Mobile check */
@@ -49,7 +50,8 @@ export const pad = (n, m) => {
}
export const courtesyS = (n, s) => n + ' ' + (n === 1 ? s : s + 's')
-
+export const capitalize = s => s.split(' ').map(capitalizeWord).join(' ')
+export const capitalizeWord = s => s.substr(0, 1).toUpperCase() + s.substr(1)
export const padSeconds = n => n < 10 ? '0' + n : n
export const timestamp = (n = 0, fps = 25) => {
@@ -61,6 +63,16 @@ export const timestamp = (n = 0, fps = 25) => {
}
return (n % 60) + ':' + s
}
+export const timestampToSeconds = time_str => {
+ const time_str_parts = (time_str || "").trim().split(":").map(s => parseFloat(s))
+ if (time_str_parts.length === 3) {
+ return (time_str_parts[0] * 60 + time_str_parts[1]) * 60 + time_str_parts[2]
+ }
+ if (time_str_parts.length === 2) {
+ return time_str_parts[0] * 60 + time_str_parts[1]
+ }
+ return time_str_parts[0]
+}
export const percent = n => (n * 100).toFixed(1) + '%'
@@ -120,6 +132,35 @@ export const preloadImage = url => (
})
)
+export const preloadVideo = url => (
+ new Promise((resolve, reject) => {
+ const video = document.createElement('video')
+ let loaded = false
+ const bind = () => {
+ video.addEventListener('loadedmetadata', onload)
+ video.addEventListener('error', onerror)
+ }
+ const unbind = () => {
+ video.removeEventListener('loadedmetadata', onload)
+ video.removeEventListener('error', onerror)
+ }
+ const onload = () => {
+ if (loaded) return
+ loaded = true
+ unbind()
+ resolve(video)
+ }
+ const onerror = (error) => {
+ if (loaded) return
+ loaded = true
+ unbind()
+ reject(error)
+ }
+ bind()
+ video.src = url
+ })
+)
+
export const cropImage = (url, crop) => {
return new Promise((resolve, reject) => {
let { x, y, w, h } = crop
diff --git a/frontend/app/views/audio/components/audio.select.js b/frontend/app/views/audio/components/audio.select.js
new file mode 100644
index 0000000..384bb7a
--- /dev/null
+++ b/frontend/app/views/audio/components/audio.select.js
@@ -0,0 +1,58 @@
+import React, { Component } from 'react'
+import { connect } from 'react-redux'
+
+import { Select } from 'app/common'
+import { unslugify } from 'app/utils'
+
+const NO_AUDIO = 0
+const AUDIO_TOP_OPTIONS = [
+ { name: NO_AUDIO, label: 'No Sound' },
+ { name: -2, label: '──────────', disabled: true },
+]
+
+class AudioSelect extends Component {
+ state = {
+ audioList: []
+ }
+
+ constructor(props) {
+ super(props)
+ this.handleChange = this.handleChange.bind(this)
+ }
+
+ componentDidMount(){
+ const { uploads } = this.props.graph.show.res
+ const audioUploads = uploads
+ .filter(upload => upload.tag === 'audio')
+ .map(upload => ({ name: upload.id, label: unslugify(upload.fn) }))
+ let audioList = [
+ ...AUDIO_TOP_OPTIONS,
+ ...audioUploads,
+ ]
+ this.setState({
+ audioList,
+ })
+ }
+
+ handleChange(name, value) {
+ this.props.onChange(name, parseInt(value))
+ }
+
+ render() {
+ return (
+ <Select
+ title={this.props.title || "Audio"}
+ name={this.props.name}
+ selected={this.props.selected || NO_AUDIO}
+ options={this.state.audioList}
+ onChange={this.handleChange}
+ />
+ )
+ }
+}
+
+const mapStateToProps = state => ({
+ graph: state.graph,
+})
+
+export default connect(mapStateToProps)(AudioSelect)
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
}
diff --git a/frontend/app/views/index/containers/graph.index.js b/frontend/app/views/index/containers/graph.index.js
index 91098a7..bf3d75e 100644
--- a/frontend/app/views/index/containers/graph.index.js
+++ b/frontend/app/views/index/containers/graph.index.js
@@ -11,6 +11,7 @@ class GraphIndex extends Component {
componentDidMount() {
actions.graph.index()
}
+
render() {
const { index } = this.props
// console.log(this.props)
diff --git a/frontend/app/views/page/components/page.editor.js b/frontend/app/views/page/components/page.editor.js
index d324874..ec6ddd3 100644
--- a/frontend/app/views/page/components/page.editor.js
+++ b/frontend/app/views/page/components/page.editor.js
@@ -5,17 +5,18 @@ import { connect } from 'react-redux'
import { session } from 'app/session'
import actions from 'app/actions'
-import * as pageActions from '../page.actions'
-import * as tileActions from '../../tile/tile.actions'
+import * as pageActions from 'app/views/page/page.actions'
+import * as tileActions from 'app/views/tile/tile.actions'
import { Loader } from 'app/common'
import { clamp, dist } from 'app/utils'
-import TileHandle from './tile.handle'
+import TileHandle from 'app/views/tile/components/tile.handle'
const defaultState = {
dragging: false,
bounds: null,
+ videoBounds: null,
mouseX: 0,
mouseY: 0,
box: {
@@ -37,6 +38,7 @@ class PageEditor extends Component {
this.handleMouseMove = this.handleMouseMove.bind(this)
this.handleMouseUp = this.handleMouseUp.bind(this)
this.handleWindowResize = this.handleWindowResize.bind(this)
+ this.handlePlaybackEnded = this.handlePlaybackEnded.bind(this)
this.pageRef = React.createRef()
}
@@ -59,7 +61,8 @@ class PageEditor extends Component {
document.body.addEventListener('mousemove', this.handleMouseMove)
document.body.addEventListener('mouseup', this.handleMouseUp)
window.addEventListener('resize', this.handleWindowResize)
- this.setState({ bounds: this.getBoundingClientRect() })
+ const bounds = this.getBoundingClientRect()
+ this.setState({ bounds })
}
componentDidUpdate(prevProps) {
@@ -72,7 +75,12 @@ class PageEditor extends Component {
this.setState({ bounds: this.getBoundingClientRect() })
}
+ handlePlaybackEnded() {
+ //
+ }
+
handleMouseDown(e, tile) {
+ if (e.metaKey || e.ctrlKey || e.altKey || e.button !== 0) return
const bounds = this.getBoundingClientRect()
const mouseX = e.pageX
const mouseY = e.pageY
@@ -130,7 +138,7 @@ class PageEditor extends Component {
const { dx, dy } = box
let url = window.location.pathname
this.setState({
- page: null,
+ tile: null,
box: null,
initialBox: null,
dragging: false,
@@ -142,6 +150,7 @@ class PageEditor extends Component {
}
const updatedTile = {
...tile,
+ target_page_id: tile.target_page_id || 0,
settings: {
...tile.settings,
x: tile.settings.x + dx,
@@ -158,7 +167,7 @@ class PageEditor extends Component {
}
}
- render(){
+ render() {
if (!this.state.bounds || (!this.props.page.show.res && !this.props.page.show.res.tiles)) {
return (
<div className='page' ref={this.pageRef} />
@@ -170,9 +179,14 @@ class PageEditor extends Component {
const { res } = this.props.page.show
const { settings } = res
const pageStyle = { backgroundColor: settings ? settings.background_color : '#000000' }
+ const videoBounds = (res.tiles && res.tiles.length && res.tiles[0].type === 'video') ? {
+ width: res.tiles[0].settings.width,
+ height: res.tiles[0].settings.height,
+ } : this.state.bounds
return (
<div className='page' ref={this.pageRef} style={pageStyle}>
- {res.tiles.map(tile => {
+ {res.tiles && res.tiles.map(tile => {
+ if (!this.props.page.editor.showingPopups && tile.settings.is_popup) return
if (temporaryTile && temporaryTile.id === tile.id) {
tile = temporaryTile
}
@@ -181,9 +195,11 @@ class PageEditor extends Component {
key={tile.id}
tile={tile}
bounds={this.state.bounds}
+ videoBounds={videoBounds}
box={currentTile && tile.id === currentTile.id && currentBox}
onMouseDown={e => this.handleMouseDown(e, tile)}
onDoubleClick={e => this.props.pageActions.showEditTileForm(tile.id)}
+ onPlaybackEnded={this.handlePlaybackEnded}
/>
)
})}
@@ -192,8 +208,10 @@ class PageEditor extends Component {
key={temporaryTile.id}
tile={temporaryTile}
bounds={this.state.bounds}
+ videoBounds={videoBounds}
box={currentTile && temporaryTile.id === currentTile.id && currentBox}
onMouseDown={e => this.handleMouseDown(e, temporaryTile)}
+ onPlaybackEnded={this.handlePlaybackEnded}
/>
)}
</div>
diff --git a/frontend/app/views/page/components/page.header.js b/frontend/app/views/page/components/page.header.js
index eb1c3b9..d40f6e0 100644
--- a/frontend/app/views/page/components/page.header.js
+++ b/frontend/app/views/page/components/page.header.js
@@ -3,6 +3,9 @@ import { bindActionCreators } from 'redux'
import { connect } from 'react-redux'
import { Link } from 'react-router-dom'
+import { Loader } from 'app/common'
+import { capitalize } from 'app/utils'
+
import * as graphActions from '../../graph/graph.actions'
import * as pageActions from '../page.actions'
@@ -10,12 +13,23 @@ function PageHeader(props) {
return (
<header>
<div>
- <Link to={props.graph.show.res ? "/" + props.graph.show.res.path : "/"} className="logo"><b>{props.site.siteTitle}</b></Link>
+ <Link to={props.graph ? "/" + props.graph.path : "/"} className="logo arrow">{"◁"}</Link>
+ <b>{props.site.siteTitle}</b>
+ {props.building && (
+ <div className='building'>
+ <div className='loader'>
+ <Loader />
+ </div>
+ {capitalize(props.building)}ing...
+ </div>
+ )}
</div>
<div>
<button onClick={() => props.pageActions.toggleAddTileForm()}>+ Add tile</button>
<button onClick={() => props.pageActions.toggleTileList()}>Sort tiles</button>
+ <button onClick={() => props.pageActions.togglePopups()}>Toggle popups</button>
<button onClick={() => props.graphActions.toggleEditPageForm()}>Edit page</button>
+ <button onClick={() => props.graphActions.viewPage(props.graph, props.page)}>View page</button>
</div>
</header>
)
@@ -24,7 +38,9 @@ function PageHeader(props) {
const mapStateToProps = (state) => ({
// auth: state.auth,
site: state.site,
- graph: state.graph,
+ graph: state.graph.show.res,
+ page: state.page.show.res,
+ building: state.graph.editor.building,
// isAuthenticated: state.auth.isAuthenticated,
})
diff --git a/frontend/app/views/page/components/tile.handle.js b/frontend/app/views/page/components/tile.handle.js
deleted file mode 100644
index 624b175..0000000
--- a/frontend/app/views/page/components/tile.handle.js
+++ /dev/null
@@ -1,146 +0,0 @@
-import React, { Component } from 'react'
-import { Link } from 'react-router-dom'
-
-const TileHandle = ({ tile, bounds, box, viewing, onMouseDown, onDoubleClick }) => {
- // console.log(tile)
- const { width, height } = tile.settings
- const style = {
- transform: generateTransform(tile, box),
- opacity: tile.settings.opacity,
- }
- // console.log(generateTransform(tile))
- let content;
- let className = ['tile', tile.type].join(' ')
- if (tile.target_page_id || (viewing && tile.href)) {
- className += ' ' + (tile.settings.cursor || 'hand_up')
- }
- // console.log(tile.settings)
- switch (tile.type) {
- case 'image':
- if (!tile.settings.url) {
- return null
- }
- if (tile.settings.is_tiled) {
- style.backgroundImage = 'url(' + tile.settings.url + ')'
- style.backgroundPosition = tile.settings.align.replace('_', ' ')
- switch (tile.settings.tile_style) {
- default:
- case 'tile':
- break
- case 'cover':
- style.backgroundSize = 'cover'
- break
- case 'contain':
- style.backgroundSize = 'contain'
- break
- case 'contain no-repeat':
- style.backgroundSize = 'contain'
- style.backgroundRepeat = 'no-repeat'
- break
- }
- className += ' is_tiled'
- } else {
- className += ' ' + tile.settings.align
- content = <img src={tile.settings.url} />
- }
- break
- case 'text':
- if (!tile.settings.content) {
- return null
- }
- content = <span dangerouslySetInnerHTML={{ __html: tile.settings.content }} />
- className += ' ' + tile.settings.align
- style.width = tile.settings.width ? tile.settings.width + 'px' : 'auto'
- style.height = tile.settings.height ? tile.settings.height + 'px' : 'auto'
- style.fontFamily = tile.settings.font_family
- style.fontSize = tile.settings.font_size + 'px'
- style.lineHeight = 1.5
- style.fontWeight = (tile.settings.font_style || "").indexOf('bold') !== -1 ? 'bold' : 'normal'
- style.fontStyle = (tile.settings.font_style || "").indexOf('italic') !== -1 ? 'italic' : 'normal'
- style.backgroundColor = tile.settings.background_color || 'transparent'
- style.color = tile.settings.font_color || '#dddddd!important'
- break
- case 'link':
- content = ""
- className += ' ' + tile.settings.align
- style.width = tile.settings.width ? tile.settings.width + 'px' : 'auto'
- style.height = tile.settings.height ? tile.settings.height + 'px' : 'auto'
- break
- case 'script':
- content = ""
- if (viewing) {
- eval(tile.settings.content)
- } else {
- content = "SCRIPT"
- }
- }
- if (viewing && tile.href) {
- if (tile.href.indexOf('http') === 0) {
- return (
- <a href={tile.href} rel='noopener'>
- <div
- className={className}
- style={style}
- >
- {content}
- </div>
- </a>
- )
- } else {
- return (
- <Link to={tile.href}>
- <div
- className={className}
- onMouseDown={onMouseDown}
- style={style}
- >
- {content}
- </div>
- </Link>
- )
- }
- } else {
- return (
- <div
- className={className}
- onMouseDown={onMouseDown}
- onDoubleClick={onDoubleClick}
- style={style}
- >
- {content}
- </div>
- )
- }
-}
-
-const generateTransform = (tile, box) => {
- let { x, y, align, rotation, scale, is_tiled } = tile.settings
- if (is_tiled) {
- return 'translateZ(0)'
- }
- if (box) {
- x += box.dx
- y += box.dy
- }
- const [yalign, xalign] = align.split('_')
- let transform = ['translateZ(0)']
- if (yalign === 'center') {
- transform.push('translateY(-50%)')
- }
- if (xalign === 'center') {
- transform.push('translateX(-50%)')
- }
- // if (x % 2 == 1) x += 0.5
- // if (y % 2 == 1) y += 0.5
- transform.push('translateX(' + x + 'px)')
- transform.push('translateY(' + y + 'px)')
- if (scale !== 1) {
- transform.push('scale(' + scale + ')')
- }
- if (rotation !== 0) {
- transform.push('rotateZ(' + rotation + 'deg)')
- }
- return transform.join(' ')
-}
-
-export default TileHandle
diff --git a/frontend/app/views/page/cursors.css b/frontend/app/views/page/cursors.css
index 56fb088..6cc37a9 100644
--- a/frontend/app/views/page/cursors.css
+++ b/frontend/app/views/page/cursors.css
@@ -13,6 +13,13 @@
.tile.hand_left {
cursor: url(/static/img/hand_left.png) 10 60, pointer;
}
+.tile.unclickable {
+ cursor: default;
+ pointer-events: none;
+}
+.tile.none {
+ cursor: default;
+}
.tile.link {
cursor: pointer;
diff --git a/frontend/app/views/page/page.actions.js b/frontend/app/views/page/page.actions.js
index d2bbbe2..e42d539 100644
--- a/frontend/app/views/page/page.actions.js
+++ b/frontend/app/views/page/page.actions.js
@@ -50,6 +50,30 @@ export const toggleTileList = () => dispatch => {
dispatch({ type: types.page.toggle_tile_list })
}
+// Popups
+
+export const loadPopups = (page, popups) => dispatch => {
+ const state = store.getState()
+ page = page || state.page.show.res
+ popups = popups || state.page.editor.popups
+ popups = page.tiles.reduce((acc, tile) => {
+ const { is_popup, popup_group } = tile.settings
+ if (is_popup) {
+ acc[popup_group] = acc[popup_group] || false
+ }
+ return acc
+ }, { ...popups })
+ // console.log(popups)
+ dispatch({ type: types.page.load_popups, popups })
+}
+export const togglePopups = () => dispatch => {
+ dispatch({ type: types.page.toggle_popups })
+}
+
+export const toggleSidebarSide = () => dispatch => {
+ dispatch({ type: types.page.toggle_sidebar_side })
+}
+
// Update local page tile state when we change it
export const updatePageTile = tile => dispatch => {
diff --git a/frontend/app/views/page/page.container.js b/frontend/app/views/page/page.container.js
index dc85f5e..0ad9806 100644
--- a/frontend/app/views/page/page.container.js
+++ b/frontend/app/views/page/page.container.js
@@ -9,13 +9,13 @@ import './cursors.css'
import actions from 'app/actions'
import { Loader } from 'app/common'
-import * as graphActions from '../graph/graph.actions'
+import * as graphActions from 'app/views/graph/graph.actions'
import * as pageActions from './page.actions'
-import PageEdit from '../graph/components/page.edit'
-import TileNew from './components/tile.new'
-import TileEdit from './components/tile.edit'
-import TileList from './components/tile.list'
+import PageEdit from 'app/views/graph/components/page.edit'
+import TileNew from 'app/views/tile/components/tile.new'
+import TileEdit from 'app/views/tile/components/tile.edit'
+import TileList from 'app/views/tile/components/tile.list'
import PageHeader from './components/page.header'
import PageEditor from './components/page.editor'
@@ -44,6 +44,7 @@ class PageContainer extends Component {
this.props.pageActions.showGraphAndPageIfUnloaded(this.props.match.params)
.then(data => {
actions.site.setSiteTitle(data.res.title)
+ this.props.pageActions.loadPopups(data.res, {})
if (!data.res.tiles.length) {
this.props.pageActions.showAddTileForm()
} else {
@@ -70,7 +71,7 @@ class PageContainer extends Component {
<PageHeader />
<div className='body'>
<PageEditor />
- <div className='sidebar'>
+ <div className={this.props.page.editor.sidebarOnRight ? 'sidebar' : 'sidebar left'}>
{this.props.graph.editor.editingPage && <PageEdit />}
{this.props.page.editor.addingTile && <TileNew />}
{this.props.page.editor.editingTile && <TileEdit />}
diff --git a/frontend/app/views/page/page.css b/frontend/app/views/page/page.css
index 4559543..2e0efb9 100644
--- a/frontend/app/views/page/page.css
+++ b/frontend/app/views/page/page.css
@@ -15,6 +15,9 @@
.tile.image {
display: block;
}
+.tile.video {
+ display: block;
+}
.tile.image.is_tiled {
width: 100%;
height: 100%;
@@ -114,6 +117,12 @@
.box .row.pair {
justify-content: space-between;
}
+.box .row.pair.with_checkbox {
+ align-items: flex-end;
+}
+.box .row.single {
+ justify-content: space-between;
+}
.box .pair label:last-child {
margin-right: 0;
}
diff --git a/frontend/app/views/page/page.reducer.js b/frontend/app/views/page/page.reducer.js
index c2d231a..a1f281a 100644
--- a/frontend/app/views/page/page.reducer.js
+++ b/frontend/app/views/page/page.reducer.js
@@ -9,6 +9,9 @@ const initialState = crudState('page', {
editingTile: false,
currentEditTileId: 0,
tileList: false,
+ showingPopups: true,
+ sidebarOnRight: true,
+ popups: {},
},
options: {
}
@@ -195,6 +198,32 @@ export default function pageReducer(state = initialState, action) {
}
}
+ case types.page.toggle_popups:
+ return {
+ ...state,
+ editor: {
+ ...state.editor,
+ showingPopups: !state.editor.showingPopups,
+ }
+ }
+
+ case types.page.load_popups:
+ return {
+ ...state,
+ editor: {
+ ...state.editor,
+ popups: action.popups,
+ }
+ }
+
+ case types.page.toggle_sidebar_side:
+ return {
+ ...state,
+ editor: {
+ ...state.editor,
+ sidebarOnRight: !state.editor.sidebarOnRight,
+ }
+ }
default:
return state
diff --git a/frontend/app/views/page/components/tile.edit.js b/frontend/app/views/tile/components/tile.edit.js
index 2ea09d1..cae9f73 100644
--- a/frontend/app/views/page/components/tile.edit.js
+++ b/frontend/app/views/tile/components/tile.edit.js
@@ -29,7 +29,9 @@ class TileEdit extends Component {
load() {
const { currentEditTileId } = this.props.page.editor
- const tile = this.props.page.show.res.tiles.filter(tile => tile.id === currentEditTileId)[0]
+ const { tiles } = this.props.page.show.res
+ if (!tiles) return
+ const tile = tiles.filter(tile => tile.id === currentEditTileId)[0]
console.log('edit', currentEditTileId)
this.setState({ tile })
}
diff --git a/frontend/app/views/page/components/tile.form.js b/frontend/app/views/tile/components/tile.form.js
index 3f43dd0..8a6a08e 100644
--- a/frontend/app/views/page/components/tile.form.js
+++ b/frontend/app/views/tile/components/tile.form.js
@@ -10,12 +10,14 @@ import {
TextInput, NumberInput, ColorInput, Slider,
Select, LabelDescription, TextArea, Checkbox,
SubmitButton, Loader } from 'app/common'
-import { preloadImage } from 'app/utils'
+import AudioSelect from 'app/views/audio/components/audio.select'
+import { preloadImage, preloadVideo } from 'app/utils'
-import * as tileActions from '../../tile/tile.actions'
+import * as pageActions from 'app/views/page/page.actions'
+import * as tileActions from 'app/views/tile/tile.actions'
const SELECT_TYPES = [
- "image", "text", "link", "script",
+ "image", "text", "video", "link", "script",
].map(s => ({ name: s, label: s }))
const ALIGNMENTS = [
@@ -31,6 +33,7 @@ const ALIGNMENTS = [
const REQUIRED_KEYS = {
image: ['url'],
+ video: ['url'],
text: ['content'],
link: [],
script: [],
@@ -40,6 +43,10 @@ const IMAGE_TILE_STYLES = [
'tile', 'cover', 'contain', 'contain no-repeat'
].map(style => ({ name: style, label: style }))
+const VIDEO_STYLES = [
+ 'normal', 'cover', 'contain',
+].map(style => ({ name: style, label: style }))
+
const TEXT_FONT_FAMILIES = [
'sans-serif', 'serif', 'fantasy', 'monospace', 'cursive',
].map(style => ({ name: style, label: style }))
@@ -49,18 +56,38 @@ const TEXT_FONT_STYLES = [
].map(style => ({ name: style, label: style }))
const CURSORS = [
+ { name: 'none', label: 'None', },
{ name: 'hand_up', label: 'Up', },
{ name: 'hand_down', label: 'Down', },
{ name: 'hand_left', label: 'Left', },
{ name: 'hand_right', label: 'Right', },
+ { name: 'unclickable', label: 'Unclickable', },
+]
+
+const UNITS = [
+ { name: 'px', label: 'pixels' },
+ { name: '%', label: 'percent' },
+ { name: 'video', label: 'video' },
+ { name: 'vmin', label: 'screen min' },
+ { name: 'vmax', label: 'screen max' },
]
const NO_LINK = 0
const EXTERNAL_LINK = -1
+const OPEN_POPUP_LINK = -2
+const CLOSE_POPUP_LINK = -3
const PAGE_LIST_TOP_OPTIONS = [
{ name: NO_LINK, label: 'No link' },
{ name: EXTERNAL_LINK, label: 'External link' },
- { name: -2, label: '──────────', disabled: true },
+ { name: OPEN_POPUP_LINK, label: 'Open popup' },
+ { name: CLOSE_POPUP_LINK, label: 'Close popup' },
+ { name: -99, label: '──────────', disabled: true },
+]
+
+const NO_POPUP = 0
+const POPUP_LIST_TOP_OPTIONS = [
+ { name: NO_POPUP, label: 'Select a popup group' },
+ { name: -99, label: '──────────', disabled: true },
]
// target_page_id = Column(Integer, ForeignKey('page.id'), nullable=True)
@@ -76,7 +103,26 @@ const newImage = (data) => ({
cursor: 'hand_up',
},
type: 'image',
- target_page_id: null,
+ target_page_id: 0,
+ ...data,
+})
+
+const newVideo = (data) => ({
+ settings: {
+ ...newPosition(),
+ video_style: 'cover',
+ url: "",
+ external_link_url: "",
+ cursor: 'none',
+ muted: false,
+ loop_style: false,
+ autoadvance: false,
+ loop_section: false,
+ loop_start: 0,
+ loop_end: 0,
+ },
+ type: 'video',
+ target_page_id: 0,
...data,
})
@@ -91,11 +137,12 @@ const newText = (data) => ({
background_color: 'transparent',
width: 0,
height: 0,
+ units: 'px',
external_link_url: "",
cursor: 'hand_up',
},
type: 'text',
- target_page_id: null,
+ target_page_id: 0,
...data,
})
@@ -104,9 +151,10 @@ const newLink = (data) => ({
...newPosition({ width: 100, height: 100, }),
external_link_url: "",
cursor: 'hand_up',
+ units: 'px',
},
type: 'link',
- target_page_id: null,
+ target_page_id: 0,
...data,
})
@@ -123,12 +171,18 @@ const newPosition = (data) => ({
width: 0, height: 0,
rotation: 0, scale: 1,
opacity: 1,
+ units: false,
align: "center_center",
+ has_audio: false,
+ audio_on_click_id: 0,
+ audio_on_hover_id: 0,
+ navigate_when_audio_finishes: false,
...data,
})
const TYPE_CONSTRUCTORS = {
image: newImage,
+ video: newVideo,
text: newText,
link: newLink,
script: newScript,
@@ -141,6 +195,20 @@ class TileForm extends Component {
errorFields: new Set([]),
modified: false,
pageList: [],
+ popupList: [],
+ }
+
+ 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.handleAlignment = this.handleAlignment.bind(this)
+ this.handleImageChange = this.handleImageChange.bind(this)
+ this.handleVideoChange = this.handleVideoChange.bind(this)
+ this.handleSubmit = this.handleSubmit.bind(this)
+ this.handleDelete = this.handleDelete.bind(this)
}
componentDidMount() {
@@ -158,7 +226,11 @@ class TileForm extends Component {
...PAGE_LIST_TOP_OPTIONS,
...linkPages.map(page => ({ name: page.id, label: page.path }))
]
- this.setState({ pageList })
+ let popupList = [
+ ...POPUP_LIST_TOP_OPTIONS,
+ ...Object.keys(page.editor.popups).map(popup_group => ({ name: popup_group, label: popup_group }))
+ ]
+ this.setState({ pageList, popupList })
if (isNew) {
const newTile = newImage({
id: "new",
@@ -277,6 +349,24 @@ class TileForm extends Component {
})
}
+ handleVideoChange(e) {
+ const { name, value } = e.target
+ this.handleSettingsSelect(name, value)
+ preloadVideo(value).then(video => {
+ // console.log(img)
+ this.props.tileActions.updateTemporaryTile({
+ ...this.props.temporaryTile,
+ settings: {
+ ...this.props.temporaryTile.settings,
+ [name]: value,
+ width: video.videoWidth,
+ height: video.videoHeight,
+ x: 0, y: 0,
+ }
+ })
+ })
+ }
+
clearErrorField(name) {
const { errorFields } = this.state
if (errorFields.has(name)) {
@@ -313,6 +403,7 @@ class TileForm extends Component {
} else {
validData.id = temporaryTile.id
}
+ validData.target_page_id = validData.target_page_id || 0
this.setState({ modified: false })
console.log('submit', validData)
onSubmit(validData)
@@ -340,21 +431,24 @@ class TileForm extends Component {
return (
<div className='box'>
<h1>{title}</h1>
- <form onSubmit={this.handleSubmit.bind(this)}>
+ <button className='box_corner' onClick={this.props.pageActions.toggleSidebarSide}>
+ {'◁'}
+ </button>
+ <form onSubmit={this.handleSubmit}>
<div className="row selects">
<Select
name='type'
selected={temporaryTile.type}
options={SELECT_TYPES}
title=''
- onChange={this.handleSelect.bind(this)}
+ onChange={this.handleSelect}
/>
<Select
name='align'
selected={temporaryTile.settings.align}
options={ALIGNMENTS}
title=''
- onChange={this.handleAlignment.bind(this)}
+ onChange={this.handleAlignment}
/>
</div>
@@ -362,6 +456,8 @@ class TileForm extends Component {
{temporaryTile.type === 'image'
? this.renderImageForm()
+ : temporaryTile.type === 'video'
+ ? this.renderVideoForm()
: temporaryTile.type === 'text'
? this.renderTextForm()
: temporaryTile.type === 'link'
@@ -372,17 +468,18 @@ class TileForm extends Component {
{this.renderHyperlinkForm()}
{this.renderMiscForm()}
+ {this.renderAudioForm()}
<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>
@@ -427,7 +524,7 @@ class TileForm extends Component {
required
data={temporaryTile.settings}
error={errorFields.has('url')}
- onChange={this.handleImageChange.bind(this)}
+ onChange={this.handleImageChange}
autoComplete="off"
/>
</div>
@@ -436,7 +533,7 @@ class TileForm extends Component {
label="Tiled"
name="is_tiled"
checked={temporaryTile.settings.is_tiled}
- onChange={this.handleSettingsSelect.bind(this)}
+ onChange={this.handleSettingsSelect}
autoComplete="off"
/>
{temporaryTile.settings.is_tiled &&
@@ -445,7 +542,7 @@ class TileForm extends Component {
selected={temporaryTile.settings.tile_style || 'tile'}
options={IMAGE_TILE_STYLES}
title=''
- onChange={this.handleSettingsSelect.bind(this)}
+ onChange={this.handleSettingsSelect}
/>
}
</div>
@@ -453,6 +550,103 @@ class TileForm extends Component {
)
}
+ renderVideoForm() {
+ // const { isNew } = this.props
+ const { temporaryTile } = this.props
+ const { errorFields } = this.state
+ // console.log(temporaryTile.settings)
+ return (
+ <div>
+ <div className='row imageUrl'>
+ <TextInput
+ title=""
+ placeholder='http://'
+ name="url"
+ required
+ data={temporaryTile.settings}
+ error={errorFields.has('url')}
+ onChange={this.handleVideoChange}
+ autoComplete="off"
+ />
+ </div>
+ <div className='row pair with_checkbox'>
+ <Select
+ name='video_style'
+ selected={temporaryTile.settings.video_style || 'none'}
+ options={VIDEO_STYLES}
+ title=''
+ onChange={this.handleSettingsSelect}
+ />
+ <Checkbox
+ label="Loop"
+ name="loop"
+ checked={temporaryTile.settings.loop}
+ onChange={this.handleSettingsSelect}
+ autoComplete="off"
+ />
+ </div>
+ <div className='row pair'>
+ <Checkbox
+ label="Muted"
+ name="muted"
+ className='short'
+ checked={temporaryTile.settings.muted}
+ onChange={this.handleSettingsSelect}
+ />
+ <Checkbox
+ label="Autoadvance"
+ name="autoadvance"
+ className='short'
+ checked={temporaryTile.settings.autoadvance}
+ onChange={this.handleSettingsSelect}
+ />
+ </div>
+ {!temporaryTile.settings.muted && (
+ <Slider
+ title='Volume'
+ name='volume'
+ value={('volume' in temporaryTile.settings) ? temporaryTile.settings.volume : 1.0}
+ onChange={this.handleSettingsSelect}
+ min={0.0}
+ max={1.0}
+ step={0.01}
+ />
+ )}
+ {temporaryTile.settings.loop && (
+ <div className='row'>
+ <Checkbox
+ label="Loop section?"
+ className='short'
+ name="loop_section"
+ checked={temporaryTile.settings.loop_section}
+ onChange={this.handleSettingsSelect}
+ />
+ </div>
+ )}
+ {temporaryTile.settings.loop && temporaryTile.settings.loop_section && (
+ <div className='row pair'>
+ <TextInput
+ title="From"
+ placeholder='0:00'
+ name="loop_start"
+ data={temporaryTile.settings}
+ onChange={this.handleSettingsChange}
+ autoComplete="off"
+ />
+ <TextInput
+ title="To"
+ placeholder='0:00'
+ name="loop_end"
+ data={temporaryTile.settings}
+ onChange={this.handleSettingsChange}
+ autoComplete="off"
+ />
+ </div>
+ )}
+ </div>
+ )
+ }
+
renderTextForm() {
const { temporaryTile } = this.props
const { errorFields } = this.state
@@ -464,7 +658,7 @@ class TileForm extends Component {
required
data={temporaryTile.settings}
error={errorFields.has('content')}
- onChange={this.handleSettingsChange.bind(this)}
+ onChange={this.handleSettingsChange}
autoComplete="off"
/>
<div className='row font'>
@@ -474,7 +668,7 @@ class TileForm extends Component {
selected={temporaryTile.settings.font_family || 'sans-serif'}
options={TEXT_FONT_FAMILIES}
title=''
- onChange={this.handleSettingsSelect.bind(this)}
+ onChange={this.handleSettingsSelect}
/>
<NumberInput
title=''
@@ -483,7 +677,7 @@ class TileForm extends Component {
min={1}
max={1200}
error={errorFields.has('font_size')}
- onChange={this.handleSettingsChange.bind(this)}
+ onChange={this.handleSettingsChange}
autoComplete="off"
/>
<Select
@@ -491,7 +685,7 @@ class TileForm extends Component {
selected={temporaryTile.settings.font_style || 'normal'}
options={TEXT_FONT_STYLES}
title=''
- onChange={this.handleSettingsSelect.bind(this)}
+ onChange={this.handleSettingsSelect}
/>
</div>
<ColorInput
@@ -499,7 +693,7 @@ class TileForm extends Component {
name='font_color'
data={temporaryTile.settings}
error={errorFields.has('font_color')}
- onChange={this.handleSettingsChange.bind(this)}
+ onChange={this.handleSettingsChange}
autoComplete="off"
/>
<ColorInput
@@ -507,7 +701,7 @@ class TileForm extends Component {
name='background_color'
data={temporaryTile.settings}
error={errorFields.has('background_color')}
- onChange={this.handleSettingsChange.bind(this)}
+ onChange={this.handleSettingsChange}
autoComplete="off"
/>
<div className='row pair'>
@@ -518,7 +712,7 @@ class TileForm extends Component {
min={0}
max={1200}
error={errorFields.has('width')}
- onChange={this.handleSettingsChange.bind(this)}
+ onChange={this.handleSettingsChange}
autoComplete="off"
/>
<NumberInput
@@ -528,7 +722,7 @@ class TileForm extends Component {
min={0}
max={1200}
error={errorFields.has('height')}
- onChange={this.handleSettingsChange.bind(this)}
+ onChange={this.handleSettingsChange}
autoComplete="off"
/>
</div>
@@ -549,7 +743,7 @@ class TileForm extends Component {
min={0}
max={1200}
error={errorFields.has('width')}
- onChange={this.handleSettingsChange.bind(this)}
+ onChange={this.handleSettingsChange}
autoComplete="off"
/>
<NumberInput
@@ -559,7 +753,7 @@ class TileForm extends Component {
min={0}
max={1200}
error={errorFields.has('height')}
- onChange={this.handleSettingsChange.bind(this)}
+ onChange={this.handleSettingsChange}
autoComplete="off"
/>
</div>
@@ -578,7 +772,7 @@ class TileForm extends Component {
required
data={temporaryTile.settings}
error={errorFields.has('content')}
- onChange={this.handleSettingsChange.bind(this)}
+ onChange={this.handleSettingsChange}
autoComplete="off"
/>
<div>
@@ -590,8 +784,12 @@ class TileForm extends Component {
renderHyperlinkForm() {
const { temporaryTile } = this.props
- const { pageList } = this.state
+ const { pageList, popupList } = this.state
const isExternalLink = temporaryTile.target_page_id === EXTERNAL_LINK
+ const isPopupLink = (
+ temporaryTile.target_page_id === OPEN_POPUP_LINK ||
+ temporaryTile.target_page_id === CLOSE_POPUP_LINK
+ )
return (
<div>
<div className={'row selects'}>
@@ -600,7 +798,7 @@ class TileForm extends Component {
name='target_page_id'
selected={temporaryTile.target_page_id || NO_LINK}
options={pageList}
- onChange={this.handleSelect.bind(this)}
+ onChange={this.handleSelect}
/>
<Select
title=''
@@ -608,21 +806,79 @@ class TileForm extends Component {
selected={temporaryTile.settings.cursor}
options={CURSORS}
defaultOption="Cursor"
- onChange={this.handleSettingsSelect.bind(this)}
+ onChange={this.handleSettingsSelect}
/>
</div>
- <div>
- {isExternalLink &&
+ {isExternalLink && (
+ <div>
<TextInput
title=""
placeholder='http://'
name="external_link_url"
data={temporaryTile.settings}
- onChange={this.handleSettingsChange.bind(this)}
+ onChange={this.handleSettingsChange}
autoComplete="off"
/>
- }
- </div>
+ </div>
+ )}
+ {(temporaryTile.target_page_id === OPEN_POPUP_LINK || temporaryTile.target_page_id === CLOSE_POPUP_LINK) && (
+ <div className='row single'>
+ <Select
+ title="Popup"
+ name='target_popup'
+ selected={temporaryTile.settings.target_popup || NO_POPUP}
+ options={popupList}
+ onChange={this.handleSettingsSelect}
+ />
+ </div>
+ )}
+ </div>
+ )
+ }
+
+ renderAudioForm() {
+ const { temporaryTile } = this.props
+ return (
+ <div>
+ <Checkbox
+ label="Sound effects"
+ name="has_audio"
+ className='short'
+ checked={temporaryTile.settings.has_audio}
+ onChange={this.handleSettingsSelect}
+ />
+ {temporaryTile.settings.has_audio && (
+ <div>
+ <div className='row single'>
+ <AudioSelect
+ title="On click"
+ name="audio_on_click_id"
+ selected={temporaryTile.settings.audio_on_click_id}
+ onChange={this.handleSettingsSelect}
+ />
+ </div>
+
+ {!!temporaryTile.settings.audio_on_click_id && (
+ <Checkbox
+ label="Navigate when audio finishes"
+ name="navigate_when_audio_finishes"
+ className='short'
+ checked={temporaryTile.settings.navigate_when_audio_finishes}
+ onChange={this.handleSettingsSelect}
+ autoComplete="off"
+ />
+ )}
+
+ <div className='row single'>
+ <AudioSelect
+ title="On hover"
+ name="audio_on_hover_id"
+ selected={temporaryTile.settings.audio_on_hover_id}
+ onChange={this.handleSettingsSelect}
+ />
+ </div>
+ </div>
+ )}
</div>
)
}
@@ -631,11 +887,20 @@ class TileForm extends Component {
const { temporaryTile } = this.props
return (
<div>
+ <div className='row single'>
+ <Select
+ name='units'
+ selected={temporaryTile.settings.units || 'px'}
+ options={UNITS}
+ title='Units'
+ onChange={this.handleSettingsSelect}
+ />
+ </div>
<Slider
title='Opacity'
name='opacity'
value={temporaryTile.settings.opacity}
- onChange={this.handleSettingsSelect.bind(this)}
+ onChange={this.handleSettingsSelect}
min={0.0}
max={1.0}
step={0.01}
@@ -644,7 +909,7 @@ class TileForm extends Component {
title='Scale'
name='scale'
value={temporaryTile.settings.scale}
- onChange={this.handleSettingsSelect.bind(this)}
+ onChange={this.handleSettingsSelect}
min={0.01}
max={10.0}
step={0.01}
@@ -653,12 +918,58 @@ class TileForm extends Component {
title='Rotation'
name='rotation'
value={temporaryTile.settings.rotation}
- onChange={this.handleSettingsSelect.bind(this)}
+ onChange={this.handleSettingsSelect}
min={-180.0}
max={180.0}
step={1}
type='int'
/>
+ <Checkbox
+ label="Element is a Popup"
+ name="is_popup"
+ className='short'
+ checked={temporaryTile.settings.is_popup}
+ onChange={this.handleSettingsSelect}
+ autoComplete="off"
+ />
+ {temporaryTile.settings.is_popup && (
+ <div className='row single_text'>
+ <TextInput
+ title="Popup group"
+ name="popup_group"
+ data={temporaryTile.settings}
+ onChange={this.handleSettingsChange}
+ autoComplete="off"
+ />
+ </div>
+ )}
+ <Checkbox
+ label="Wait to appear"
+ name="wait_to_appear"
+ className='short'
+ checked={temporaryTile.settings.wait_to_appear}
+ onChange={this.handleSettingsSelect}
+ autoComplete="off"
+ />
+ {temporaryTile.settings.wait_to_appear && (
+ <div className='row single_text'>
+ <TextInput
+ title="Appear after"
+ name="appear_after"
+ data={temporaryTile.settings}
+ onChange={this.handleSettingsChange}
+ autoComplete="off"
+ />
+ </div>
+ )}
+ <Checkbox
+ label="Hide on click"
+ name="hide_on_click"
+ className='short'
+ checked={temporaryTile.settings.hide_on_click}
+ onChange={this.handleSettingsSelect}
+ autoComplete="off"
+ />
</div>
)
}
@@ -672,6 +983,7 @@ const mapStateToProps = state => ({
})
const mapDispatchToProps = dispatch => ({
+ pageActions: bindActionCreators({ ...pageActions }, dispatch),
tileActions: bindActionCreators({ ...tileActions }, dispatch),
})
diff --git a/frontend/app/views/tile/components/tile.handle.js b/frontend/app/views/tile/components/tile.handle.js
new file mode 100644
index 0000000..03b9f88
--- /dev/null
+++ b/frontend/app/views/tile/components/tile.handle.js
@@ -0,0 +1,8 @@
+import React from 'react'
+
+import handles from 'app/views/tile/handles'
+
+export default function TileHandle (props) {
+ const Tile = handles[props.tile.type]
+ return <Tile {...props} />
+}
diff --git a/frontend/app/views/page/components/tile.list.js b/frontend/app/views/tile/components/tile.list.js
index c455489..127ca52 100644
--- a/frontend/app/views/page/components/tile.list.js
+++ b/frontend/app/views/tile/components/tile.list.js
@@ -114,6 +114,10 @@ const TileListLink = ({ tile, pageTitles }) => (
{'Link: '}
{tile.target_page_id === -1
? 'External'
+ : tile.target_page_id === -2
+ ? 'Open popup'
+ : tile.target_page_id === -3
+ ? 'Close popup'
: !tile.target_page_id
? 'No link specified!'
: tile.target_page_id in pageTitles
@@ -123,6 +127,14 @@ const TileListLink = ({ tile, pageTitles }) => (
</div>
)
+const TileListVideo = ({ tile }) => {
+ return (
+ <div className='row' data-id={tile.id}>
+ <span className='snippet'>{"Vido: "}{tile.settings.url}</span>
+ </div>
+ )
+}
+
const TileListMisc = ({ tile }) => (
<div className='row' data-id={tile.id}>
<span className='snippet'>{"Tile: "}{tile.type}</span>
diff --git a/frontend/app/views/page/components/tile.new.js b/frontend/app/views/tile/components/tile.new.js
index b491fdd..e0f61a6 100644
--- a/frontend/app/views/page/components/tile.new.js
+++ b/frontend/app/views/tile/components/tile.new.js
@@ -21,6 +21,7 @@ class TileNew extends Component {
// history.push('/' + graph.path + '/' + res.res.path)
// }
this.props.pageActions.hideAddTileForm()
+ this.props.pageActions.loadPopups()
this.props.tileActions.clearTemporaryTile()
})
.catch(err => {
diff --git a/frontend/app/views/tile/handles/index.js b/frontend/app/views/tile/handles/index.js
new file mode 100644
index 0000000..8aaeb06
--- /dev/null
+++ b/frontend/app/views/tile/handles/index.js
@@ -0,0 +1,14 @@
+
+import TileImage from './tile.image'
+import TileVideo from './tile.video'
+import TileLink from './tile.link'
+import TileText from './tile.text'
+import TileScript from './tile.script'
+
+export default {
+ image: TileImage,
+ video: TileVideo,
+ link: TileLink,
+ text: TileText,
+ script: TileScript,
+}
diff --git a/frontend/app/views/tile/handles/tile.image.js b/frontend/app/views/tile/handles/tile.image.js
new file mode 100644
index 0000000..9ab5616
--- /dev/null
+++ b/frontend/app/views/tile/handles/tile.image.js
@@ -0,0 +1,56 @@
+import React from 'react'
+import { generateTransform } from 'app/views/tile/tile.utils'
+
+export default function TileImage({ tile, box, bounds, videoBounds, viewing, onMouseDown, onDoubleClick }) {
+ // console.log(tile)
+ const style = {
+ transform: generateTransform(tile, box, bounds, videoBounds),
+ opacity: tile.settings.opacity,
+ }
+ // console.log(generateTransform(tile))
+ let content
+ let className = ['tile', tile.type].join(' ')
+ if (tile.target_page_id || (viewing && tile.href)) {
+ if (viewing || tile.settings.cursor !== 'unclickable') {
+ className += ' ' + (tile.settings.cursor || 'hand_up')
+ }
+ }
+
+ if (!tile.settings.url) {
+ return null
+ }
+ if (tile.settings.is_tiled) {
+ style.backgroundImage = 'url(' + tile.settings.url + ')'
+ style.backgroundPosition = tile.settings.align.replace('_', ' ')
+ switch (tile.settings.tile_style) {
+ default:
+ case 'tile':
+ break
+ case 'cover':
+ style.backgroundSize = 'cover'
+ break
+ case 'contain':
+ style.backgroundSize = 'contain'
+ break
+ case 'contain no-repeat':
+ style.backgroundSize = 'contain'
+ style.backgroundRepeat = 'no-repeat'
+ break
+ }
+ className += ' is_tiled'
+ } else {
+ className += ' ' + tile.settings.align
+ content = <img src={tile.settings.url} />
+ }
+
+ return (
+ <div
+ className={className}
+ onMouseDown={onMouseDown}
+ onDoubleClick={onDoubleClick}
+ style={style}
+ >
+ {content}
+ </div>
+ )
+}
diff --git a/frontend/app/views/tile/handles/tile.link.js b/frontend/app/views/tile/handles/tile.link.js
new file mode 100644
index 0000000..4dd4fd4
--- /dev/null
+++ b/frontend/app/views/tile/handles/tile.link.js
@@ -0,0 +1,33 @@
+import React from 'react'
+import { generateTransform, unitsDimension } from 'app/views/tile/tile.utils'
+
+export default function TileScript({ tile, box, bounds, videoBounds, viewing, onMouseDown, onDoubleClick }) {
+ // console.log(tile)
+ const style = {
+ transform: generateTransform(tile, box, bounds, videoBounds),
+ opacity: tile.settings.opacity,
+ }
+ // console.log(generateTransform(tile))
+ let className = ['tile', tile.type].join(' ')
+ if (tile.target_page_id || (viewing && tile.href)) {
+ if (viewing || tile.settings.cursor !== 'unclickable') {
+ className += ' ' + (tile.settings.cursor || 'hand_up')
+ }
+ }
+
+ let content = ""
+ className += ' ' + tile.settings.align
+ style.width = unitsDimension(tile, 'width', bounds, videoBounds)
+ style.height = unitsDimension(tile, 'height', bounds, videoBounds)
+
+ return (
+ <div
+ className={className}
+ onMouseDown={onMouseDown}
+ onDoubleClick={onDoubleClick}
+ style={style}
+ >
+ {content}
+ </div>
+ )
+}
diff --git a/frontend/app/views/tile/handles/tile.script.js b/frontend/app/views/tile/handles/tile.script.js
new file mode 100644
index 0000000..e844adf
--- /dev/null
+++ b/frontend/app/views/tile/handles/tile.script.js
@@ -0,0 +1,36 @@
+import React, { Component } from 'react'
+import { generateTransform } from 'app/views/tile/tile.utils'
+
+export default class TileScript extends Component {
+ componentDidMount(){
+ const { viewing, tile } = this.props
+ if (viewing) {
+ eval(tile.settings.content)
+ }
+ }
+ render() {
+ if (viewing) {
+ return <div style={{ display: 'none' }} />
+ }
+
+ const { tile, box, bounds, videoBounds, viewing, onMouseDown, onDoubleClick } = this.props
+ // console.log(tile)
+ const style = {
+ transform: generateTransform(tile, box, bounds, videoBounds),
+ opacity: tile.settings.opacity,
+ }
+ // console.log(generateTransform(tile))
+ let className = ['tile', tile.type, 'hand_up'].join(' ')
+
+ return (
+ <div
+ className={className}
+ onMouseDown={onMouseDown}
+ onDoubleClick={onDoubleClick}
+ style={style}
+ >
+ {"SCRIPT"}
+ </div>
+ )
+ }
+}
diff --git a/frontend/app/views/tile/handles/tile.text.js b/frontend/app/views/tile/handles/tile.text.js
new file mode 100644
index 0000000..2fd63b2
--- /dev/null
+++ b/frontend/app/views/tile/handles/tile.text.js
@@ -0,0 +1,44 @@
+import React from 'react'
+import { generateTransform, unitsDimension } from 'app/views/tile/tile.utils'
+
+export default function TileScript({ tile, box, bounds, videoBounds, viewing, onMouseDown, onDoubleClick }) {
+ // console.log(tile)
+ const style = {
+ transform: generateTransform(tile, box, bounds, videoBounds),
+ opacity: tile.settings.opacity,
+ }
+ // console.log(generateTransform(tile))
+ let className = ['tile', tile.type].join(' ')
+ if (tile.target_page_id || (viewing && tile.href)) {
+ if (viewing || tile.settings.cursor !== 'unclickable') {
+ className += ' ' + (tile.settings.cursor || 'hand_up')
+ }
+ }
+
+ if (!tile.settings.content) {
+ return null
+ }
+
+ let content = <span dangerouslySetInnerHTML={{ __html: tile.settings.content }} />
+ className += ' ' + tile.settings.align
+ style.width = unitsDimension(tile, 'width', bounds, videoBounds)
+ style.height = unitsDimension(tile, 'height', bounds, videoBounds)
+ style.fontFamily = tile.settings.font_family
+ style.fontSize = tile.settings.font_size + 'px'
+ style.lineHeight = 1.5
+ style.fontWeight = (tile.settings.font_style || "").indexOf('bold') !== -1 ? 'bold' : 'normal'
+ style.fontStyle = (tile.settings.font_style || "").indexOf('italic') !== -1 ? 'italic' : 'normal'
+ style.backgroundColor = tile.settings.background_color || 'transparent'
+ style.color = tile.settings.font_color || '#dddddd!important'
+
+ return (
+ <div
+ className={className}
+ onMouseDown={onMouseDown}
+ onDoubleClick={onDoubleClick}
+ style={style}
+ >
+ {content}
+ </div>
+ )
+}
diff --git a/frontend/app/views/tile/handles/tile.video.js b/frontend/app/views/tile/handles/tile.video.js
new file mode 100644
index 0000000..3166848
--- /dev/null
+++ b/frontend/app/views/tile/handles/tile.video.js
@@ -0,0 +1,108 @@
+import React, { Component } from 'react'
+
+import { generateTransform, generateVideoStyle } from 'app/views/tile/tile.utils'
+import { timestampToSeconds } from 'app/utils'
+
+export default class TileVideo extends Component {
+ constructor(props) {
+ super(props)
+ this.videoRef = React.createRef()
+ this.handleTimeUpdate = this.handleTimeUpdate.bind(this)
+ this.handleEnded = this.handleEnded.bind(this)
+ }
+
+ componentDidMount() {
+ this.bind()
+ }
+
+ componentDidUpdate() {
+ this.unbind()
+ this.bind()
+ }
+
+ componentWillUnmount() {
+ this.unbind()
+ }
+
+ bind() {
+ if (!this.videoRef.current) return
+ this.el = this.videoRef.current
+ this.el.addEventListener('ended', this.handleEnded)
+ this.el.addEventListener('timeupdate', this.handleTimeUpdate)
+ const muted = this.props.viewing
+ ? this.props.tile.settings.muted
+ : true
+ const volume = muted
+ ? 0.0
+ : ('volume' in this.props.tile.settings)
+ ? this.props.tile.settings.volume
+ : 1.0
+ this.el.volume = volume
+ }
+
+ unbind() {
+ if (!this.el) return
+ this.el.removeEventListener('timeupdate', this.handleTimeUpdate)
+ this.el.removeEventListener('ended', this.handleEnded)
+ }
+
+ handleTimeUpdate() {
+ if (this.props.tile.settings.loop && this.props.tile.settings.loop_section) {
+ const loop_start = timestampToSeconds(this.props.tile.settings.loop_start) || 0
+ const loop_end = timestampToSeconds(this.props.tile.settings.loop_end) || this.videoRef.current.duration
+ if (this.videoRef.current.currentTime > loop_end) {
+ this.videoRef.current.currentTime = loop_start
+ }
+ }
+ }
+
+ handleEnded() {
+ this.props.onPlaybackEnded(this.props.tile)
+ if (this.props.tile.settings.loop && this.props.tile.settings.loop_section) {
+ const loop_start = timestampToSeconds(this.props.tile.settings.loop_start) || 0
+ this.videoRef.current.currentTime = loop_start
+ }
+ }
+
+ render() {
+ let { tile, box, bounds, videoBounds, viewing, onMouseDown, onDoubleClick } = this.props
+ // console.log(tile)
+ const style = {
+ transform: generateTransform(tile, box, bounds, videoBounds),
+ opacity: tile.settings.opacity,
+ }
+ let className = ['tile', tile.type].join(' ')
+ if (tile.target_page_id || (viewing && tile.href)) {
+ if (viewing || tile.settings.cursor !== 'unclickable') {
+ className += ' ' + (tile.settings.cursor || 'hand_up')
+ }
+ }
+ // console.log(tile.settings)
+ if (!tile.settings.url) {
+ return null
+ }
+ className += ' ' + tile.settings.align
+ const muted = viewing
+ ? tile.settings.muted
+ : true
+ return (
+ <div
+ className={className}
+ onMouseDown={onMouseDown}
+ onDoubleClick={onDoubleClick}
+ style={style}
+ >
+ <video
+ ref={this.videoRef}
+ src={tile.settings.url}
+ autoPlay={true}
+ controls={false}
+ disablePictureInPicture={true}
+ loop={tile.settings.loop}
+ muted={muted}
+ style={generateVideoStyle(tile, bounds)}
+ />
+ </div>
+ )
+ }
+}
diff --git a/frontend/app/views/tile/tile.utils.js b/frontend/app/views/tile/tile.utils.js
new file mode 100644
index 0000000..46d7764
--- /dev/null
+++ b/frontend/app/views/tile/tile.utils.js
@@ -0,0 +1,80 @@
+export const generateTransform = (tile, box, bounds, videoBounds) => {
+ let { x, y, align, rotation, scale, units, is_tiled } = tile.settings
+ if (is_tiled) {
+ return 'translateZ(0)'
+ }
+ if (box) {
+ x += box.dx
+ y += box.dy
+ }
+ units = units || 'px'
+ const [yalign, xalign] = align.split('_')
+ let transform = ['translateZ(0)']
+ if (yalign === 'center') {
+ transform.push('translateY(-50%)')
+ }
+ if (xalign === 'center') {
+ transform.push('translateX(-50%)')
+ }
+ // if (x % 2 == 1) x += 0.5
+ // if (y % 2 == 1) y += 0.5
+ const xUnits = units === 'video' ? videoUnits(x, bounds, videoBounds) : x + units
+ const yUnits = units === 'video' ? videoUnits(y, bounds, videoBounds) : y + units
+
+ transform.push('translateX(' + xUnits + ')')
+ transform.push('translateY(' + yUnits + ')')
+ if (scale !== 1) {
+ transform.push('scale(' + scale + ')')
+ }
+ if (rotation !== 0) {
+ transform.push('rotateZ(' + rotation + 'deg)')
+ }
+ return transform.join(' ')
+}
+
+export const generateVideoStyle = (tile, bounds) => {
+ const style = {
+ pointerEvents: "none",
+ }
+ switch (tile.settings.video_style) {
+ case 'normal':
+ style.width = "auto"
+ style.height = "auto"
+ break
+ case 'cover':
+ if (tile.settings.width && (tile.settings.width / tile.settings.height) > (bounds.width / bounds.height)) {
+ style.width = Math.round((tile.settings.width / tile.settings.height) * bounds.height)
+ style.height = bounds.height
+ } else {
+ style.width = bounds.width
+ style.height = Math.round((tile.settings.height / tile.settings.width) * bounds.width)
+ }
+ break
+ case 'contain':
+ if (tile.settings.width && (tile.settings.width / tile.settings.height) > (bounds.width / bounds.height)) {
+ style.width = bounds.width
+ style.height = Math.round((tile.settings.height / tile.settings.width) * bounds.width)
+ } else {
+ style.width = Math.round((tile.settings.width / tile.settings.height) * bounds.height)
+ style.height = bounds.height
+ }
+ break
+ }
+ return style
+}
+
+export const unitsDimension = (tile, dimension, bounds, videoBounds) => {
+ const value = tile.settings[dimension]
+ if (!value) return "auto"
+ if (tile.settings.units) {
+ if (tile.settings.units === 'video') {
+ return videoUnits(value, bounds, videoBounds)
+ }
+ return value + tile.settings.units
+ }
+ return value + "px"
+}
+
+export const videoUnits = (value, bounds, videoBounds) => (
+ Math.round(value / videoBounds.width * bounds.width) + 'px'
+) \ No newline at end of file
diff --git a/frontend/app/site/actions.js b/frontend/site/actions.js
index e672028..dea882c 100644
--- a/frontend/app/site/actions.js
+++ b/frontend/site/actions.js
@@ -1,9 +1,9 @@
import { bindActionCreators } from 'redux'
// import { actions as crudActions } from './api'
-import * as siteActions from './site/site.actions'
+import * as siteActions from 'site/site/site.actions'
-import { store } from './store'
+import { store } from 'site/store'
export default
// Object.keys(crudActions)
diff --git a/frontend/app/site/app.js b/frontend/site/app.js
index 389e5b5..098bd44 100644
--- a/frontend/app/site/app.js
+++ b/frontend/site/app.js
@@ -2,19 +2,19 @@ import React, { Component } from 'react'
import { ConnectedRouter } from 'connected-react-router'
import { Route } from 'react-router'
-import ViewerContainer from './viewer/viewer.container'
-import actions from './actions'
+import ViewerContainer from 'site/viewer/viewer.container'
+import actions from 'site/actions'
export default class App extends Component {
componentDidMount() {
const path_partz = window.location.pathname.split('/')
const graph_name = path_partz[1]
- let path_name = null
- if (path_partz.length > 2) {
- path_name = path_partz[2]
- }
+ // let path_name = null
+ // if (path_partz.length > 2) {
+ // path_name = path_partz[2]
+ // }
// console.log('loading', graph_name, path_name)
- actions.site.loadSite(graph_name, path_name)
+ actions.site.loadSite(graph_name)
}
render() {
diff --git a/frontend/site/audio/audio.player.js b/frontend/site/audio/audio.player.js
new file mode 100644
index 0000000..17edeee
--- /dev/null
+++ b/frontend/site/audio/audio.player.js
@@ -0,0 +1,141 @@
+import { history } from 'site/store'
+
+export default class AudioPlayer {
+ files = {}
+ players = {}
+ current_background_id = 0
+
+ constructor() {
+ this.done = this.done.bind(this)
+ }
+
+ load(graph) {
+ this.files = graph.uploads
+ .filter(upload => upload.tag === 'audio')
+ .reduce((accumulator, item) => {
+ accumulator[item.id] = item
+ return accumulator
+ }, {})
+ }
+
+ has(id) {
+ return (
+ (id > 0) &&
+ (id in this.files)
+ )
+ }
+
+ done(id) {
+ // console.log('remove', id)
+ delete this.players[id]
+ }
+
+ playPage(page) {
+ const { background_audio_id, restart_audio } = page.settings
+ // console.log('playPage', page)
+ if (
+ this.current_background_id
+ && this.current_background_id !== background_audio_id
+ && this.current_background_id in this.players
+ ) {
+ this.players[this.current_background_id].stop()
+ }
+ if (this.has(background_audio_id)) {
+ this.current_background_id = background_audio_id
+ this.playFile({
+ id: background_audio_id,
+ type: 'background',
+ restart: !!restart_audio,
+ })
+ }
+ }
+
+ playTile({ tile, type }) {
+ let id = type === 'click'
+ ? tile.settings.audio_on_click_id
+ : type === 'hover'
+ ? tile.settings.audio_on_hover_id
+ : null
+ if (this.has(id)) {
+ this.playFile({ id, tile, type })
+ }
+ }
+
+ playFile({ id, tile, type, restart, loop }) {
+ const item = this.files[id]
+ if (id in this.players) {
+ if (restart) {
+ this.players[id].restart()
+ }
+ if (tile && !this.players[id].tile) {
+ this.players[id].tile = tile
+ this.players[id].type = type
+ }
+ return this.players[id]
+ } else {
+ this.players[id] = new Player({
+ item,
+ tile,
+ type,
+ done: this.done
+ })
+ this.players[id].play()
+ return this.players[id]
+ }
+ }
+}
+
+class Player {
+ constructor({ item, tile, type, done }) {
+ this.item = item
+ this.tile = tile
+ this.type = type
+ this.done = done
+ this.audio = document.createElement('audio')
+ this.handleEnded = this.handleEnded.bind(this)
+ this.handleError = this.handleError.bind(this)
+ this.release = this.release.bind(this)
+ this.audio.addEventListener('ended', this.handleEnded)
+ this.audio.addEventListener('error', this.handleError)
+ this.audio.src = item.url
+ }
+
+ release() {
+ if (this.type === 'click' && this.tile && this.tile.settings.navigate_when_audio_finishes) {
+ history.push(this.tile.href)
+ }
+ this.audio.removeEventListener('ended', this.handleEnded)
+ this.audio.removeEventListener('error', this.handleError)
+ this.done(this.item.id)
+ this.item = null
+ this.done = null
+ this.audio = null
+ }
+
+ handleError(error) {
+ console.error(error)
+ this.release()
+ }
+
+ handleEnded() {
+ if (this.type === 'background') {
+ this.restart()
+ } else {
+ this.release()
+ }
+ }
+
+ play() {
+ this.audio.play()
+ }
+
+ restart() {
+ this.audio.currentTime = 0
+ this.audio.play()
+ }
+
+ stop() {
+ this.audio.pause()
+ this.release()
+ }
+}
diff --git a/frontend/site/audio/audio.reducer.js b/frontend/site/audio/audio.reducer.js
new file mode 100644
index 0000000..f0bf0e9
--- /dev/null
+++ b/frontend/site/audio/audio.reducer.js
@@ -0,0 +1,18 @@
+import AudioPlayer from 'site/audio/audio.player'
+import * as types from 'site/types'
+
+const initialState = {
+ player: new AudioPlayer(),
+}
+
+export default function audioReducer(state = initialState, action) {
+ // console.log(action.type, action)
+ switch (action.type) {
+ case types.site.loaded:
+ state.player.load(action.data.graph)
+ return state
+
+ default:
+ return state
+ }
+}
diff --git a/frontend/app/site/index.js b/frontend/site/index.js
index 6f1a0a5..337d362 100644
--- a/frontend/app/site/index.js
+++ b/frontend/site/index.js
@@ -2,9 +2,11 @@ import React from 'react'
import ReactDOM from 'react-dom'
import { Provider } from 'react-redux'
-import App from './app'
+import './site.css'
-import { store, history } from './store'
+import App from 'site/app'
+
+import { store, history } from 'site/store'
const container = document.createElement('div')
container.classList.add('container')
diff --git a/frontend/site/site.css b/frontend/site/site.css
new file mode 100644
index 0000000..0597514
--- /dev/null
+++ b/frontend/site/site.css
@@ -0,0 +1,20 @@
+.roadblock {
+ position: fixed;
+ top: 0; left: 0;
+ display: flex;
+ width: 100vw;
+ height: 100vh;
+ justify-content: center;
+ align-items: center;
+ cursor: pointer;
+}
+.roadblock div {
+ display: inline-block;
+ text-align: center;
+}
+.roadblock h2 {
+ font-style: italic;
+}
+.roadblock button {
+ padding: 0.5rem;
+}
diff --git a/frontend/app/site/site/site.actions.js b/frontend/site/site/site.actions.js
index 79e4573..aab68e8 100644
--- a/frontend/app/site/site/site.actions.js
+++ b/frontend/site/site/site.actions.js
@@ -1,4 +1,4 @@
-import * as types from '../types'
+import * as types from 'site/types'
import { api } from 'app/utils'
export const setSiteTitle = title => dispatch => {
@@ -6,6 +6,10 @@ export const setSiteTitle = title => dispatch => {
dispatch({ type: types.site.set_site_title, payload: title })
}
-export const loadSite = (graph_name, path_name) => dispatch => (
- api(dispatch, types.site, 'site', '/' + graph_name + '/index.json')
+export const loadSite = graph_name => dispatch => (
+ api(dispatch, types.site, 'site', '/' + graph_name + '/index.json?t=' + (Date.now() / 3600000))
)
+
+export const interact = () => dispatch => {
+ dispatch({ type: types.site.interact })
+} \ No newline at end of file
diff --git a/frontend/app/site/site/site.reducer.js b/frontend/site/site/site.reducer.js
index 85c3486..9763e48 100644
--- a/frontend/app/site/site/site.reducer.js
+++ b/frontend/site/site/site.reducer.js
@@ -1,14 +1,15 @@
-import * as types from '../types'
+import * as types from 'site/types'
const initialState = {
siteTitle: 'swimmer',
+ interactive: false,
graph: {
loading: true,
}
}
export default function siteReducer(state = initialState, action) {
- console.log(action.type, action)
+ // console.log(action.type, action)
switch (action.type) {
case types.site.set_site_title:
return {
@@ -22,6 +23,12 @@ export default function siteReducer(state = initialState, action) {
graph: action.data.graph,
}
+ case types.site.interact:
+ return {
+ ...state,
+ interactive: true,
+ }
+
case '@@router/LOCATION_CHANGE':
return {
...state,
diff --git a/frontend/app/site/store.js b/frontend/site/store.js
index a228e2b..60c3116 100644
--- a/frontend/app/site/store.js
+++ b/frontend/site/store.js
@@ -3,12 +3,14 @@ import { connectRouter, routerMiddleware } from 'connected-react-router'
import { createBrowserHistory } from 'history'
import thunk from 'redux-thunk'
-import siteReducer from './site/site.reducer'
+import siteReducer from 'site/site/site.reducer'
+import audioReducer from 'site/audio/audio.reducer'
const createRootReducer = history => (
combineReducers({
auth: (state = {}) => state,
router: connectRouter(history),
+ audio: audioReducer,
site: siteReducer,
})
)
diff --git a/frontend/app/site/types.js b/frontend/site/types.js
index 23bed98..4ab897f 100644
--- a/frontend/app/site/types.js
+++ b/frontend/site/types.js
@@ -1,7 +1,7 @@
-import { with_type, crud_type } from 'app/api/crud.types'
+import { with_type } from 'app/api/crud.types'
export const site = with_type('site', [
- 'set_site_title', 'loading', 'loaded', 'error',
+ 'set_site_title', 'loading', 'loaded', 'error', 'interact'
])
export const system = with_type('system', [
diff --git a/frontend/site/viewer/viewer.container.js b/frontend/site/viewer/viewer.container.js
new file mode 100644
index 0000000..9bf4442
--- /dev/null
+++ b/frontend/site/viewer/viewer.container.js
@@ -0,0 +1,223 @@
+import React, { Component } from 'react'
+import { Route } from 'react-router-dom'
+import { bindActionCreators } from 'redux'
+import { connect } from 'react-redux'
+
+import { history } from 'site/store'
+import actions from 'site/actions'
+import { Loader } from 'app/common/loader.component'
+import TileHandle from 'app/views/tile/components/tile.handle'
+
+import 'app/views/page/page.css'
+
+class ViewerContainer extends Component {
+ state = {
+ page: {},
+ bounds: { width: window.innerWidth, height: window.innerHeight },
+ roadblock: false,
+ popups: {},
+ hidden: {},
+ time: 0,
+ maxDeferTime: 0,
+ }
+
+ constructor(props) {
+ super(props)
+ this.pageRef = React.createRef()
+ this.handleMouseDown = this.handleMouseDown.bind(this)
+ this.handleResize = this.handleResize.bind(this)
+ this.removeRoadblock = this.removeRoadblock.bind(this)
+ this.updateTimer = this.updateTimer.bind(this)
+ window.addEventListener('resize', this.handleResize)
+ }
+
+ componentDidUpdate(prevProps) {
+ // console.log('didUpdate', this.props.graph !== prevProps.graph, this.props.location.pathname !== prevProps.location.pathname)
+ if (this.props.graph !== prevProps.graph || this.props.location.pathname !== prevProps.location.pathname) {
+ this.load()
+ }
+ }
+
+ componentWillUnmount() {
+ window.removeEventListener('resize', this.handleResize)
+ actions.site.interact()
+ }
+
+ load() {
+ const { graph_name, page_name } = this.props.match.params
+ const page_path = ["", graph_name, page_name].join('/')
+ const { pages, home_page } = this.props.graph
+ const page = pages[page_path] || pages[home_page]
+ if (!this.props.interactive && hasAutoplay(page)) {
+ this.setState({ page, popups: {}, hidden: {}, roadblock: true })
+ } else {
+ this.setState({ page, popups: {}, hidden: {}, roadblock: false })
+ actions.site.interact()
+ this.props.audio.player.playPage(page)
+ this.resetTimer(page)
+ }
+ }
+
+ resetTimer(page) {
+ clearTimeout(this.timeout)
+ const maxDeferTime = page.tiles.reduce((max_time, tile) => Math.max(tile.settings.appear_after || 0, max_time), 0)
+ if (maxDeferTime) {
+ this.setState({ time: 0, maxDeferTime })
+ this.timeout = setTimeout(this.updateTimer, 500)
+ }
+ }
+
+ updateTimer() {
+ clearTimeout(this.timeout)
+ this.setState({ time: this.state.time + 0.500 })
+ if (this.state.time < this.state.maxDeferTime) {
+ this.timeout = setTimeout(this.updateTimer, 500)
+ }
+ }
+
+ handleResize() {
+ this.setState({
+ bounds: {
+ width: window.innerWidth,
+ height: window.innerHeight,
+ }
+ })
+ }
+
+ handleMouseDown(e, tile) {
+ if (tile.href) {
+ if (tile.href.indexOf('http') === 0) {
+ window.location.href = tile.href
+ return
+ }
+ else if (tile.href === '__open_popup') {
+ this.setState({
+ popups: {
+ ...this.state.popups,
+ [tile.settings.target_popup]: true,
+ },
+ })
+ }
+ else if (tile.href === '__close_popup') {
+ this.setState({
+ popups: {
+ ...this.state.popups,
+ [tile.settings.target_popup]: false,
+ },
+ })
+ }
+ else if (!tile.settings.navigate_when_audio_finishes) {
+ history.push(tile.href)
+ }
+ }
+ if (tile.settings.audio_on_click_id > 0) {
+ this.props.audio.player.playTile({
+ type: "click",
+ tile,
+ })
+ }
+ if (tile.settings.hide_on_click) {
+ this.setState({
+ hidden: {
+ ...this.state.hidden,
+ [tile.id]: true,
+ }
+ })
+ }
+ }
+
+ handlePlaybackEnded(tile) {
+ if (tile.href && tile.settings.autoadvance) {
+ history.push(tile.href)
+ }
+ }
+
+ render() {
+ const { page, audio, popups, hidden, time } = this.state
+ if (this.state.roadblock) {
+ return this.renderRoadblock()
+ }
+ if (this.props.graph.loading || !page.id) {
+ return (
+ <div>
+ <div className='body'>
+ <div className='page loading'>
+ <Loader />
+ </div>
+ </div>
+ </div>
+ )
+ }
+ const { settings } = page
+ const pageStyle = { backgroundColor: settings ? settings.background_color : '#000000' }
+ const videoBounds = (page.tiles.length && page.tiles[0].type === 'video') ? {
+ width: page.tiles[0].settings.width,
+ height: page.tiles[0].settings.height,
+ } : this.state.bounds
+ // console.log(page)
+ return (
+ <div className='body'>
+ <div className='page' ref={this.pageRef} style={pageStyle}>
+ {page.tiles.map(tile => {
+ if (tile.settings.is_popup && !popups[tile.settings.popup_group]) return
+ if (tile.settings.appear_after && time < tile.settings.appear_after) return
+ if (tile.settings.hide_on_click && hidden[tile.id]) return
+ return (
+ <TileHandle
+ viewing
+ key={tile.id}
+ tile={tile}
+ audio={audio}
+ bounds={this.state.bounds}
+ videoBounds={videoBounds}
+ onMouseDown={e => this.handleMouseDown(e, tile)}
+ onPlaybackEnded={e => this.handlePlaybackEnded(e, tile)}
+ onDoubleClick={e => {}}
+ />
+ )
+ })}
+ </div>
+ </div>
+ )
+ }
+
+ removeRoadblock() {
+ console.log("remove roadblock")
+ actions.site.interact()
+ this.setState({ roadblock: false })
+ this.props.audio.player.playPage(this.state.page)
+ this.resetTimer(this.state.page)
+ }
+
+ renderRoadblock() {
+ const { title } = this.props.graph
+ return (
+ <div className='roadblock' onClick={this.removeRoadblock}>
+ <div>
+ <h2>{title}</h2>
+ <button>Enter</button>
+ </div>
+ </div>
+ )
+ }
+}
+
+const hasAutoplay = page => {
+ const hasAutoplayVideo = page.tiles.some(tile => {
+ return tile.type === 'video' && !tile.settings.muted
+ })
+ const hasAutoplayAudio = page.settings.background_audio_id > 0
+ return hasAutoplayAudio || hasAutoplayVideo
+}
+
+const mapStateToProps = state => ({
+ site: state.site,
+ audio: state.audio,
+ graph: state.site.graph,
+ interactive: state.site.interactive,
+})
+
+const mapDispatchToProps = dispatch => ({
+})
+
+export default connect(mapStateToProps, mapDispatchToProps)(ViewerContainer)