frontend: initial commit

This commit is contained in:
Franz Heinzmann (Frando) 2021-10-18 21:48:38 +02:00
parent 8d70acc5d2
commit 1ad243f043
21 changed files with 30437 additions and 0 deletions

2
frontend/.gitignore vendored Normal file
View file

@ -0,0 +1,2 @@
node_modules
yarn*

76
frontend/bin.js Executable file
View file

@ -0,0 +1,76 @@
#!/usr/bin/env node
const p = require('path')
const { build, cliopts } = require('estrella')
const fs = require('fs/promises')
const copy = require('recursive-copy')
const [opts, args] = cliopts.parse(
['host', 'Development server: Hostname', '<host>'],
['port', 'Development server: Port']
)
const base = process.cwd()
const outdir = p.join(base, 'build')
const staticdir = p.join(base, 'static')
const entry = p.join(base, 'src/index.js')
const outfile = p.join(outdir, 'bundle.js')
let devServerMessage
let firstRun = true
build({
entry,
outfile,
// external: ['http', 'https'],
bundle: true,
sourcemap: true,
minify: false,
loader: {
'.js': 'jsx',
'.woff': 'file',
'.woff2': 'file'
},
// This banner fixes some modules that were designed for Node.js
// to run in the browser by providing minimal shims.
banner: `
var global = window;
window.process = {
title: "browser",
env: {},
nextTick: function (cb, ...args) {
Promise.resolve().then(() => cb(...args))
}
};
`,
define: {
'process.title': 'browser',
'process.env.NODE_ENV': JSON.stringify(process.env.NODE_ENV || 'development')
},
// dgram external needed for osc-js to compile
external: ['dgram'],
onEnd
})
// Run a local web server with livereload when -watch is set
if (cliopts.watch) {
const instant = require('instant')
const express = require('express')
const port = cliopts.port || 3000
const host = opts.host || 'localhost'
const app = express()
app.use(instant({ root: outdir }))
app.listen(port, host, () => {
devServerMessage = `Listening on http://${host}:${port} and watching for changes ...`
console.log(devServerMessage)
})
}
async function onEnd () {
if (!firstRun) return
firstRun = false
if (devServerMessage) console.log(devServerMessage)
try {
const stat = await fs.stat(staticdir)
if (!stat.isDirectory()) return
await copy(staticdir, outdir)
} catch (err) {}
}

29224
frontend/build/bundle.js Normal file

File diff suppressed because it is too large Load diff

File diff suppressed because one or more lines are too long

11
frontend/build/index.html Normal file
View file

@ -0,0 +1,11 @@
<!DOCTYPE html>
<html lang="en">
<head>
<title>arsonar</title>
<meta charset="utf-8">
<link rel="stylesheet" href="bundle.css">
</head>
<body>
<script src="bundle.js"></script>
</body>
</html>

35
frontend/package.json Normal file
View file

@ -0,0 +1,35 @@
{
"name": "@studiox/frontend",
"version": "1.0.0",
"description": "",
"main": "src/index.js",
"scripts": {
"build": "node bin.js",
"dev": "node bin.js -w"
},
"keywords": [],
"author": "",
"license": "GPL-3.0",
"dependencies": {
"@chakra-ui/core": "^0.6.1",
"@emotion/core": "^10.0.28",
"@emotion/styled": "^10.0.27",
"debug": "^4.1.1",
"emotion-theming": "^10.0.27",
"osc-js": "^2.1.0",
"react": "^16.13.1",
"react-dom": "^16.13.1",
"react-hook-form": "^5.1.1",
"react-router": "^5.1.2",
"react-router-dom": "^5.1.2",
"reconnecting-websocket": "^4.4.0",
"split2": "^3.1.1",
"thunky": "^1.1.0"
},
"devDependencies": {
"estrella": "^1.3.0",
"express": "^4.17.1",
"instant": "^1.11.0",
"recursive-copy": "^2.0.11"
}
}

45
frontend/src/App.js Normal file
View file

