From 8792e9fe1c7ab76c35f9a18d866880ba3da2c13e Mon Sep 17 00:00:00 2001 From: Jules Laplace Date: Mon, 15 Mar 2021 19:09:37 +0100 Subject: move frontend site folder. add video support --- README.md | 20 +- cli/app/settings/app_cfg.py | 2 + cli/app/site/export.py | 125 + cli/commands/site/export.py | 123 +- frontend/app/site/actions.js | 19 - frontend/app/site/app.js | 35 - frontend/app/site/index.js | 17 - frontend/app/site/site/site.actions.js | 11 - frontend/app/site/site/site.reducer.js | 36 - frontend/app/site/store.js | 37 - frontend/app/site/types.js | 11 - frontend/app/site/viewer/viewer.container.js | 96 - frontend/app/utils/index.js | 29 + frontend/app/views/page/components/page.editor.js | 1 + frontend/app/views/page/components/tile.form.js | 103 +- frontend/app/views/page/components/tile.handle.js | 52 + frontend/app/views/page/cursors.css | 3 + frontend/app/views/page/page.css | 3 + frontend/site/actions.js | 19 + frontend/site/app.js | 35 + frontend/site/index.js | 17 + frontend/site/site/site.actions.js | 11 + frontend/site/site/site.reducer.js | 36 + frontend/site/store.js | 37 + frontend/site/types.js | 11 + frontend/site/viewer/viewer.container.js | 96 + package-lock.json | 83 +- package.json | 6 +- static/media | 1 + webpack.config.site.js | 54 +- yarn.lock | 7012 +++++++++++++++++++++ 31 files changed, 7694 insertions(+), 447 deletions(-) create mode 100644 cli/app/site/export.py delete mode 100644 frontend/app/site/actions.js delete mode 100644 frontend/app/site/app.js delete mode 100644 frontend/app/site/index.js delete mode 100644 frontend/app/site/site/site.actions.js delete mode 100644 frontend/app/site/site/site.reducer.js delete mode 100644 frontend/app/site/store.js delete mode 100644 frontend/app/site/types.js delete mode 100644 frontend/app/site/viewer/viewer.container.js create mode 100644 frontend/site/actions.js create mode 100644 frontend/site/app.js create mode 100644 frontend/site/index.js create mode 100644 frontend/site/site/site.actions.js create mode 100644 frontend/site/site/site.reducer.js create mode 100644 frontend/site/store.js create mode 100644 frontend/site/types.js create mode 100644 frontend/site/viewer/viewer.container.js create mode 120000 static/media create mode 100644 yarn.lock diff --git a/README.md b/README.md index 0daf5db..dcfa219 100644 --- a/README.md +++ b/README.md @@ -1,19 +1,19 @@ -# swimmer +# Swimmer Network-based tile tool. -## installation +## Installation First, install miniconda and node. ``` conda create env -f environment.yml -npm install +yarn install ``` (this should work on Linux as well but let me know if it doesn't) -## running the site +## Running the site Before running the commands, enter the client directory, load the Conda environment, and make sure the database is current: @@ -26,19 +26,18 @@ conda activate swimmer Then build the frontend and run the Flask server: ``` -npm run build:dev -./cli.py flask run +yarn build:dev +yarn server ``` The server will be running on http://0.0.0.0:5000/ - -## development +## Development Monitor the Javascript for changes (run in another window): ``` -npm run watch +yarn watch ``` Generate a new migration if you've modified the database: @@ -48,10 +47,9 @@ Generate a new migration if you've modified the database: ./cli.py db upgrade head ``` -## building the site +## Building the site ``` npm run build:production ./cli.py site export --graph swimmer ``` - diff --git a/cli/app/settings/app_cfg.py b/cli/app/settings/app_cfg.py index 5fc4982..3865abd 100644 --- a/cli/app/settings/app_cfg.py +++ b/cli/app/settings/app_cfg.py @@ -63,6 +63,8 @@ DIR_STATIC = join(DIR_APP, 'static') HASH_TREE_DEPTH = 3 # for sha256 subdirs HASH_BRANCH_SIZE = 3 # for sha256 subdirs +DIR_PUBLIC_EXPORTS = os.getenv('DIR_PUBLIC_EXPORTS') or DIR_EXPORTS + # ----------------------------------------------------------------------------- # S3 storage diff --git a/cli/app/site/export.py b/cli/app/site/export.py new file mode 100644 index 0000000..d513286 --- /dev/null +++ b/cli/app/site/export.py @@ -0,0 +1,125 @@ +import click + +from app.settings import app_cfg +from app.utils.file_utils import load_text, write_json, write_text +from os.path import join +import os + +from app.sql.common import db, Session, Graph, Page, Tile +from distutils.dir_util import copy_tree + +def export_site(ctx, opt_graph_path, opt_output_dir): + """Export a graph""" + + # ------------------------------------------------ + # generate HTML for index and all pages + + session = Session() + graph = session.query(Graph).filter(Graph.path == opt_graph_path).first() + if graph is None: + print(f"Not a graph: {opt_graph_path}") + return + + # build everything here + graph_dir = os.path.abspath(join(opt_output_dir, graph.path)) + + # load site index + index_html = load_text(join(app_cfg.DIR_STATIC, 'site.html'), split=False) + index_html = index_html.replace('SITE_PATH', '/' + graph.path) + + # write site JSON data + site_data = { 'graph': sanitize_graph(graph.toSiteJSON()) } + write_json(site_data, join(graph_dir, 'index.json'), default=str, minify=False) + + # import custom css + site_css = load_text(join(app_cfg.DIR_STATIC, 'site.css'), split=False) + site_css = site_css.replace('SITE_PATH', '/' + graph.path) + write_text(site_css, join(graph_dir, 'site.css')) + copy_tree(join(app_cfg.DIR_STATIC, 'fonts'), join(graph_dir, 'static/fonts')) + copy_tree(join(app_cfg.DIR_STATIC, 'img'), join(graph_dir, 'static/img')) + + # write index file, redirects to homepage + home_page = site_data['graph']['home_page'] + if home_page is None: + print("Homepage not set! Shift-click a page on the graph to make it the homepage.") + return + write_text(f'', join(graph_dir, 'index.html')) + + index_path = "" + for page in graph.pages: + page_path = f'{graph.path}/{page.path}' + if page.id == graph.home_page_id: + index_path = page_path + print(f'/{page_path} [index]') + else: + print(f'/{page_path}') + write_index(graph, page, index_html, join(graph_dir, page.path, 'index.html')) + + build_javascript(graph_dir) + + print("Site export complete!") + print(f"Graph exported to: {graph_dir}") + +def build_javascript(graph_dir): + print("Building javascript...") + print(f'NODE_ENV=production node ./node_modules/webpack-cli/bin/cli.js --config ./webpack.config.site.js -o {graph_dir}/bundle.js') + os.chdir(app_cfg.DIR_PROJECT_ROOT) + os.system(f'NODE_ENV=production node ./node_modules/webpack-cli/bin/cli.js --config ./webpack.config.site.js -o {graph_dir}/bundle.js') + +def write_index(graph, page, index_html, fp_out): + if page is None: + page_title = graph.title + else: + page_title = page.title + index_html = index_html.replace('BUNDLE_PATH', join('/', graph.path, 'bundle.js')) + index_html = index_html.replace('PAGE_TITLE', page_title) + write_text(index_html, fp_out) + +def sanitize_graph(graph): + page_path_lookup = {} + page_lookup = {} + for page in graph['pages']: + page_path = join('/', graph['path'], page['path']) + if page_path in page_path_lookup: + print(f"/!\\ WARNING! Duplicate found of {page_path}") + else: + page_path_lookup[page['id']] = page_path + for page in graph['pages']: + sanitize_page(page) + if page['id'] == 12: + print(page) + for tile in page['tiles']: + if tile['target_page_id']: + if tile['target_page_id'] == -1: + tile['href'] = tile['settings']['external_link_url'] + elif tile['target_page_id'] > 0: + tile['href'] = page_path_lookup[tile['target_page_id']] + sanitize_tile(tile) + page_path = page_path_lookup[page['id']] + page_lookup[page_path] = page + # print(page_lookup['/asdf/testttt']) + graph['pages'] = page_lookup + graph['home_page'] = page_path_lookup[graph['home_page_id']] + return graph + +def sanitize_page(data): + if 'created_at' in data: + del data['created_at'] + if 'updated_at' in data: + del data['updated_at'] + if 'graph_id' in data: + del data['graph_id'] + +def sanitize_tile(data): + if 'created_at' in data: + del data['created_at'] + if 'updated_at' in data: + del data['updated_at'] + if 'username' in data: + del data['username'] + if 'graph_id' in data: + del data['graph_id'] + if 'page_id' in data: + del data['page_id'] + if 'target_page_id' in data: + del data['target_page_id'] diff --git a/cli/commands/site/export.py b/cli/commands/site/export.py index 0ba6a62..68773e9 100644 --- a/cli/commands/site/export.py +++ b/cli/commands/site/export.py @@ -1,9 +1,7 @@ import click from app.settings import app_cfg -from app.utils.file_utils import load_text, write_json, write_text -from os.path import join -import os +from app.site.export import export_site @click.command('info') @click.option('-g', '--graph', 'opt_graph_path', required=True, @@ -14,121 +12,4 @@ import os def cli(ctx, opt_graph_path, opt_output_dir): """Export a graph""" - # ------------------------------------------------ - # imports - - from app.sql.common import db, Session, Graph, Page, Tile - from distutils.dir_util import copy_tree - - # ------------------------------------------------ - # generate HTML for index and all pages - - session = Session() - graph = session.query(Graph).filter(Graph.path == opt_graph_path).first() - if graph is None: - print(f"Not a graph: {opt_graph_path}") - return - - # build everything here - graph_dir = os.path.abspath(join(opt_output_dir, graph.path)) - - # load site index - index_html = load_text(join(app_cfg.DIR_STATIC, 'site.html'), split=False) - index_html = index_html.replace('SITE_PATH', '/' + graph.path) - - # write site JSON data - site_data = { 'graph': sanitize_graph(graph.toSiteJSON()) } - write_json(site_data, join(graph_dir, 'index.json'), default=str, minify=False) - - # import custom css - site_css = load_text(join(app_cfg.DIR_STATIC, 'site.css'), split=False) - site_css = site_css.replace('SITE_PATH', '/' + graph.path) - write_text(site_css, join(graph_dir, 'site.css')) - copy_tree(join(app_cfg.DIR_STATIC, 'fonts'), join(graph_dir, 'static/fonts')) - copy_tree(join(app_cfg.DIR_STATIC, 'img'), join(graph_dir, 'static/img')) - - # write index file, redirects to homepage - home_page = site_data['graph']['home_page'] - if home_page is None: - print("Homepage not set! Shift-click a page on the graph to make it the homepage.") - return - write_text(f'', join(graph_dir, 'index.html')) - - index_path = "" - for page in graph.pages: - page_path = f'{graph.path}/{page.path}' - if page.id == graph.home_page_id: - index_path = page_path - print(f'/{page_path} [index]') - else: - print(f'/{page_path}') - write_index(graph, page, index_html, join(graph_dir, page.path, 'index.html')) - - # ------------------------------------------------ - # build javascript - - print("Building javascript...") - print(f'NODE_ENV=production node ./node_modules/webpack-cli/bin/cli.js --config ./webpack.config.site.js -o {graph_dir}/bundle.js') - os.chdir(app_cfg.DIR_PROJECT_ROOT) - os.system(f'NODE_ENV=production node ./node_modules/webpack-cli/bin/cli.js --config ./webpack.config.site.js -o {graph_dir}/bundle.js') - - print("Site export complete!") - print(f"Graph exported to: {graph_dir}") - -def write_index(graph, page, index_html, fp_out): - if page is None: - page_title = graph.title - else: - page_title = page.title - index_html = index_html.replace('BUNDLE_PATH', join('/', graph.path, 'bundle.js')) - index_html = index_html.replace('PAGE_TITLE', page_title) - write_text(index_html, fp_out) - -def sanitize_graph(graph): - page_path_lookup = {} - page_lookup = {} - for page in graph['pages']: - page_path = join('/', graph['path'], page['path']) - if page_path in page_path_lookup: - print(f"/!\\ WARNING! Duplicate found of {page_path}") - else: - page_path_lookup[page['id']] = page_path - for page in graph['pages']: - sanitize_page(page) - if page['id'] == 12: - print(page) - for tile in page['tiles']: - if tile['target_page_id']: - if tile['target_page_id'] == -1: - tile['href'] = tile['settings']['external_link_url'] - elif tile['target_page_id'] > 0: - tile['href'] = page_path_lookup[tile['target_page_id']] - sanitize_tile(tile) - page_path = page_path_lookup[page['id']] - page_lookup[page_path] = page - # print(page_lookup['/asdf/testttt']) - graph['pages'] = page_lookup - graph['home_page'] = page_path_lookup[graph['home_page_id']] - return graph - -def sanitize_page(data): - if 'created_at' in data: - del data['created_at'] - if 'updated_at' in data: - del data['updated_at'] - if 'graph_id' in data: - del data['graph_id'] - -def sanitize_tile(data): - if 'created_at' in data: - del data['created_at'] - if 'updated_at' in data: - del data['updated_at'] - if 'username' in data: - del data['username'] - if 'graph_id' in data: - del data['graph_id'] - if 'page_id' in data: - del data['page_id'] - if 'target_page_id' in data: - del data['target_page_id'] + export_site(opt_graph_path, opt_output_dir) \ No newline at end of file diff --git a/frontend/app/site/actions.js b/frontend/app/site/actions.js deleted file mode 100644 index e672028..0000000 --- a/frontend/app/site/actions.js +++ /dev/null @@ -1,19 +0,0 @@ -import { bindActionCreators } from 'redux' -// import { actions as crudActions } from './api' - -import * as siteActions from './site/site.actions' - -import { store } from './store' - -export default - // Object.keys(crudActions) - // .map(a => [a, crudActions[a]]) - // .concat( - [ - ['site', siteActions], - ] //) - .map(p => [p[0], bindActionCreators(p[1], store.dispatch)]) - .concat([ - // ['socket', socketActions], - ]) - .reduce((a,b) => (a[b[0]] = b[1])&&a,{}) \ No newline at end of file diff --git a/frontend/app/site/app.js b/frontend/app/site/app.js deleted file mode 100644 index 389e5b5..0000000 --- a/frontend/app/site/app.js +++ /dev/null @@ -1,35 +0,0 @@ -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' - -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] - } - // console.log('loading', graph_name, path_name) - actions.site.loadSite(graph_name, path_name) - } - - render() { - return ( - -
- - { - // setTimeout(() => this.props.history.push('/'), 10) - return null - }} /> -
-
- ) - } -} -/* -*/ diff --git a/frontend/app/site/index.js b/frontend/app/site/index.js deleted file mode 100644 index 6f1a0a5..0000000 --- a/frontend/app/site/index.js +++ /dev/null @@ -1,17 +0,0 @@ -import React from 'react' -import ReactDOM from 'react-dom' -import { Provider } from 'react-redux' - -import App from './app' - -import { store, history } from './store' - -const container = document.createElement('div') -container.classList.add('container') -document.body.appendChild(container) - -ReactDOM.render( - - - , container -) diff --git a/frontend/app/site/site/site.actions.js b/frontend/app/site/site/site.actions.js deleted file mode 100644 index 79e4573..0000000 --- a/frontend/app/site/site/site.actions.js +++ /dev/null @@ -1,11 +0,0 @@ -import * as types from '../types' -import { api } from 'app/utils' - -export const setSiteTitle = title => dispatch => { - document.querySelector('title').innerText = title - 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') -) diff --git a/frontend/app/site/site/site.reducer.js b/frontend/app/site/site/site.reducer.js deleted file mode 100644 index 85c3486..0000000 --- a/frontend/app/site/site/site.reducer.js +++ /dev/null @@ -1,36 +0,0 @@ -import * as types from '../types' - -const initialState = { - siteTitle: 'swimmer', - graph: { - loading: true, - } -} - -export default function siteReducer(state = initialState, action) { - console.log(action.type, action) - switch (action.type) { - case types.site.set_site_title: - return { - ...state, - siteTitle: action.payload, - } - - case types.site.loaded: - return { - ...state, - graph: action.data.graph, - } - - case '@@router/LOCATION_CHANGE': - return { - ...state, - graph: { - ...state.graph, - } - } - - default: - return state - } -} diff --git a/frontend/app/site/store.js b/frontend/app/site/store.js deleted file mode 100644 index a228e2b..0000000 --- a/frontend/app/site/store.js +++ /dev/null @@ -1,37 +0,0 @@ -import { applyMiddleware, compose, combineReducers, createStore } from 'redux' -import { connectRouter, routerMiddleware } from 'connected-react-router' -import { createBrowserHistory } from 'history' -import thunk from 'redux-thunk' - -import siteReducer from './site/site.reducer' - -const createRootReducer = history => ( - combineReducers({ - auth: (state = {}) => state, - router: connectRouter(history), - site: siteReducer, - }) -) - -const configureStore = (initialState = {}, history) => { - const composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose - - const store = createStore( - createRootReducer(history), - initialState, - composeEnhancers( - applyMiddleware( - thunk, - routerMiddleware(history) - ), - ), - ) - - return store -} - -const history = createBrowserHistory() -const store = configureStore({}, history) -const { dispatch } = store - -export { store, history, dispatch } diff --git a/frontend/app/site/types.js b/frontend/app/site/types.js deleted file mode 100644 index 23bed98..0000000 --- a/frontend/app/site/types.js +++ /dev/null @@ -1,11 +0,0 @@ -import { with_type, crud_type } from 'app/api/crud.types' - -export const site = with_type('site', [ - 'set_site_title', 'loading', 'loaded', 'error', -]) - -export const system = with_type('system', [ - 'load_site', -]) - -export const init = '@@INIT' 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 ( -
-
-
- -
-
-
- ) - } - const { settings } = page - const pageStyle = { backgroundColor: settings ? settings.background_color : '#000000' } - // console.log(page) - return ( -
-
- {page.tiles.map(tile => { - return ( - this.handleMouseDown(e, tile)} - onDoubleClick={e => {}} - /> - ) - })} -
-
- ) - } -} - -const mapStateToProps = state => ({ - site: state.site, - graph: state.site.graph, -}) - -const mapDispatchToProps = dispatch => ({ -}) - -export default connect(mapStateToProps, mapDispatchToProps)(ViewerContainer) diff --git a/frontend/app/utils/index.js b/frontend/app/utils/index.js index bb5e01d..ce8d75c 100644 --- a/frontend/app/utils/index.js +++ b/frontend/app/utils/index.js @@ -120,6 +120,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/page/components/page.editor.js b/frontend/app/views/page/components/page.editor.js index d324874..25342d2 100644 --- a/frontend/app/views/page/components/page.editor.js +++ b/frontend/app/views/page/components/page.editor.js @@ -73,6 +73,7 @@ class PageEditor extends Component { } 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 diff --git a/frontend/app/views/page/components/tile.form.js b/frontend/app/views/page/components/tile.form.js index 3f43dd0..a9f34a7 100644 --- a/frontend/app/views/page/components/tile.form.js +++ b/frontend/app/views/page/components/tile.form.js @@ -10,12 +10,12 @@ import { TextInput, NumberInput, ColorInput, Slider, Select, LabelDescription, TextArea, Checkbox, SubmitButton, Loader } from 'app/common' -import { preloadImage } from 'app/utils' +import { preloadImage, preloadVideo } from 'app/utils' import * as tileActions from '../../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 +31,7 @@ const ALIGNMENTS = [ const REQUIRED_KEYS = { image: ['url'], + video: ['url'], text: ['content'], link: [], script: [], @@ -40,6 +41,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,6 +54,7 @@ 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', }, @@ -80,6 +86,22 @@ const newImage = (data) => ({ ...data, }) +const newVideo = (data) => ({ + settings: { + ...newPosition(), + video_style: 'cover', + url: "", + external_link_url: "", + cursor: 'none', + muted: false, + loop: false, + autoadvance: false, + }, + type: 'video', + target_page_id: null, + ...data, +}) + const newText = (data) => ({ settings: { ...newPosition(), @@ -123,12 +145,14 @@ const newPosition = (data) => ({ width: 0, height: 0, rotation: 0, scale: 1, opacity: 1, + units: false, align: "center_center", ...data, }) const TYPE_CONSTRUCTORS = { image: newImage, + video: newVideo, text: newText, link: newLink, script: newScript, @@ -277,6 +301,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)) { @@ -362,6 +404,8 @@ class TileForm extends Component { {temporaryTile.type === 'image' ? this.renderImageForm() + : temporaryTile.type === 'video' + ? this.renderVideoForm() : temporaryTile.type === 'text' ? this.renderTextForm() : temporaryTile.type === 'link' @@ -453,6 +497,61 @@ class TileForm extends Component { ) } + renderVideoForm() { + // const { isNew } = this.props + const { temporaryTile } = this.props + const { errorFields } = this.state + // console.log(temporaryTile.settings) + return ( +
+
+ +
+
+ +
+ {graph.uploads.map(upload => ( +
this.toggleAudio(upload)} > + +
+
{unslugify(upload.fn)}
+
+
+ ))} +
+ ) + } +} + +const unslugify = fn => fn.replace(/-/g, ' ').replace(/_/g, ' ').replace('.mp3', '') + +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 b969400..0766580 100644 --- a/frontend/app/views/graph/components/graph.header.js +++ b/frontend/app/views/graph/components/graph.header.js @@ -14,6 +14,7 @@ function GraphHeader(props) {
+
) diff --git a/frontend/app/views/graph/graph.actions.js b/frontend/app/views/graph/graph.actions.js index a24ccc2..eba3f92 100644 --- a/frontend/app/views/graph/graph.actions.js +++ b/frontend/app/views/graph/graph.actions.js @@ -25,6 +25,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 }) } 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 {
{this.props.graph.editor.addingPage && } {this.props.graph.editor.editingPage && } + {this.props.graph.editor.showingAudio && }
diff --git a/frontend/app/views/graph/graph.css b/frontend/app/views/graph/graph.css index 2805cb0..c6ef115 100644 --- a/frontend/app/views/graph/graph.css +++ b/frontend/app/views/graph/graph.css @@ -146,6 +146,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 { diff --git a/frontend/app/views/graph/graph.reducer.js b/frontend/app/views/graph/graph.reducer.js index 6be5089..30049b5 100644 --- a/frontend/app/views/graph/graph.reducer.js +++ b/frontend/app/views/graph/graph.reducer.js @@ -7,6 +7,7 @@ const initialState = crudState('graph', { editor: { addingPage: false, editingPage: false, + showingAudio: false, }, options: { } @@ -36,6 +37,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 +57,7 @@ export default function graphReducer(state = initialState, action) { ...state.editor, addingPage: true, editingPage: false, + showingAudio: false, } } @@ -52,6 +67,7 @@ export default function graphReducer(state = initialState, action) { editor: { ...state.editor, addingPage: false, + showingAudio: false, } } @@ -62,6 +78,7 @@ export default function graphReducer(state = initialState, action) { ...state.editor, addingPage: !state.editor.addingPage, editingPage: false, + showingAudio: false, } } @@ -72,6 +89,7 @@ export default function graphReducer(state = initialState, action) { ...state.editor, addingPage: false, editingPage: true, + showingAudio: false, } } @@ -81,6 +99,7 @@ export default function graphReducer(state = initialState, action) { editor: { ...state.editor, editingPage: false, + showingAudio: false, } } @@ -91,6 +110,18 @@ 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, } } diff --git a/static/img/icons_pause_white.svg b/static/img/icons_pause_white.svg new file mode 100755 index 0000000..59c7e60 --- /dev/null +++ b/static/img/icons_pause_white.svg @@ -0,0 +1,6 @@ + + + + + diff --git a/static/img/icons_play_white.svg b/static/img/icons_play_white.svg new file mode 100755 index 0000000..78ff002 --- /dev/null +++ b/static/img/icons_play_white.svg @@ -0,0 +1,6 @@ + + + + + -- cgit v1.2.3-70-g09d2 From 6d435be9facbc5e4badc0f310c4199b113dc1aec Mon Sep 17 00:00:00 2001 From: Jules Laplace Date: Tue, 16 Mar 2021 18:21:44 +0100 Subject: log --- frontend/app/views/graph/components/audio.list.js | 1 - 1 file changed, 1 deletion(-) diff --git a/frontend/app/views/graph/components/audio.list.js b/frontend/app/views/graph/components/audio.list.js index bd8fe16..b7bf19a 100644 --- a/frontend/app/views/graph/components/audio.list.js +++ b/frontend/app/views/graph/components/audio.list.js @@ -97,7 +97,6 @@ class AudioList extends Component { const { playing, play_id } = this.state const { graph } = this.props // console.log(graph.uploads) - console.log(playing, play_id) return (
-- cgit v1.2.3-70-g09d2 From 92566ba17f5e921d5bff1f3fb4e4b0d92ca4fd39 Mon Sep 17 00:00:00 2001 From: Jules Laplace Date: Wed, 17 Mar 2021 11:54:58 +0100 Subject: audio select --- frontend/app/utils/index.js | 1 + .../app/views/audio/components/audio.select.js | 53 ++++++++++++++ frontend/app/views/graph/components/audio.list.js | 3 +- frontend/app/views/graph/components/page.form.js | 40 ++++++---- frontend/app/views/page/components/tile.form.js | 85 ++++++++++++++-------- 5 files changed, 133 insertions(+), 49 deletions(-) create mode 100644 frontend/app/views/audio/components/audio.select.js diff --git a/frontend/app/utils/index.js b/frontend/app/utils/index.js index ce8d75c..1114d65 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 */ 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..73142f0 --- /dev/null +++ b/frontend/app/views/audio/components/audio.select.js @@ -0,0 +1,53 @@ +import React, { Component } from 'react' +import { connect } from 'react-redux' + +import { Select } from 'app/common' + +const NO_AUDIO = 0 +const AUDIO_TOP_OPTIONS = [ + { name: NO_AUDIO, label: 'No Audio' }, + { name: -2, label: '──────────', disabled: true }, +] + +class AudioSelect extends Component { + state = { + audioList: [] + } + + constructor(props) { + super(props) + this.handleSelect = this.handleSelect.bind(this) + } + + componentDidMount(){ + const { uploads } = this.props.graph.show.res + const audioUploads = uploads + .filter(upload => upload.tag === 'audio') + .map(page => ({ name: upload.id, label: page.path })) + let audioList = [ + ...AUDIO_TOP_OPTIONS, + ...audioUploads, + ] + this.setState({ + audioList, + }) + } + + render() { + return ( +