@ -0,0 +1,45 @@
import React from 'react'
// import { hot } from 'react-hot-loader/root'
import { Flex, ThemeProvider, CSSReset, Box } from '@chakra-ui/core'
import createTheme from './theme'
import Player from './comp/Player'
import Mixer from './comp/Mixer'
import Chat from './comp/Chat'
import { Pane, Container } from './comp/Pane'
function App (props) {
return (
<Wrapper>
<Container direction='row' minHeight='100vh' bg='#222' p={2}>
<Pane flex={1}>
<Mixer />
</Pane>
<Container direction='column'>
<Pane>
<Player label='DeckA' address='/player/decka' />
</Pane>
<Pane>
<Player label='DeckB' address='/player/deckb' />
</Pane>
</Container>
<Pane maxWidth='sm'>
<Chat />
</Pane>
</Container>
</Wrapper>
)
}
// export default hot(App)
export default App
function Wrapper (props) {
const { children } = props
const theme = createTheme()
return (
<ThemeProvider theme={theme}>
<CSSReset />
{children}
</ThemeProvider>
)
}

26
frontend/src/comp/Bar.js Normal file
View file

@ -0,0 +1,26 @@
import React from 'react'
import { Flex, Box } from '@chakra-ui/core'
export default function Bar (props) {
return (
<Flex
borderBottom='barSeperator'
{...props}
/>
)
}
export function Element (props) {
return (
<Box
px={2}
py={1}
borderRight='barSeperator'
fontFamily='mono'
color='textBar'
{...props}
/>
)
}
Bar.Element = Element

View file

@ -0,0 +1,61 @@
import React, { useState } from 'react'
import { PseudoBox } from '@chakra-ui/core'
export default function Button (props) {
const {
children,
active,
size = 12,
square,
variant = 'redgreen',
...other
} = props
const bg = 'button.' + variant + (active ? '.on' : '.off')
const width = square ? size : undefined
const color = active ? 'black' : '#eee'
// other._hover = {
// boxShadow: '0 0 10px 2px rgba(255, 50, 200, 0.7)'
// }
return (
<PseudoBox
as='button'
w={width}
h={size}
px={2}
bg={bg}
borderWidth='2px'
borderStyle='solid'
borderColor={bg}
fontWeight='600'
color={color}
_focus={{
border: 'inputFocus',
boxShadow: 'inputFocus'
}}
_hover={{
border: 'inputHover'
}}
{...other}
>
{children}
</PseudoBox>
)
}
export function ToggleButton ({
defaultValue,
onChange,
...props
}) {
const [state, setState] = useState(defaultValue || false)
return <Button active={state} onClick={onClick} {...props} />
function onClick (_e) {
setState(state => {
if (onChange) onChange(!state)
return !state
})
}
}
Button.Toggle = ToggleButton

126
frontend/src/comp/Chat.js Normal file
View file

@ -0,0 +1,126 @@
import React, { useState, useEffect } from 'react'
import { Flex, Box, Input, Textarea } from '@chakra-ui/core'
import Button from './Button'
import { useForm } from 'react-hook-form'
// import client from '@arso-project/sonar-ui/src/lib/client'
// async function loadMessages () {
// const records = await client.query('records', { schema: 'sonar.chat' })
// console.log(records)
// return records
// }
function useMessages () {
const [messages, setMessages] = useState([])
return [messages, postMessage]
// useEffect(() => {
// loadMessages()
// .then(res => setMessages(res))
// .catch(err => console.error(err))
// }, [])
function postMessage (message) {
console.log('post', message)
message = { value: message }
setMessages(messages => [...messages, message])
// const { message } = values
// try {
// const record = {
// schema: 'sonar.chat',
// value: {
// message
// }
// }
// const id = await client.put(record)
// console.log('done', id)
// } catch (err) {
// console.log('error', err)
// setError('message', err.message)
// }
}
}
export default function Chat (props) {
const [messages, postMessage] = useMessages()
console.log('messages', messages)
return (
<Flex
height='100%'
direction='column'
bg='black'
>
<Messages messages={messages} />
<MessageBox onSubmit={postMessage} />
</Flex>
)
}
function Messages (props) {
const { messages } = props
return (
<Box
flex={1}
p={2}
overflow='auto'
>
{messages.map((message, i) => (
<Message key={i} message={message} />
))}
list
</Box>
)
}
function Message (props) {
const { message } = props
const text = message.value.message
return (
<Box p={2} fontSize='lg' fontFamily='mono' color='white'>
{text}
</Box>
)
}
function MessageBox (props) {
const { onSubmit } = props
const { handleSubmit, register, setError } = useForm()
return (
<form onSubmit={handleSubmit(onSubmit)}>
<Flex
margin={2}
>
<Textarea
w='full'
h='12'
resize='none'
borderRadius={0}
fontSize='lg'
bg='black'
color='white'
fontFamily='mono'
fontWeight='600'
flex={1}
p='2'
m='0'
// lineHeight='2em'
border='4px solid black'
borderColor='black'
_focus={{
border: '4px solid magenta'
}}
_hover={{
border: '4px solid #440077'
}}
placeholder='type here to chat'
ref={register()}
name='message'
/>
<Button square onSubmit={handleSubmit(onSubmit)}>
Send
</Button>
</Flex>
</form>
)
}

129
frontend/src/comp/Meter.js Normal file
View file

@ -0,0 +1,129 @@
import React, { useState, useEffect, useMemo } from 'react'
import { Box } from '@chakra-ui/core'
// import client from './client'
const SCALE_MIN = -60
const SCALE_MAX = 0
export default function Meter (props) {
// style={{ position: 'absolute', left: 0, top: 0, bottom: 0, right: 0 }} {...props} />
const style = {
minWidth: 0,
position: 'absolute',
height: '100%',
width: '100%'
}
return (
<Box position='relative' width='100%' height='100%'>
<SvgMeter style={style} {...props} />
</Box>
)
}
function SvgMeter (props) {
const { min, max, value, style, redPeak = 80, width = 10, name } = props
let peak = value2px(value, min, max, 0, 100)
if (peak > 100) peak = 100
const [peakDrop, updatePeakDrop] = usePeakDrop()
useEffect(() => {
updatePeakDrop(peak)
}, [peak, min, max])
const maskid = useMemo(() => 'svg-meter-mask-' + (name || Date.now()), [name])
const peakRed = value2px(redPeak, min, max, 0, 100)
const meter = useMemo(() => (
<svg
xmlns='http://www.w3.org/2000/svg'
viewBox={`0 0 ${width} 100`}
preserveAspectRatio='none'
style={style}
>
<defs>
<mask id={maskid}>
<rect x='0' y={100 - peak} width={width} height={peak} fill='white' />
</mask>
</defs>
<g
mask={`url(#${maskid})`}
style={{ opacity: 0.9 }}
>
<rect
y='0'
x='0'
height={100 - peakRed}
width={width}
style={{ fill: '#dd1100' }}
/>
<rect
y={100 - peakRed}
x='0'
height={peakRed}
width={width}
style={{ fill: '#00dd00' }}
/>
/>
</g>
<rect y={100 - peakDrop} x='0' width={width} height='0.5' fill='#00f' />
<rect x='0' width={width} height='0.2' fill='#ff0' y={100 - peakRed} />
</svg>
), [peak, peakDrop])
return meter
}
function usePeakDrop () {
const tickDown = 1
const tickTime = 50
const [state, setState] = useState({ max: 0, peak: 0 })
useEffect(() => {
const interval = setInterval(() => {
setState(state => update(state, state.peak))
}, tickTime)
return () => clearInterval(interval)
}, [])
return [state.max, updatePeak]
function update (state, peak) {
const now = Date.now()
const nextState = { ...state }
if (peak !== state.peak) {
nextState.peak = peak
nextState.lastPeak = Date.now()
}
if (state.max < peak) {
nextState.max = peak
} else if (now - state.lastPeak > 1000) {
if (now - state.lastPeak > 3000) {
nextState.max = state.peak
} else {
nextState.max = Math.max(state.max - tickDown, state.peak)
}
}
return nextState
}
function updatePeak (peak) {
setState(state => update(state, peak))
}
}
function toPx (val) {
return value2px(val, SCALE_MIN, SCALE_MAX, 0, 100)
}
function value2px (value, valueMin, valueMax, pxMin, pxMax) {
if (value < valueMin) value = valueMin
if (value > valueMax) value = valueMax
if (isNaN(value) || value === null) value = valueMin
const valueWidth = valueMax - valueMin
const pixelWidth = pxMax - pxMin
const ratio = pixelWidth / valueWidth
return ratio * (value - valueMin) + pxMin
}
function style (color) {
return { style: { fill: color } }
}

120
frontend/src/comp/Mixer.js Normal file
View file

@ -0,0 +1,120 @@
import React, { useReducer, useState } from 'react'
import {
Box, Flex, Badge, Text, Icon, Image, PseudoBox,
FormControl, FormLabel, Input, FormHelperText,
Slider, SliderTrack, SliderFilledTrack, SliderThumb
} from '@chakra-ui/core'
// import css from '@emotion/css'
import { usePeak } from '../hooks/use-peak'
import Button from './Button'
import Meter from './Meter'
import { useOscReducer, useOscValue, useOscWritable } from '../hooks/use-osc-reducer'
export default function Mixer () {
return (
<Flex flex={1} h='100%' p='2'>
<FaderChannel channel={0} />
<FaderChannel channel={1} />
<FaderChannel channel={2} />
<FaderChannel channel={3} />
<FaderChannel channel={4} />
<FaderChannel channel={5} />
</Flex>
)
}
// function reducer (state, action) {
// console.log('reduce', state, action)
// if (action.cmd === 'set') {
// return { ...state, [action.key]: action.args[0] }
// }
// return state
// }
// const initialState = {
// volume: 50,
// pgm: 1,
// pfl: 0
// }
function OscMeter (props) {
const { offset } = props
const peak = usePeak(offset)
console.log('USE PEAK', offset, peak)
const peaks = [peak, peak]
// const { address, combine } = props
// const peak = useOscValue(address, -60, { single: true })
// // TODO: Support stereo peaks?
// const peaks = [peak, peak]
// const peak = (peaks[0] + peaks[1]) / 2
// console.log('render meter', peak)
return (
<>
{peaks.map((peak, i) => (
<Box w={2} key={i}>
<Meter key={i} min={-60} max={0} value={peak} />
</Box>
))}
</>
)
}
function FaderChannel (props) {
const { channel, label } = props
const bus = 1
const peakOffset = ((bus + 2) * 8) + channel
// const peak = usePeak(peakOffset)
// const [state, dispatch] = useOscReducer(reducer, initialState, name)
const volumeAddress = '/mixer/bus/1/ch/' + channel + '/volume'
const [volume, setVolume] = useOscWritable(volumeAddress, 0, { single: true })
const [pgm, setPgm] = useOscWritable('/mixer/bus/1/ch/' + channel + '/on', 0, { single: true })
const [pfl, setPfl] = useOscWritable('/mixer/bus/2/ch/' + channel + '/on', 0, { single: true })
console.log('render fc', { volume, pgm, pfl })
const peakAddress = '/mixer/bus/1/ch/' + channel + '/level'
// const [value, setValue] = useState(0)
return (
<Flex direction='column' w={16} pr={2} mr={2} overflow='hidden'>
<Text display='block' fontSize='xs' fontWeight='bold' color='#ddd'>{label}</Text>
<Flex flex={1}>
// <OscMeter address={peakAddress} channel={0} />
<OscMeter offset={peakOffset} />
</Flex>
<Button onClick={e => setPgm(pgm => pgm ? 0 : 1)} active={!!pgm} mx='auto' mt={1} square size={12} variant='green'>ON</Button>
<Button onClick={e => setPfl(pfl => pfl ? 0 : 1)} active={!!pfl} mx='auto' mt={1} square size={12} variant='blue'>PFL</Button>
<Fader onChange={setVolume} value={volume} />
<Text textAlign='center' display='block' fontSize='xs' fontWeight='bold' color='#ddd'>{label}</Text>
</Flex>
)
// function onFaderChange (value) {
// // setValue(value)
// // dispatch({ address: address + '/volume', args: [value] })
// dispatch({ cmd: 'volume', args: [parseFloat(value)] })
// }
// function onBusChange (name) {
// return function onClick (e) {
// console.log('dipatch', name, state)
// const intValue = state[name] ? 0 : 1
// dispatch({ cmd: name, args: [intValue] })
// }
// }
}
function Fader (props) {
return (
<Slider
color='pink'
defaultValue={0}
min={-70}
max={4}
step={0.2}
orientation='vertical'
{...props}
>
<SliderTrack />
<SliderFilledTrack />
<SliderThumb />
</Slider>
)
}

42
frontend/src/comp/Pane.js Normal file
View file

@ -0,0 +1,42 @@
import React, { useRef } from 'react'
import { css, Box, PseudoBox, Stack } from '@chakra-ui/core'
export function Pane (props) {
const { children, ...rest } = props
return (
<PseudoBox
bg='black'
display='flex'
alignItems='stretch'
flex={1}
position='relative'
border='paneBorder'
_focusWithin={{
border: 'paneBorderFocus'
}}
p={2}
{...rest}
>
<Box
position='absolute'
left='0'
right='0'
top='0'
bottom='0'
overflow='auto'
>
{children}
</Box>
</PseudoBox>
)
}
export function Container (props) {
return (
<Stack
flex={1}
{...props}
alignItems='stretch'
/>
)
}

113
frontend/src/comp/Player.js Normal file
View file

@ -0,0 +1,113 @@
import React, { useReducer, useState, useEffect } from 'react'
import {
Box, Flex, Input
} from '@chakra-ui/core'
import { useForm } from 'react-hook-form'
// import css from '@emotion/css'
import Button from './Button'
import Bar from './Bar'
import { useOscReducer } from '../hooks/use-osc-reducer'
const initialState = { play: 'stop' }
function reducer (state, action) {
const x = s => ({ ...state, ...s })
const { cmd, args, key } = action
if (cmd === 'set') {
return x({ [key]: args[0] })
}
if (cmd === 'play') {
return x({ play: 'play' })
}
if (cmd === 'pause') {
if (state.play === 'stop') return state
return x({ play: 'pause' })
}
if (cmd === 'playpause') {
if (state.play === 'stop') return x({ play: 'play' })
if (state.play === 'pause') return x({ play: 'play' })
return x({ play: 'pause' })
}
if (cmd === 'stop') {
return x({ play: 'stop' })
}
if (cmd === 'fullscreen') {
return x({ fullscreen: !state.fullscreen })
}
return state
}
export default function Player (props) {
const { label = 'Player', name = '/player/default' } = props
const [state, dispatch] = useOscReducer(reducer, initialState, name)
const { handleSubmit, register, errors } = useForm()
let style = {}
if (state.fullscreen) {
style = {
position: 'fixed',
left: 0,
top: 0,
bottom: 0,
right: 0,
zIndex: 1000
}
}
return (
<Flex
height='100%'
direction='column'
bg='black'
contentAlign='stretch'
style={style}
>
<Bar>
<Bar.Element>{label}</Bar.Element>
<Bar.Element>{state.play}</Bar.Element>
</Bar>
<Box flex={1} />
<Flex as='form' onSubmit={handleSubmit(onAdd)} p={2}>
<Input name='url' placeholder='Enter URL to play' ref={register()} />
<Button>Add</Button>
</Flex>
<Flex p={2}>
<PlayerButton state={state.play} dispatch={dispatch} action='play' active='play'>
Play
</PlayerButton>
<PlayerButton variant='yellow' state={state.play} dispatch={dispatch} action='pause' active='pause'>
Pause
</PlayerButton>
<PlayerButton variant='red' state={state.play} dispatch={dispatch} action='stop' active='stop'>
Stop
</PlayerButton>
</Flex>
</Flex>
)
function onAdd (values) {
console.log('add', values)
dispatch({ cmd: 'load', args: [values.url] })
// e.preventDefault()
// console.log(e.target.value)
// const url = e.target.name.value
// console.log(values)
}
// <Button square onClick={_e => dispatch('fullscreen')} active={state.fullscreen}>Fullscreen</Button>
// <PlayerButton state={state.play} dispatch={dispatch} action='playpause' active='play'>
// Playpause
// </PlayerButton>
}
function PlayerButton (props) {
let { action, dispatch, active, state, ...other } = props
if (action && dispatch && !other.onClick) {
other.onClick = _e => dispatch({ cmd: action })
}
if (typeof active !== 'boolean' && active) {
active = state === active
}
return (
<Button square variant='green' mr={2} size={16} active={active} {...other} />
)
}

View file

@ -0,0 +1,108 @@
import { useState, useReducer, useEffect } from 'react'
import osc from '../lib/osc'
import Debug from 'debug'
// const STATE = '/s'
// const COMMAND = '/c'
const debug = Debug('ui:osc:reducer')
/**
* Listen on a OSC value change over time.
*/
export function useOscValue (address, defaultValue, opts = {}) {
// TODO: Global store.
const [state, setState] = useState(defaultValue)
useEffect(() => {
// const stateAddress = STATE + address
// console.log('useOscValue init', stateAddress)
const unwatch = osc.on(address, message => {
const { address, args } = message
if (opts.single) setState(args[0])
else setState(args)
// setState(state => ({ ...state, [address]: args }))
})
return unwatch
}, [address])
return state
}
/**
* Write to an OSC value.
*/
export function useOscWritable (address, defaultValue, opts = {}) {
const state = useOscValue(address, defaultValue, opts)
return [state, setState]
function setState (nextValue) {
if (typeof nextValue === 'function') nextValue = nextValue(state)
// const commandAddress = COMMAND + address
// const parts = address.split('/')
// const last = parts.pop()
// const cmdAddress = [...parts, 'cmd', last].join('/')
console.log('useOscWritable write', address, nextValue)
osc.send(address, nextValue)
}
}
export function useOscReducer (reducer, initialState, prefix = '') {
const [state, localDispatch] = useReducer(reducer, initialState)
const prefixLen = prefix.split('/').length
// Listen for all state change messages and dispatch into the reducer.
useEffect(() => {
const wildcardAddress = prefix + '/*'
const unwatch = osc.on(wildcardAddress, message => {
const { address, args } = message
const key = address.split('/').slice(prefixLen + 1).join('/')
localDispatch({ cmd: 'set', address: key, args })
})
return unwatch
})
return [state, dispatch]
function dispatch (action) {
console.log('useOscReducer dispatch', action)
send(action)
}
function send (action) {
const { address, args } = action
const fullAddress = COMMAND + prefix + address
osc.send(fullAddress, args)
}
}
// TODO: Broken, fix address paths.
export function useOscReducerOld (reducer, initialState, prefix) {
const [state, localDispatch] = useReducer(reducer, initialState)
// const [remoteState, remoteDispatch] = useReducer(reducer, initialState)
//
useEffect(() => {
const path = prefix + '/state/*'
const unwatch = osc.on(path, message => {
const { address, args } = message
// console.log('RECVV!!!!', message)
const key = address.split('/').pop()
localDispatch({ cmd: 'set', key, args })
})
return unwatch
}, [])
return [state, dispatch]
function dispatch (action) {
console.log('dispatch', action)
send(action)
// localDispatch(action)
}
function send (action) {
const { cmd, args } = action
const address = [prefix, 'cmd', cmd].join('/')
osc.send(address, args)
}
}

View file

@ -0,0 +1,47 @@
import osc from '../lib/osc'
import { useState, useEffect } from 'react'
class PeakStore {
constructor () {
this.watchers = []
}
watch (fn) {
this.watchers.push(fn)
return () => (this.watchers = this.watchers.filter(w => w !== fn))
}
set (val) {
this.val = val
for (const watcher of this.watchers) {
watcher(val)
}
}
get () {
return this.val
}
}
const peaks = new PeakStore()
osc.on('/peaks', message => {
const { args } = message
const buf = args[0]
const nextPeaks = {}
for (let offset = 0; offset < 48; offset++) {
let pos = offset * 2
let val = buf[pos] * 256 + buf[pos + 1]
val = (val / 800) - 70
nextPeaks[offset] = val
}
console.log('RECV PEAKS', nextPeaks)
peaks.set(nextPeaks)
})
export function usePeak (offset) {
const [peak, setPeak] = useState(-70)
useEffect(() => {
const unwatch = peaks.watch(peaks => {
setPeak(peaks[offset])
})
})
return peak
}

9
frontend/src/index.js Normal file
View file

@ -0,0 +1,9 @@
import React from 'react'
import ReactDOM from 'react-dom'
import App from './App'
const el = document.createElement('div')
document.body.appendChild(el)
ReactDOM.render(<App />, el)

View file

@ -0,0 +1,126 @@
// eslint-disable-next-line no-undef
import ReconnectingWebSocket from 'reconnecting-websocket'
/**
* Status flags
* @private
*/
const STATUS = {
IS_NOT_INITIALIZED: -1,
IS_CONNECTING: 0,
IS_OPEN: 1,
IS_CLOSING: 2,
IS_CLOSED: 3,
}
const DEFAULT_URL = `${window.location.protocol === 'https:' ? 'wss' : 'ws'}://${window.location.host}`
/**
* Default options
* @private
*/
const defaultOptions = {
url: DEFAULT_URL,
path: null
}
/**
* OSC plugin for a Websocket client running in node or browser context
*/
export default class WebsocketClientPlugin {
/**
* Create an OSC WebsocketClientPlugin instance with given options.
* Defaults to *localhost:8080* for connecting to a Websocket server
* @param {object} [options] Custom options
* @param {string} [options.host='localhost'] Hostname of Websocket server
* @param {number} [options.port=8080] Port of Websocket server
* @param {boolean} [options.secure=false] Use wss:// for secure connections
*
* @example
* const plugin = new OSC.WebsocketClientPlugin({ port: 9912 })
* const osc = new OSC({ plugin: plugin })
*/
constructor(customOptions) {
this.opts = { ...defaultOptions, ...customOptions }
this.socket = null
this.socketStatus = STATUS.IS_NOT_INITIALIZED
this.notify = () => {}
}
/**
* Internal method to hook into osc library's
* EventHandler notify method
* @param {function} fn Notify callback
* @private
*/
registerNotify (fn) {
this.notify = fn
}
/**
* Returns the current status of the connection
* @return {number} Status identifier
*/
status () {
return this.socketStatus
}
/**
* Connect to a Websocket server. Defaults to global options
* @param {object} [customOptions] Custom options
* @param {string} [customOptions.host] Hostname of Websocket server
* @param {number} [customOptions.port] Port of Websocket server
* @param {boolean} [customOptions.secure] Use wss:// for secure connections
*/
open (customOptions = {}) {
const options = { ...this.opts, ...customOptions }
let { url, path } = options
if (path) url = url + path
console.log('WS CONNECT', url)
this.socket = new ReconnectingWebSocket(url, null, { debug: true, reconnectInterval: 1000, autoOpen: true, binaryType: 'arraybuffer' })
this.socket.binaryType = 'arraybuffer'
// this.socketStatus = STATUS.IS_CONNECTING
// register events
this.socket.onopen = () => {
console.log('WS OPEN')
this.socketStatus = STATUS.IS_OPEN
this.notify('open')
}
this.socket.onclose = () => {
this.socketStatus = STATUS.IS_CLOSED
this.notify('close')
}
this.socket.onerror = (error) => {
this.notify('error', error)
}
this.socket.onmessage = (message) => {
this.notify(message.data, { url: this.opts.url })
}
// this.socket.open()
}
/**
* Close Websocket
*/
close () {
this.socketStatus = STATUS.IS_CLOSING
this.socket.close()
}
/**
* Send an OSC Packet, Bundle or Message to Websocket server
* @param {Uint8Array} binary Binary representation of OSC Packet
*/
send (binary) {
console.log('!! SEND', binary)
this.socket.send(binary)
}
}

58
frontend/src/lib/osc.js Normal file
View file

@ -0,0 +1,58 @@
import OSC from 'osc-js'
import Debug from 'debug'
import Client from './osc-client-plugin'
const debug = Debug('osc')
const DEFAULT_OPTS = {
plugin: new Client(),
// path: '/ws',
url: 'ws://localhost:8080/ws'
// host: 'localhost',
// host: window.location.hostname,
// port: '18443/ws',
// port: '8080'
// secure: false
}
export class OSCHandler {
constructor (opts = {}) {
this.opts = { ...DEFAULT_OPTS, ...opts }
this.osc = new OSC({ plugin: this.opts.plugin })
this.osc.on('*', (message) => {
const { address, args } = message
debug('RECV %s %o', address, args)
})
this.osc.on('error', (err) => {
console.error('OSC error', err)
})
}
open () {
this.osc.open(this.opts)
}
on (address, fn) {
const id = this.osc.on(address, fn)
return () => this.osc.off(address, id)
}
send (address, args = [], opts) {
if (address instanceof OSC.Message || address instanceof OSC.Bundle || address instanceof OSC.Packet) {
debug('SEND', address)
return this.osc.send(address, args)
} else if (typeof address === 'string') {
debug('SEND', address, args)
if (!Array.isArray(args)) args = [args]
const message = new OSC.Message(address, ...args)
return this.osc.send(message, opts)
} else {
throw new Error('Invalid arguments to osc.send()', address)
}
}
}
const osc = new OSCHandler()
osc.open()
export default osc

67
frontend/src/theme.js Normal file
View file

@ -0,0 +1,67 @@
import { theme as chakra } from '@chakra-ui/core'
import { Input } from '@chakra-ui/core'
Input.defaultProps = {
...Input.defaultProps,
borderRadius: 0,
bg: '#444',
color: '#eee',
border: 'paneBorder',
borderColor: 'inputBorder',
borderWidth: '2px',
_hover: {
borderColor: 'inputBorderHover'
},
_focus: {
boxShadow: 'inputFocus',
border: 'inputFocus'
},
h: 12
}
export default function studioxtheme (props = {}) {
const theme = {
...chakra,
shadows: {
inputFocus: '0 0 10px 4px rgba(255, 50, 200, 0.7)',
},
colors: {
...chakra.colors,
textBar: '#add',
inputBorder: 'rgba(70, 100, 100, 0.5)',
inputBorderHover: 'rgba(70, 100, 100, 1)',
button: {
redgreen: {
on: chakra.colors.green['400'],
off: chakra.colors.red['600']
},
blue: {
on: '#00aaff',
off: '#333'
},
green: {
on: '#00cc44',
off: '#333'
},
red: {
on: '#cc3300',
off: '#333'
},
yellow: {
on: '#eedd00',
off: '#333'
}
}
},
borders: {
...chakra.borders,
paneBorder: '4px solid rgba(0, 150, 150, 0.4)',
paneBorderFocus: '4px solid rgba(0, 200, 200, 0.5)',
barSeperator: '4px solid rgba(0, 150, 150, 0.4)',
inputFocus: '2px solid magenta',
inputHover: '2px solid rgba(255,255,255,0.3)'
}
}
console.log(theme)
return theme
}

View file

@ -0,0 +1,11 @@
<!DOCTYPE html>
<html lang="en">
<head>
<title>arsonar</title>
<meta charset="utf-8">
<link rel="stylesheet" href="bundle.css">
</head>
<body>
<script src="bundle.js"></script>
</body>
</html>