Feat/navigate (#81)

* feat/navigate upgrade react router
* feat/navigate migrate to react router 6
* feat/navigate style modal
* feat/navigate create endpoints for app settings
* feat/navigate protect editor with pin
* feat/navigate apply dynamic routing draft
* feat/navigate upgrade relevant packages
* feat/navigate restructure directory and add tests
* feat/navigate test aliases validation
* feat/navigate validate aliases before sending
* feat/navigate create data endpoint
* feat/navigate config: prettier
* feat/navigate invalidate empty strings
* feat/navigate create endpoints
* feat/navigate parse on import
* feat/navigate navigate to alias
* feat/navigate user help and sample data
* feat/navigate refact aliases modal
* feat/navigate refact settings style
* feat/navigate update sample db
* feat/navigate link is relative to hostname
* feat/navigate navigate to first match
* feat/navigate update readme and version bump
* feat/navigate config: create shared module
* feat/navigate config: cheat module install
* feat/navigate fix tests
* feat/navigate run tests in pull request
* Update ontime_cy.yml
This commit is contained in:
Carlos Valente
2022-01-04 22:12:59 +01:00
committed by GitHub
parent dd43d5e20a
commit 2fa397548a
74 changed files with 2592 additions and 1428 deletions
+64
View File
@@ -0,0 +1,64 @@
# This workflow will do a clean install of node dependencies, cache/restore them, build the source code and run tests across different versions of node
# For more information see: https://help.github.com/actions/language-and-framework-guides/using-nodejs-with-github-actions
name: ontime_test_CI
on: [push, pull_request]
jobs:
build:
runs-on: ubuntu-latest
env:
CI: ''
steps:
- uses: actions/checkout@v2
- name: Use Node.js
uses: actions/setup-node@v2
with:
node-version: '14.x'
# Utils server
- name: Utils - Install dependencies
run: yarn install
working-directory: ./server/utils
- name: Utils - run link command
run: yarn link
working-directory: ./server/utils
# React
- name: React - Link to utils
run: yarn link ontime-utils
working-directory: ./client
- name: React - Install dependencies
run: yarn install
working-directory: ./client
- name: React - Run tests
run: yarn test:pipeline
working-directory: ./client
- name: React - Build project
run: yarn build
working-directory: ./client
# Node server
- name: React - Link to utils
run: yarn link ontime-utils
working-directory: ./server/src
- name: Server - Install dependencies
run: yarn install
working-directory: ./server/src
# App
- name: Electron - Install dependencies
run: yarn install
working-directory: ./server
- name: Electron - Run tests
run: yarn test
working-directory: ./server
+7
View File
@@ -0,0 +1,7 @@
{
"trailingComma": "es5",
"tabWidth": 2,
"semi": true,
"singleQuote": true,
"jsxSingleQuote": true
}
+11 -2
View File
@@ -21,8 +21,11 @@ From here, any device in the same network with a browser is able to render the v
You can then use the ontime logo in the top right corner to select the desired view (event in the lower thirds view, where it is hidden).
In case of unattended machines or automations, it is possible to use different URL to recall individual views
and extend with using the URL aliases feature
```
For the presentation views...
-------------------------------------------------------------
IP.ADDRESS:4001 > Web server default to presenter timer view
IP.ADDRESS:4001/preseter > Presenter / Stage timer view
IP.ADDRESS:4001/sm > Stage Manager / Backstage view
@@ -30,6 +33,11 @@ IP.ADDRESS:4001/public > Public / Foyer view
IP.ADDRESS:4001/pip > Picture in Picture view
IP.ADDRESS:4001/lower > Lower Thirds
IP.ADDRESS:4001/studio > Studio Clock
...and for the editor (the control interface, same as the app)
-------------------------------------------------------------
IP.ADDRESS:4001/studio > Studio Clock
```
More documentation available [here](https://cpvalente.gitbook.io/ontime/)
@@ -47,6 +55,9 @@ More documentation available [here](https://cpvalente.gitbook.io/ontime/)
- [x] Open Sound Control (OSC) Control and Feedback
- [x] Roll mode: run independently using the system clock
- [x] Import event list from Excel
- [x] URL Aliases (define configurable aliases to ease onsite setup)
- [x] Logging view
- [x] Edit anywhere: run ontime in your local network and use any machine to reach the editor page (same as app)
## Unopinionated
We are not interested in forcing workflows and have made ontime, so it is flexible to whichever way you would like to work.
@@ -77,8 +88,6 @@ These will be implemented in a development friendly order unless there is user d
- [ ] Companion module
- [ ] Lower Third Manager
- [ ] Note only event
- [ ] URL Aliases (define configurable aliases to ease onsite setup)
- [ ] Logging view
- [ ] Reach Schedule: way to speedup timer to meet a deadline
- [ ] vMix integration
+8 -7
View File
@@ -3,26 +3,26 @@
"version": "0.1.0",
"private": true,
"dependencies": {
"@chakra-ui/react": "^1.7.1",
"@emotion/react": "^11.5.0",
"@emotion/styled": "^11.3.0",
"@chakra-ui/react": "^1.7.3",
"@emotion/react": "^11.7.1",
"@emotion/styled": "^11.6.0",
"@testing-library/jest-dom": "^5.11.4",
"@testing-library/react": "^11.1.0",
"@testing-library/user-event": "^12.1.10",
"axios": "^0.24.0",
"framer-motion": "^4.1.6",
"jotai": "^0.16.5",
"ontime-utils": "link: ../server/utils/",
"react": "17.0.2",
"react-beautiful-dnd": "^13.1.0",
"react-dom": "^17.0.1",
"react-fast-compare": "^3.2.0",
"react-icons": "^4.3.1",
"react-qr-code": "^2.0.2",
"react-qr-code": "2.0.3",
"react-query": "^3.34.5",
"react-router": "^5.2.0",
"react-router-dom": "^5.2.0",
"react-router-dom": "^6.2.1",
"react-scripts": "5.0.0",
"socket.io-client": "^4.3.2",
"socket.io-client": "4.4.0",
"typeface-open-sans": "^1.1.13",
"use-fit-text": "^2.4.0",
"web-vitals": "^1.0.1"
@@ -31,6 +31,7 @@
"start": "set BROWSER=none&&react-scripts start",
"build": "react-scripts build",
"test": "react-scripts test",
"test:pipeline": "react-scripts test --watchAll=false --runInBand --detectOpenHandles --forceExit ",
"eject": "react-scripts eject"
},
"browserslist": {
+49 -41
View File
@@ -1,18 +1,17 @@
import { lazy, Suspense, useCallback, useEffect } from 'react';
import { Route, Switch } from 'react-router-dom';
import { Route, Routes, useLocation, useNavigate } from 'react-router-dom';
import './App.scss';
import { QueryClient, QueryClientProvider } from 'react-query';
import SocketProvider from 'app/context/socketContext';
import withSocket from 'features/viewers/ViewWrapper';
import ErrorBoundary from 'common/components/errorBoundary/ErrorBoundary';
import ProtectRoute from './common/components/protectRoute/ProtectRoute';
import { useFetch } from './app/hooks/useFetch';
import { ALIASES } from './app/api/apiConstants';
import { getAliases } from './app/api/ontimeApi';
const Editor = lazy(() => import('features/editors/Editor'));
const PresenterView = lazy(() =>
import('features/viewers/presenter/PresenterView')
);
const PresenterSimple = lazy(() =>
import('features/viewers/presenter/PresenterSimple')
);
const StageManager = lazy(() =>
import('features/viewers/backstage/StageManager')
);
@@ -21,18 +20,9 @@ const Lower = lazy(() =>
import('features/viewers/production/lower/LowerWrapper')
);
const Pip = lazy(() => import('features/viewers/production/Pip'));
const StudioClock = lazy(() => import('features/viewers/studio/StudioClock'));
const queryClient = new QueryClient();
// Seemed to cause issues
// broadcastQueryClient({
// queryClient,
// broadcastChannel: 'ontime',
// });
const SPresenter = withSocket(PresenterView);
const SPresenterSimple = withSocket(PresenterSimple);
const SStageManager = withSocket(StageManager);
const SPublic = withSocket(Public);
const SLowerThird = withSocket(Lower);
@@ -40,6 +30,10 @@ const SPip = withSocket(Pip);
const SStudio = withSocket(StudioClock);
function App() {
const { data } = useFetch(ALIASES, getAliases);
const location = useLocation();
const navigate = useNavigate();
// Handle keyboard shortcuts
const handleKeyPress = useCallback((e) => {
// check if the alt key is pressed
@@ -65,33 +59,47 @@ function App() {
};
}, [handleKeyPress]);
// navigate if is alias route
useEffect(() => {
if (data == null) return;
for (const d of data) {
if (`/${d.alias}` === location.pathname && d.enabled) {
navigate(`/${d.pathAndParams}`);
break;
}
}
}, [data, location, navigate]);
return (
<SocketProvider>
<QueryClientProvider client={queryClient}>
<div className='App'>
<ErrorBoundary>
<Suspense fallback={null}>
<Switch>
<Route exact path='/' component={SPresenter} />
<Route exact path='/sm' component={SStageManager} />
<Route exact path='/speaker' component={SPresenter} />
<Route exact path='/presenter' component={SPresenter} />
<Route exact path='/stage' component={SPresenter} />
<Route exact path='/presentersimple' component={SPresenterSimple} />
<Route exact path='/editor' component={Editor} />
<Route exact path='/public' component={SPublic} />
<Route exact path='/pip' component={SPip} />
<Route exact path='/studio' component={SStudio} />
{/* Lower cannot have fallback */}
<Route exact path='/lower' component={SLowerThird} />
{/* Send to default if nothing found */}
<Route component={SPresenter} />
</Switch>
</Suspense>
</ErrorBoundary>
</div>
</QueryClientProvider>
</SocketProvider>
<div className='App'>
<ErrorBoundary>
<Suspense fallback={null}>
<Routes>
<Route path='/' element={<SPresenter />} />
<Route path='/sm' element={<SStageManager />} />
<Route path='/speaker' element={<SPresenter />} />
<Route path='/presenter' element={<SPresenter />} />
<Route path='/stage' element={<SPresenter />} />
<Route path='/public' element={<SPublic />} />
<Route path='/pip' element={<SPip />} />
<Route path='/studio' element={<SStudio />} />
{/*/!* Lower cannot have fallback *!/*/}
<Route path='/lower' element={<SLowerThird />} />
{/*/!* Protected Routes *!/*/}
<Route
path='/editor'
element={
<ProtectRoute>
<Editor />
</ProtectRoute>
}
/>
{/* Send to default if nothing found */}
<Route path='*' element={<SPresenter />} />
</Routes>
</Suspense>
</ErrorBoundary>
</div>
);
}
-15
View File
@@ -1,15 +0,0 @@
$ontime-accent: #4bffabcc;
$ontime-pink: #ff7597;
$ontime-roll: #2b6cb0;
$notes-color: #d69e2e;
$header-gray: #ccc;
$label-gray: #aaa;
@mixin container-bg {
background-color: rgba(0, 0, 0, 0.13);
border-radius: 2px;
padding: 0 0.5em;
margin: 0 0.5em;
}
+2
View File
@@ -1,8 +1,10 @@
export const NODE_PORT = 4001;
export const EVENT_TABLE = 'event';
export const ALIASES = 'aliases';
export const EVENTS_TABLE = 'events';
export const APP_TABLE = 'appinfo';
export const OSC_SETTINGS = 'oscSettings';
export const APP_SETTINGS = 'appSettings';
const calculateServer = () => {
return window.location.origin.replace(window.location.port, `${NODE_PORT}/`);
+30
View File
@@ -9,6 +9,18 @@ export const ontimePlaceholderInfo = {
},
};
export const ontimePlaceholderSettings = {
pinCode: null,
};
export const eventPlaceholderSettings = {
title: '',
url: '',
publicInfo: '',
backstageInfo: '',
endMessage: '',
};
export const oscPlaceholderSettings = {
port: '',
portOut: '',
@@ -74,6 +86,15 @@ export const ontimeVars = [
},
];
export const getSettings = async () => {
const res = await axios.get(`${ontimeURL}/settings`);
return res.data;
};
export const postSettings = async (data) => {
return await axios.post(`${ontimeURL}/settings`, data);
};
export const getInfo = async () => {
const res = await axios.get(`${ontimeURL}/info`);
return res.data;
@@ -83,6 +104,15 @@ export const postInfo = async (data) => {
return await axios.post(`${ontimeURL}/info`, data);
};
export const getAliases = async () => {
const res = await axios.get(`${ontimeURL}/aliases`);
return res.data;
};
export const postAliases = async (data) => {
return await axios.post(`${ontimeURL}/aliases`, data);
};
export const getOSC = async () => {
const res = await axios.get(`${ontimeURL}/osc`);
return res.data;
+14
View File
@@ -0,0 +1,14 @@
// Exported viewer links
const speakerLink = 'http://localhost:4001/speaker';
const smLink = 'http://localhost:4001/sm';
const publicLink = 'http://localhost:4001/public';
const pipLink = 'http://localhost:4001/pip';
const studioLink = 'http://localhost:4001/studio';
export const viewerLinks = [
{ link: speakerLink, label: 'Speaker Screen' },
{ link: smLink, label: 'Backstage Screen' },
{ link: publicLink, label: 'Public Screen' },
{ link: pipLink, label: 'Picture in Picture' },
{ link: studioLink, label: 'Studio Clock' }
];
+37
View File
@@ -0,0 +1,37 @@
import { createContext, useCallback, useEffect, useState } from 'react';
import { useFetch } from '../hooks/useFetch';
import { APP_SETTINGS } from '../api/apiConstants';
import { getSettings } from '../api/ontimeApi';
export const AppContext = createContext({
auth: false,
data: {
pinCode: null
}
});
export const AppContextProvider = (props) => {
const [auth, setAuth] = useState(true);
const { data } = useFetch(APP_SETTINGS, getSettings);
useEffect(() => {
if (data == null) return;
if (data?.pinCode === null || data?.pinCode === '') {
setAuth(true);
} else {
setAuth(false);
}
},[data])
const validate = useCallback((pin) => {
const correct = pin === data.pinCode;
setAuth(correct);
return correct;
}, [data]);
return (
<AppContext.Provider value={{ auth, validate }}>
{props.children}
</AppContext.Provider>
);
};
+2 -2
View File
@@ -1,7 +1,7 @@
import { useSocket } from './socketContext';
import { createContext, useCallback, useEffect, useState } from 'react';
import { generateId } from 'ontime-server/utils/generate_id';
import { nowInMillis, stringFromMillis } from 'ontime-server/utils/time';
import { generateId } from 'ontime-utils/generate_id';
import { nowInMillis, stringFromMillis } from 'ontime-utils/time';
export const LoggingContext = createContext({
logData: [],
-7
View File
@@ -1,7 +0,0 @@
export const userConfig = {
timerColorOnPause: '#555',
timerColorOnRunning: '#FFF',
timerColorOnMessage: '#CCC',
timerColorOnTimeOver: '#F00',
overTimeText: '',
}
-2
View File
@@ -1,2 +0,0 @@
export const clamp = (num, a, b) =>
Math.max(Math.min(num, Math.max(a, b)), Math.min(a, b));
@@ -0,0 +1,27 @@
import { validateAlias } from '../aliases';
describe('An alias fails if incorrect', () => {
const testsToFail = [
// no empty
'',
// no https, http or www
'https://www.test.com',
'http://www.test.com',
'www.test.com',
// no hostname
'localhost/test',
'127.0.0.1/test',
'0.0.0.0/test',
// no editor
'editor',
'editor?test'
];
testsToFail.forEach((t) => (
test(`${t}`, () => {
expect(validateAlias(t).status).toBeFalsy();
})
)
);
});
@@ -0,0 +1,21 @@
import { clamp } from '../math';
test('Clamps a set of numbers correctly', () => {
const testCases = [
{ num: 10, min: 0, max: 20, result: 10 },
{ num: 0, min: 0, max: 20, result: 0 },
{ num: 20, min: 0, max: 20, result: 20 },
{ num: 20, min: 0, max: 20, result: 20 },
{ num: -20, min: 0, max: 20, result: 0 },
{ num: -0, min: 0, max: 20, result: 0 },
{ num: -50, min: -30, max: -20, result: -30 },
{ num: -50, min: 0, max: 0, result: 0 },
{ num: 50.5, min: 0, max: 100, result: 50.5 },
{ num: 50, min: 0, max: 20.32, result: 20.32 },
{ num: 10, min: 20.32, max: 40, result: 20.32 }
];
testCases.forEach((t) => (
expect(clamp(t.num, t.min, t.max)).toBe(t.result)
));
});
+29
View File
@@ -0,0 +1,29 @@
/**
* Validates an alias against defined parameters
* @param {string} alias
* @returns {{message: string, status: boolean}}
*/
export const validateAlias = (alias) => {
const valid = { status: true, message: 'ok' };
if (alias === '' || alias == null) {
// cannot be empty
valid.status = false;
valid.message = 'should not be empty';
} else if (alias.includes('http') || alias.includes('https') || alias.includes('www')) {
// cannot contain http, https or www
valid.status = false;
valid.message = 'should not include http, https, www';
} else if (alias.includes('127.0.0.1') || alias.includes('localhost') || alias.includes('0.0.0.0')) {
// aliases cannot contain hostname
valid.status = false;
valid.message = 'should not include hostname';
} else if (alias.includes('editor')) {
// no editor
valid.status = false;
valid.message = 'No aliases to editor page allowed';
}
return valid;
};
+9
View File
@@ -0,0 +1,9 @@
/**
* Clamps a value between a min and a max
* @param {number} num - Value to clamp
* @param {number} min - min value
* @param {number} max - max value
* @returns {number}
*/
export const clamp = (num, min, max) =>
Math.max(Math.min(num, Math.max(min, max)), Math.min(min, max));
@@ -1,5 +1,5 @@
import EditableTimer from 'common/input/EditableTimer';
import { stringFromMillis } from 'ontime-server/utils/time';
import { stringFromMillis } from 'ontime-utils/time';
import { useContext } from 'react';
import { LoggingContext } from '../../../app/context/LoggingContext';
@@ -1,4 +1,4 @@
import { clamp } from 'app/utils';
import { clamp } from 'app/utils/math';
import styles from './MyProgressBar.module.css';
export default function MyProgressBar(props) {
+1 -7
View File
@@ -1,5 +1,5 @@
import PropTypes from "prop-types";
import { Link, Redirect } from 'react-router-dom';
import { Link } from 'react-router-dom';
import { Image } from '@chakra-ui/react';
import { AnimatePresence, motion } from 'framer-motion';
import { useState, useEffect, useCallback } from 'react';
@@ -57,7 +57,6 @@ export default function NavLogo(props) {
to='/presenter'
className={style.navItem}
tabIndex={1}
onKeyDownCapture={() => <Redirect push to='/presenter' />}
>
Presenter
</Link>
@@ -65,7 +64,6 @@ export default function NavLogo(props) {
to='/sm'
className={style.navItem}
tabIndex={2}
onKeyDownCapture={() => <Redirect push to='/sm' />}
>
Backstage
</Link>
@@ -73,7 +71,6 @@ export default function NavLogo(props) {
to='/public'
className={style.navItem}
tabIndex={3}
onKeyDownCapture={() => <Redirect push to='/public' />}
>
Public
</Link>
@@ -81,7 +78,6 @@ export default function NavLogo(props) {
to='/lower'
className={style.navItem}
tabIndex={4}
onKeyDownCapture={() => <Redirect push to='/lower' />}
>
Lower Thirds
</Link>
@@ -89,7 +85,6 @@ export default function NavLogo(props) {
to='/pip'
className={style.navItem}
tabIndex={4}
onKeyDownCapture={() => <Redirect push to='/pip' />}
>
PIP
</Link>
@@ -97,7 +92,6 @@ export default function NavLogo(props) {
to='/studio'
className={style.navItem}
tabIndex={5}
onKeyDownCapture={() => <Redirect push to='/studio' />}
>
Studio Clock
</Link>
@@ -0,0 +1,67 @@
import PropTypes from 'prop-types';
import style from './ProtectRoute.module.scss';
import { PinInput, PinInputField } from '@chakra-ui/react';
import { IconButton } from '@chakra-ui/button';
import { FiCheck } from 'react-icons/fi';
import { AppContext } from '../../../app/context/AppContext';
import { useContext, useEffect, useState } from 'react';
export default function ProtectRoute(props) {
const isLocal = window.location.hostname === 'localhost' || window.location.hostname === '127.0.0.1';
const [pin, setPin] = useState('');
const [failed, setFailed] = useState(false);
const { auth, validate } = useContext(AppContext);
// Set window title
useEffect(() => {
document.title = 'ontime';
}, []);
const handleValidation = () => {
const r = validate(pin);
if (!r) {
setFailed(true);
}
}
return (
<>
{!isLocal && !auth ? (
<div className={style.container}>
ontime
<div className={failed ? style.pin__failed : style.pin}>
<PinInput
type='alphanumeric'
size='lg'
mask
onChange={(value) => {
setFailed(false);
setPin(value);
}}
>
<PinInputField />
<PinInputField />
<PinInputField />
<PinInputField />
</PinInput>
<IconButton
aria-label='Enter'
size='lg'
isRound
icon={<FiCheck />}
style={{ fontSize: '1.5em' }}
onClick={() => handleValidation()}
/>
</div>
</div>
) : (
props.children
)}
</>
);
}
ProtectRoute.propTypes = {
children: PropTypes.node.isRequired
};
@@ -0,0 +1,47 @@
@use '../../../styles/main' as *;
@use '../../../styles/mixins' as *;
.container {
background: #222;
display: grid;
place-content: center;
height: 100vh;
padding-bottom: 30vh;
color: $ontime-pink;
font-family: 'Open Sans', sans-serif;
font-weight: 200;
text-align: center;
font-size: 3vw;
}
.pin,
.pin__failed {
display: flex;
gap: 10px;
padding: 20px;
input {
border-radius: 50%;
}
button {
margin-left: 20px;
}
}
.pin__failed {
input {
animation: colourFade 1.5s ease;
}
}
@keyframes colourFade {
from {
background: $ontime-pink;
}
to {
background: rgba($ontime-pink, 0);
}
}
@@ -1,4 +1,4 @@
import { stringFromMillis } from 'ontime-server/utils/time';
import { stringFromMillis } from 'ontime-utils/time';
import style from './Paginator.module.css';
export default function TodayItem(props) {
const { selected, timeStart, timeEnd, title, backstageEvent } = props;
+1 -1
View File
@@ -4,7 +4,7 @@ import {
isTimeString,
timeStringToMillis,
} from '../utils/dateConfig';
import { stringFromMillis } from 'ontime-server/utils/time';
import { stringFromMillis } from 'ontime-utils/time';
import style from './EditableTimer.module.css';
import { LoggingContext } from '../../app/context/LoggingContext';
+1 -1
View File
@@ -1,4 +1,4 @@
import { stringFromMillis } from 'ontime-server/utils/time';
import { stringFromMillis } from 'ontime-utils/time';
/**
* @description From a list of events, returns only events of type event with calculated delays
+13
View File
@@ -0,0 +1,13 @@
/**
* Handles link to external URLs: specifically for a electron / browser case
* If electron: ask main process to call a new browser window
* If browser: open in new tab
* @param url
*/
export default function handleLink(url) {
if (window.process?.type === 'renderer') {
window.ipcRenderer.send('send-to-link', url);
} else {
window.open(url);
}
}
@@ -1,6 +1,6 @@
import style from './PlaybackControl.module.scss';
import Countdown from 'common/components/countdown/Countdown';
import { stringFromMillis } from 'ontime-server/utils/time';
import { stringFromMillis } from 'ontime-utils/time';
import {Tooltip} from '@chakra-ui/react';
import {Button} from '@chakra-ui/button';
import {memo} from 'react';
+4 -2
View File
@@ -24,12 +24,14 @@ export default function Editor() {
return (
<LoggingProvider>
<ModalManager isOpen={isOpen} onClose={onClose} />
<ErrorBoundary>
<ModalManager isOpen={isOpen} onClose={onClose} />
</ErrorBoundary>
<div className={styles.mainContainer}>
<Box id='settings' className={styles.settings}>
<ErrorBoundary>
<MenuBar onOpen={onOpen} />
<MenuBar onOpen={onOpen} isOpen={isOpen} />
</ErrorBoundary>
</Box>
+2 -1
View File
@@ -1,4 +1,5 @@
@use '../../main' as *;
@use '../../styles/main' as *;
@use '../../styles/mixins' as *;
@mixin container {
margin-top: 1em;
@@ -1,5 +1,6 @@
@use 'Info.module' as *;
@use '../../main' as *;
@use '../../styles/main' as *;
@use '../../styles/mixins' as *;
.container,
.container__expanded{
+1 -8
View File
@@ -4,6 +4,7 @@ import { getInfo, ontimePlaceholderInfo } from 'app/api/ontimeApi';
import { useFetch } from 'app/hooks/useFetch';
import style from './Info.module.scss';
import CollapseBar from '../../common/components/collapseBar/CollapseBar';
import handleLink from '../../common/utils/handleLink';
export default function InfoNif() {
const { data, status } = useFetch(APP_TABLE, getInfo, {
@@ -12,14 +13,6 @@ export default function InfoNif() {
const [collapsed, setCollapsed] = useState(false);
const baseURL = 'http://__IP__:4001';
const handleLink = (url) => {
if (window.process?.type === 'renderer') {
window.ipcRenderer.send('send-to-link', url);
} else {
window.open(url);
}
};
return (
<div className={style.container}>
<CollapseBar title={'Network Info'} isCollapsed={collapsed} onClick={() => setCollapsed((c) => !c)}/>
+21 -8
View File
@@ -6,14 +6,15 @@ import SettingsIconBtn from './buttons/SettingsIconBtn';
import MaxIconBtn from './buttons/MaxIconBtn';
import MinIconBtn from './buttons/MinIconBtn';
import QuitIconBtn from './buttons/QuitIconBtn';
import style from './MenuBar.module.css';
import style from './MenuBar.module.scss';
import HelpIconBtn from './buttons/HelpIconBtn';
import UploadIconBtn from './buttons/UploadIconBtn';
import { useContext, useRef } from 'react';
import { LoggingContext } from '../../app/context/LoggingContext';
import PropTypes from 'prop-types';
export default function MenuBar(props) {
const { onOpen } = props;
const { isOpen, onOpen } = props;
const { emitError } = useContext(LoggingContext);
const hiddenFileInput = useRef(null);
const queryClient = useQueryClient();
@@ -33,6 +34,10 @@ export default function MenuBar(props) {
}
};
const buttonStyle = {
fontSize: '1.5em'
};
const handleUpload = (event) => {
const fileUploaded = event.target.files[0];
if (fileUploaded == null) return;
@@ -93,25 +98,27 @@ export default function MenuBar(props) {
<>
<QuitIconBtn size='lg' clickhandler={() => handleIPC('shutdown')} />
<MaxIconBtn
style={{ fontSize: '1.5em' }}
style={{ ...buttonStyle }}
size='lg'
clickhandler={() => handleIPC('max')}
/>
<MinIconBtn
style={{ fontSize: '1.5em' }}
style={{ ...buttonStyle }}
size='lg'
clickhandler={() => handleIPC('min')}
/>
<div className={style.gap} />
<HelpIconBtn
style={{ fontSize: '1.5em' }}
style={{ ...buttonStyle }}
size='lg'
clickhandler={() => handleIPC('help')}
/>
<SettingsIconBtn
style={{ fontSize: '1.5em' }}
style={{...buttonStyle}}
size='lg'
className={isOpen ? style.open : ''}
clickhandler={onOpen}
isRound
/>
<div className={style.gap} />
<input
@@ -122,15 +129,21 @@ export default function MenuBar(props) {
accept='.json, .xlsx'
/>
<UploadIconBtn
style={{ fontSize: '1.5em' }}
style={{ ...buttonStyle }}
size='lg'
clickhandler={handleClick}
/>
<DownloadIconBtn
style={{ fontSize: '1.5em' }}
style={{ ...buttonStyle }}
size='lg'
clickhandler={handleDownload}
/>
</>
);
}
MenuBar.propTypes = {
isOpen: PropTypes.bool.isRequired,
onOpen: PropTypes.func.isRequired,
};
@@ -1,3 +0,0 @@
.gap {
height: 1em;
}
@@ -0,0 +1,9 @@
@use '../../styles/main' as *;
.gap {
height: 1em;
}
.open {
background: $light-bg;
}
+294 -147
View File
@@ -1,174 +1,321 @@
import { Button, IconButton } from '@chakra-ui/button';
import { FiPlus, FiMinus } from 'react-icons/fi';
import { FiInfo, FiMinus, FiSun } from 'react-icons/fi';
import { ModalBody } from '@chakra-ui/modal';
import { Input } from '@chakra-ui/react';
import { fetchEvent } from 'app/api/eventApi';
import { useState } from 'react';
import { getAliases, postAliases } from '../../app/api/ontimeApi';
import { useContext, useEffect, useState } from 'react';
import { useFetch } from 'app/hooks/useFetch';
import { EVENT_TABLE } from 'app/api/apiConstants';
import { ALIASES } from 'app/api/apiConstants';
import style from './Modals.module.scss';
import { viewerLinks } from '../../app/appConstants';
import { LoggingContext } from '../../app/context/LoggingContext';
import { validateAlias } from '../../app/utils/aliases';
import { Tooltip } from '@chakra-ui/tooltip';
import SubmitContainer from './SubmitContainer';
import handleLink from '../../common/utils/handleLink';
export default function AliasesModal() {
const { data, status, isError } = useFetch(EVENT_TABLE, fetchEvent);
const { data, status, refetch } = useFetch(ALIASES, getAliases);
const { emitError } = useContext(LoggingContext);
const [changed, setChanged] = useState(false);
const [submitting, setSubmitting] = useState(false);
const [aliases, setAliases] = useState([]);
const host = window.location.host;
/**
* Set formdata from server state
*/
useEffect(() => {
if (data == null) return;
if (changed) return;
setAliases([...data]);
}, [changed, data]);
/**
* Validate and submit data
*/
const submitHandler = async (event) => {
event.preventDefault();
// NOTHING HERE YET
setSubmitting(true);
const validatedAliases = [...aliases];
let errors = false;
for (const alias of validatedAliases) {
// validate url
const isURLValid = validateAlias(alias.pathAndParams);
if (!isURLValid.status) {
alias.urlError = isURLValid.message;
errors = true;
} else {
alias.urlError = undefined;
}
// validate alias
const isAliasValid = validateAlias(alias.alias);
if (!isAliasValid.status) {
alias.aliasError = isAliasValid.message;
errors = true;
} else {
alias.aliasError = undefined;
}
}
setAliases(validatedAliases);
if (!errors) {
await postAliases(aliases);
await refetch();
setChanged(false);
}
setSubmitting(false);
};
// Hardcoded links for now
// it will need dynamic PORT assignment
const speakerLink = 'http://localhost:4001/speaker';
const smLink = 'http://localhost:4001/sm';
const publicLink = 'http://localhost:4001/public';
const pipLink = 'http://localhost:4001/pip';
const studioLink = 'http://localhost:4001/studio';
/**
* Creates a new alias in state with a temporary id
*/
const addNew = () => {
if (aliases.length > 20) {
emitError('Maximum amount of aliases reacted (20)');
return;
}
const emptyAlias = {
id: Math.floor(Math.random() * 1000),
enabled: false,
alias: '',
pathAndParams: '',
};
setAliases((prevState) => [...prevState, emptyAlias]);
setChanged(true);
};
/**
* Deletes an alias by a given id
* @param {string} id - id of alias to delete
*/
const deleteAlias = (id) => {
setAliases((prevState) => [...prevState.filter((a) => a.id !== id)]);
setChanged(true);
};
/**
* Sets enabled flag to true / false
* @param {string} id - object id
* @param {boolean} isEnabled - whether to enable / disable flag
*/
const setEnabled = (id, isEnabled) => {
const aliasesState = [...aliases];
for (const a of aliasesState) {
if (a.id === id) {
if (isEnabled) {
if (a.alias === '' || a.pathAndParams === '') {
emitError('Alias incomplete');
break;
}
const isRepeated = aliases.some(
(r) => a.alias === r.alias && r.enabled
);
if (isRepeated) {
emitError('There is already an alias with this name');
break;
}
}
a.enabled = isEnabled;
break;
}
}
setChanged(true);
setAliases(aliasesState);
};
/**
* Reverts local state equals to server state
*/
const revert = async () => {
setChanged(false);
await refetch();
};
/**
* Handles change of input field in local state
* @param {number} index - index of item in array
* @param {string} field - object parameter to update
* @param {string} value - new object parameter value
*/
const handleChange = (index, field, value) => {
const temp = [...aliases];
temp[index][field] = value;
setAliases(temp);
setChanged(true);
};
return (
<>
<ModalBody className={style.modalBody}>
<p className={style.notes}>
Configure easy to use URL Aliases
<br />
🔥 Changes take effect on save 🔥
</p>
<form onSubmit={submitHandler}>
<ModalBody className={style.modalBody}>
<p className={style.notes}>
Configure easy to use URL Aliases
<div className={style.modalFields}>
<div className={style.hSeparator}>Default URLs</div>
<div className={style.blockNotes}>
{viewerLinks.map((l) => (
<a
href={l.link}
target='_blank'
rel='noreferrer'
className={style.flexNote}
key={l.link}
onClick={() => handleLink(`${host}/${l.link}`)}
>
{`${l.label} - ${l.link}`}
</a>
))}
</div>
<div className={style.hSeparator}>Custom Aliases</div>
<div className={style.blockNotes}>
<span className={style.inlineFlex}>
<FiInfo color='#2b6cb0' fontSize={'2em'} />
URL aliases are useful in two main scenarios
</span>
<span className={style.labelNote}>Complicated URLs</span>
<br />
Feature is not yet implemented
</p>
<span>Default URLs</span>
<div className={style.highNotes}>
<p className={style.flexNote}>
Presenter Screen <br />
<a
href={speakerLink}
target='_blank'
rel='noreferrer'
className={style.label}
>
{speakerLink}
</a>
</p>
<p className={style.flexNote}>
Backstage / Stage Manager Screen <br />
<a
href={smLink}
target='_blank'
rel='noreferrer'
className={style.label}
>
{smLink}
</a>
</p>
<p className={style.flexNote}>
Public / Foyer Screen <br />
<a
href={publicLink}
target='_blank'
rel='noreferrer'
className={style.label}
>
{publicLink}
</a>
</p>
<p className={style.flexNote}>
Picture in Picture Screen <br />
<a
href={pipLink}
target='_blank'
rel='noreferrer'
className={style.label}
>
{pipLink}
</a>
</p>
<p className={style.flexNote}>
Studio Clock<br />
<a
href={studioLink}
target='_blank'
rel='noreferrer'
className={style.label}
>
{studioLink}
</a>
</p>
eg. a lower third url with some custom parameters
<table>
<tbody>
<tr>
<td className={style.labelNote} style={{ width: '30%' }}>
Alias
</td>
<td className={style.labelNote}>Page URL</td>
</tr>
<tr>
<td>mylower</td>
<td>lower?bg=ff2&text=f00&size=0.6&transition=5</td>
</tr>
</tbody>
</table>
<br />
<span className={style.labelNote}>
URLs to be changed dynamically
</span>
<br />
eg. an unattended screen that you would need to change route from
the app
<table>
<tbody>
<tr>
<td className={style.labelNote} style={{ width: '30%' }}>
Alias
</td>
<td className={style.labelNote}>Page URL</td>
</tr>
<tr>
<td>thirdfloor</td>
<td>public</td>
</tr>
</tbody>
</table>
</div>
<div
className={style.inlineAliasPlaceholder}
style={{ padding: '0.5em 0' }}
>
<span className={style.labelNote}>Alias</span>
<span className={style.labelNote}>Page URL</span>
</div>
{aliases.map((alias, index) => (
<div key={alias.id}>
<div className={style.inlineAlias}>
<Input
size='sm'
variant='flushed'
name='Alias'
placeholder='URL Alias'
autoComplete='off'
value={alias.alias}
isInvalid={alias.aliasError}
onChange={(event) =>
handleChange(index, 'alias', event.target.value)
}
/>
<Input
size='sm'
fontSize={'0.75em'}
variant='flushed'
name='URL'
placeholder='URL (portion after ontime Port)'
autoComplete='off'
value={alias.pathAndParams}
isInvalid={alias.urlError}
onChange={(event) =>
handleChange(index, 'pathAndParams', event.target.value)
}
/>
<Tooltip label={`Test /${alias.pathAndParams}`} openDelay={500}>
<a
href='#!'
target='_blank'
rel='noreferrer'
onClick={(e) => {
e.preventDefault();
handleLink(`http://${host}/${alias.pathAndParams}`);
}}
/>
</Tooltip>
<Tooltip label='Enable alias' openDelay={500}>
<IconButton
size='xs'
icon={<FiSun />}
colorScheme='blue'
variant={alias.enabled ? null : 'outline'}
onClick={() => setEnabled(alias.id, !alias.enabled)}
/>
</Tooltip>
<Tooltip label='Delete alias' openDelay={500}>
<IconButton
size='xs'
icon={<FiMinus />}
colorScheme='red'
onClick={() => deleteAlias(alias.id)}
/>
</Tooltip>
</div>
{alias.aliasError ? (
<div
className={style.error}
>{`Alias error: ${alias.aliasError}`}</div>
) : null}
{alias.urlError ? (
<div
className={style.error}
>{`URL error: ${alias.urlError}`}</div>
) : null}
</div>
))}
<span>Manage custom aliases</span>
<div className={style.modalInline}>
<Input
size='sm'
name='URL'
placeholder='A long URL'
autoComplete='off'
value={'A long URL'}
onChange={(event) => {
// Nothing here yet
}}
isDisabled={true}
/>
<Input
size='sm'
name='Alias'
placeholder='A nice alias'
autoComplete='off'
value={'A nice alias'}
onChange={(event) => {
// Nothing here yet
}}
isDisabled={true}
/>
<IconButton
size='sm'
icon={<FiMinus />}
colorScheme='red'
disabled
/>
</div>
<div className={style.separator} />
<div className={style.modalInline}>
<Input
size='sm'
name='URL'
placeholder='URL'
autoComplete='off'
value={'URL'}
onChange={(event) => {
// Nothing here yet
}}
isDisabled={true}
/>
<Input
size='sm'
name='Alias'
placeholder='Alias'
autoComplete='off'
value={'Alias'}
onChange={(event) => {
// Nothing here yet
}}
isDisabled={true}
/>
<IconButton
size='sm'
icon={<FiPlus />}
colorScheme='blue'
disabled
/>
</div>
<div className={style.submitContainer}>
<div
className={style.inlineAliasPlaceholder}
style={{ padding: '0.5em 0' }}
>
<Button
size='xs'
colorScheme='blue'
type='submit'
isLoading={submitting}
disabled={true}
variant='outline'
onClick={() => addNew()}
>
Save
Add new
</Button>
</div>
</ModalBody>
</div>
<SubmitContainer
revert={revert}
submitting={submitting}
changed={changed}
status={status}
/>
</form>
</>
</ModalBody>
);
}
+131 -144
View File
@@ -1,183 +1,170 @@
import { ModalBody } from '@chakra-ui/modal';
import { FormLabel, FormControl, Input, Button } from '@chakra-ui/react';
import { getOSC, oscPlaceholderSettings, postOSC } from 'app/api/ontimeApi';
import {
FormControl,
FormLabel,
Input,
PinInput,
PinInputField,
} from '@chakra-ui/react';
import {
getSettings,
ontimePlaceholderSettings,
postSettings,
} from 'app/api/ontimeApi';
import { useContext, useEffect, useState } from 'react';
import { useFetch } from 'app/hooks/useFetch';
import { OSC_SETTINGS } from 'app/api/apiConstants';
import { APP_SETTINGS } from 'app/api/apiConstants';
import style from './Modals.module.scss';
import { LoggingContext } from '../../app/context/LoggingContext';
import { IconButton } from '@chakra-ui/button';
import { FiEye } from 'react-icons/fi';
import SubmitContainer from './SubmitContainer';
import { inputProps } from './modalHelper';
export default function AppSettingsModal() {
const { data, status } = useFetch(OSC_SETTINGS, getOSC);
const { emitError } = useContext(LoggingContext);
const [formData, setFormData] = useState(oscPlaceholderSettings);
const { data, status, refetch } = useFetch(APP_SETTINGS, getSettings);
const { emitError, emitWarning } = useContext(LoggingContext);
const [formData, setFormData] = useState(ontimePlaceholderSettings);
const [changed, setChanged] = useState(false);
const [submitting, setSubmitting] = useState(false);
const [hidePin, setHidePin] = useState(true);
/**
* Set formdata from server state
*/
useEffect(() => {
if (data == null) return;
setFormData({ ...data });
}, [data]);
if (changed) return;
setFormData({
pinCode: data.pinCode,
});
}, [changed, data]);
/**
* Validate and submit data
*/
const submitHandler = async (event) => {
event.preventDefault();
setSubmitting(true);
const f = formData;
let e = { status: false, message: '' };
// Validate fields
if (f.port < 1024 || f.port > 65535) {
// Port in incorrect range
if (f.pinCode === '' || f.pinCode == null) {
e.status = true;
e.message += 'OSC IN Port in incorrect range (1024 - 65535)';
} else if (f.portOut < 1024 || f.portOut > 65535) {
// Port in incorrect range
e.message += 'App pin code removed';
} else {
e.status = true;
e.message += 'OSC OUT Port in incorrect range (1024 - 65535)';
} else if (f.port === f.portOut) {
// Cant use the same port
e.status = true;
e.message += 'OSC IN and OUT Ports cant be the same';
e.message += 'App pin code added';
}
// set fields with error
if (e.status) {
if (!e.status) {
emitError(`Invalid Input: ${e.message}`);
return;
} else {
await postSettings(formData);
await refetch();
emitWarning(e.message);
setChanged(false);
}
// Post here
postOSC(formData);
setChanged(false);
setSubmitting(false);
};
return (
<>
<form onSubmit={submitHandler}>
<ModalBody className={style.modalBody}>
{status === 'success' && (
<>
<p className={style.notes}>
Options related to the application
<br />
🔥 Changes take effect after app restart 🔥
</p>
/**
* Reverts local state equals to server state
*/
const revert = async () => {
setChanged(false);
await refetch();
};
<FormControl id='serverPort'>
<FormLabel htmlFor='serverPort'>
Viewer Port
<span className={style.notes}>Port to access viewers</span>
</FormLabel>
<Input
/**
* Handles change of input field in local state
* @param {string} field - object parameter to update
* @param {string} value - new object parameter value
*/
const handleChange = (field, value) => {
const temp = { ...formData };
temp[field] = value;
setFormData(temp);
setChanged(true);
};
const disableModal = status !== 'success';
return (
<ModalBody className={style.modalBody}>
<p className={style.notes}>
Options related to the application
<br />
🔥 Changes take effect after app restart 🔥
</p>
<form onSubmit={submitHandler}>
<div className={style.modalFields}>
<div className={style.hSeparator}>General App Settings</div>
<div className={style.modalInline}>
<FormControl id='serverPort'>
<FormLabel htmlFor='serverPort'>
Viewer Port
<span className={style.labelNote}>
<br />
Ontime is available at port
</span>
</FormLabel>
<Input
{...inputProps}
name='title'
value={4001}
disabled
style={{ width: '6em', textAlign: 'center' }}
/>
</FormControl>
<FormControl id='editorPin'>
<FormLabel htmlFor='editorPin'>
Editor Pincode
<span className={style.labelNote}>
<br />
Protect the editor with a Pincode
</span>
</FormLabel>
<div className={style.pin}>
<PinInput
{...inputProps}
type='alphanumeric'
defaultValue=''
value={formData.pinCode}
mask={hidePin}
isDisabled={disableModal}
onChange={(value) => handleChange('pinCode', value)}
>
<PinInputField />
<PinInputField />
<PinInputField />
<PinInputField />
</PinInput>
<IconButton
size='sm'
name='title'
placeholder='4001'
autoComplete='off'
value={4001}
readOnly
style={{ width: '6em', textAlign: 'center' }}
colorScheme='blue'
variant='ghost'
icon={<FiEye />}
aria-label='Editor pin code'
onMouseDown={() => setHidePin(false)}
onMouseUp={() => setHidePin(true)}
isDisabled={disableModal}
/>
<span className={style.notes}>(Read Only Value)</span>
</FormControl>
<FormControl id='port'>
<FormLabel htmlFor='port'>
OSC In Port
<span className={style.notes}>
<br />
App Control - Default 8888
</span>
</FormLabel>
<Input
size='sm'
name='port'
placeholder='8888'
autoComplete='off'
type='number'
value={formData.port}
min='1024'
max='65535'
onChange={(event) => {
setChanged(true);
setFormData({
...formData,
port: parseInt(event.target.value),
});
}}
isDisabled={submitting}
style={{ width: '6em', textAlign: 'center' }}
/>
</FormControl>
<div className={style.modalInline}>
<FormControl id='targetIP' width='auto'>
<FormLabel htmlFor='targetIP'>
OSC Out Target IP
<span className={style.notes}>
<br />
App Feedback - Default 127.0.0.1
</span>
</FormLabel>
<Input
size='sm'
name='targetIP'
placeholder='127.0.0.1'
autoComplete='off'
value={formData.targetIP}
onChange={(event) => {
setChanged(true);
setFormData({
...formData,
targetIP: event.target.value,
});
}}
isDisabled={submitting}
style={{ width: '12em', textAlign: 'right' }}
/>
</FormControl>
<FormControl id='portOut' width='auto'>
<FormLabel htmlFor='portOut'>
OSC Out Port
<span className={style.notes}>
<br />
Default 9999
</span>
</FormLabel>
<Input
size='sm'
name='portOut'
placeholder='9999'
autoComplete='off'
type='number'
value={formData.portOut}
min='1024'
max='65535'
onChange={(event) => {
setChanged(true);
setFormData({
...formData,
portOut: parseInt(event.target.value),
});
}}
isDisabled={submitting}
style={{ width: '6em', textAlign: 'left' }}
/>
</FormControl>
</div>
</>
)}
<div className={style.submitContainer}>
<Button
colorScheme='blue'
type='submit'
isLoading={submitting}
disabled={!changed}
>
Save
</Button>
</FormControl>
</div>
</ModalBody>
</div>
<SubmitContainer
revert={revert}
submitting={submitting}
changed={changed}
status={status}
/>
</form>
</>
</ModalBody>
);
}
+133 -140
View File
@@ -1,31 +1,26 @@
import { ModalBody } from '@chakra-ui/modal';
import {
FormLabel,
FormControl,
Input,
Button,
Textarea,
} from '@chakra-ui/react';
import { FormLabel, Input, Textarea } from '@chakra-ui/react';
import { fetchEvent, postEvent } from 'app/api/eventApi';
import { useEffect, useState } from 'react';
import { useFetch } from 'app/hooks/useFetch';
import { EVENT_TABLE } from 'app/api/apiConstants';
import style from './Modals.module.scss';
import { eventPlaceholderSettings } from '../../app/api/ontimeApi';
import SubmitContainer from './SubmitContainer';
import { inputProps } from './modalHelper';
export default function SettingsModal() {
const { data, status } = useFetch(EVENT_TABLE, fetchEvent);
const [formData, setFormData] = useState({
title: '',
url: '',
publicInfo: '',
backstageInfo: '',
endMessage: '',
});
const { data, status, refetch } = useFetch(EVENT_TABLE, fetchEvent);
const [formData, setFormData] = useState(eventPlaceholderSettings);
const [changed, setChanged] = useState(false);
const [submitting, setSubmitting] = useState(false);
/**
* Set formdata from server state
*/
useEffect(() => {
if (data == null) return;
if (changed) return;
setFormData({
title: data.title,
@@ -34,8 +29,11 @@ export default function SettingsModal() {
backstageInfo: data.backstageInfo,
endMessage: data.endMessage,
});
}, [data]);
}, [changed, data]);
/**
* Validate and submit data
*/
const submitHandler = async (event) => {
event.preventDefault();
setSubmitting(true);
@@ -46,133 +44,128 @@ export default function SettingsModal() {
setSubmitting(false);
};
/**
* Reverts local state equals to server state
*/
const revert = async () => {
setChanged(false);
await refetch();
};
/**
* Handles change of input field in local state
* @param {string} field - object parameter to update
* @param {string} value - new object parameter value
*/
const handleChange = (field, value) => {
const temp = { ...formData };
temp[field] = value;
setFormData(temp);
setChanged(true);
};
return (
<>
<ModalBody className={style.modalBody}>
<p className={style.notes}>
Options related to the running event
<br />
Affects rendered views
</p>
<form onSubmit={submitHandler}>
<ModalBody className={style.modalBody}>
{status === 'success' && (
<>
<p className={style.notes}>
Options related to the running event
<br />
Affect rendered views
</p>
<FormControl id='title'>
<FormLabel htmlFor='title'>Event Title</FormLabel>
<Input
size='sm'
maxLength={35}
name='title'
placeholder='Event Title'
autoComplete='off'
value={formData.title}
onChange={(event) => {
setChanged(true);
setFormData({ ...formData, title: event.target.value });
}}
isDisabled={submitting}
/>
</FormControl>
<FormControl id='url'>
<FormLabel htmlFor='url'>
Event URL
<span className={style.notes}>
(shown as a QR code in some views)
</span>
</FormLabel>
<Input
size='sm'
name='url'
placeholder='www.onsite.no'
autoComplete='off'
value={formData.url}
onChange={(event) => {
setChanged(true);
setFormData({ ...formData, url: event.target.value });
}}
isDisabled={submitting}
/>
</FormControl>
<FormControl id='pubInfo'>
<FormLabel htmlFor='pubInfo'>Public Info</FormLabel>
<Textarea
size='sm'
name='pubInfo'
placeholder='Information to be shown on public screens'
autoComplete='off'
value={formData.publicInfo}
onChange={(event) => {
setChanged(true);
setFormData({
...formData,
publicInfo: event.target.value,
});
}}
isDisabled={submitting}
/>
</FormControl>
<FormControl id='backstageInfo'>
<FormLabel htmlFor='backstageInfo'>Backstage Info</FormLabel>
<Textarea
size='sm'
name='backstageInfo'
placeholder='Information to be shown on backstage screens'
autoComplete='off'
resize={false}
value={formData.backstageInfo}
onChange={(event) => {
setChanged(true);
setFormData({
...formData,
backstageInfo: event.target.value,
});
}}
isDisabled={submitting}
/>
</FormControl>
<FormControl id='endMessage'>
<FormLabel htmlFor='endMessage'>
End Message
<span className={style.notes}>
Shown on presenter view when time is finished
</span>
</FormLabel>
<Input
size='sm'
maxLength={30}
name='endMessage'
placeholder='Empty message shows elapsed time'
autoComplete='off'
value={formData.endMessage}
onChange={(event) => {
setChanged(true);
setFormData({
...formData,
endMessage: event.target.value,
});
}}
isDisabled={submitting}
/>
</FormControl>
</>
)}
<div className={style.submitContainer}>
<Button
colorScheme='blue'
type='submit'
isLoading={submitting}
disabled={!changed}
>
Save
</Button>
<div className={style.modalFields}>
<div className={style.hSeparator}>Event Data</div>
<div className={style.spacedEntry}>
<FormLabel htmlFor='title'>Event Title</FormLabel>
<Input
{...inputProps}
maxLength={35}
name='title'
placeholder='Event Title'
value={formData.title}
onChange={(event) => handleChange('title', event.target.value)}
/>
</div>
</ModalBody>
<div className={style.hSeparator}>Additional Screen Info</div>
<div className={style.spacedEntry}>
<FormLabel htmlFor='url'>
Event URL
<span className={style.labelNote}>
<br />
Shown as a QR code in some views
</span>
</FormLabel>
<Input
{...inputProps}
name='url'
placeholder='www.onsite.no'
value={formData.url}
onChange={(event) => handleChange('url', event.target.value)}
/>
</div>
<div className={style.spacedEntry}>
<FormLabel htmlFor='pubInfo'>
Public Info
<span className={style.labelNote}>
<br />
Information to be shown on public screens
</span>
</FormLabel>
<Textarea
{...inputProps}
name='pubInfo'
placeholder='Information to be shown on public screens'
value={formData.publicInfo}
onChange={(event) =>
handleChange('publicInfo', event.target.value)
}
/>
</div>
<div className={style.spacedEntry}>
<FormLabel htmlFor='backstageInfo'>
Backstage Info
<span className={style.labelNote}>
<br />
Information to be shown on backstage screens
</span>
</FormLabel>
<Textarea
{...inputProps}
name='backstageInfo'
placeholder='Information to be shown on backstage screens'
resize={false}
value={formData.backstageInfo}
onChange={(event) =>
handleChange('backstageInfo', event.target.value)
}
/>
</div>
<div className={style.spacedEntry}>
<FormLabel htmlFor='endMessage'>
End Message
<span className={style.labelNote}>
<br />
Shown on presenter view when time is finished
</span>
</FormLabel>
<Input
{...inputProps}
maxLength={30}
name='endMessage'
placeholder='Empty message shows elapsed time'
value={formData.endMessage}
onChange={(event) =>
handleChange('endMessage', event.target.value)
}
/>
</div>
</div>
<SubmitContainer
revert={revert}
submitting={submitting}
changed={changed}
status={status}
/>
</form>
</>
</ModalBody>
);
}
@@ -1,11 +1,5 @@
import { ModalBody } from '@chakra-ui/modal';
import {
FormLabel,
FormControl,
Input,
Button,
Switch,
} from '@chakra-ui/react';
import { FormControl, FormLabel, Input, Switch } from '@chakra-ui/react';
import {
getInfo,
httpPlaceholder,
@@ -17,23 +11,23 @@ import { useFetch } from 'app/hooks/useFetch';
import { APP_TABLE } from 'app/api/apiConstants';
import style from './Modals.module.scss';
import { LoggingContext } from '../../app/context/LoggingContext';
import { FiInfo } from 'react-icons/fi';
import SubmitContainer from './SubmitContainer';
import { inputProps } from './modalHelper';
export default function IntegrationSettingsModal() {
const { data, status } = useFetch(APP_TABLE, getInfo);
const { data, status, refetch } = useFetch(APP_TABLE, getInfo);
const { emitError } = useContext(LoggingContext);
const [formData, setFormData] = useState(httpPlaceholder);
const [changed, setChanged] = useState(false);
const [submitting, setSubmitting] = useState(false);
const ready = status === 'success';
const integrationInputProps = {
size: 'sm',
autoComplete: 'off',
isDisabled: submitting || !ready,
};
/**
* Set formdata from server state
*/
useEffect(() => {
if (data == null) return;
if (changed) return;
setFormData({
onLoad: data?.onLoad,
@@ -42,8 +36,11 @@ export default function IntegrationSettingsModal() {
onPause: data?.onPause,
onStop: data?.onStop,
});
}, [data]);
}, [changed, data]);
/**
* Validate and submit data
*/
const submitHandler = async (event) => {
event.preventDefault();
@@ -54,283 +51,312 @@ export default function IntegrationSettingsModal() {
if (e.status) {
emitError(`Invalid Input: ${e.message}`);
return;
} else {
await postInfo(f);
setChanged(false);
}
// Post here
postInfo(f);
setChanged(false);
setSubmitting(false);
};
return (
<>
<form onSubmit={submitHandler}>
<ModalBody
className={ready ? style.modalBody : style.modalBodyDisabled}
>
<>
<p className={style.notes}>
Integrate with third party over an HTTP API
<br />
🔥 Changes take effect after app restart 🔥
</p>
<div className={style.highNotes}>
<p>
Add HTTP messages that ontime will send during the app lifecycle
</p>
<p>
You can use the variables below to pass data directly from
ontime eg:
<span className={style.emNote}>
http://127.0.0.1:8088/API/?setHeadline=<b>$title</b>
&setSub=<b>$presenter</b>
</span>
</p>
/**
* Reverts local state equals to server state
*/
const revert = async () => {
setChanged(false);
await refetch();
};
<table>
// Todo: make change handler
// Todo: toggle between GET / POST
// Todo: add test button
// Todo: enabled should be button
// Todo: add friendly placeholder to input
return (
<ModalBody className={style.modalBody}>
<p className={style.notes}>
Integrate with third party over an HTTP API
<br />
🔥 Changes take effect after app restart 🔥
</p>
<form onSubmit={submitHandler}>
<div className={style.modalFields}>
<div className={style.hSeparator}>Ontime event cycle</div>
<div className={style.blockNotes}>
<span className={style.inlineFlex}>
<FiInfo color='#2b6cb0' fontSize={'2em'} />
Add HTTP messages that ontime will send during the event cycle
</span>
<span className={style.labelNote}>
You can use variables in the HTTP request URL to send data from
ontime
</span>
<span className={style.emNote}>
http://127.0.0.1:8088/API/?setHeadline=
<span className={style.labelNoteInline}>$title</span>
&setSub=<span className={style.labelNoteInline}>$presenter</span>
</span>
<table>
<tbody>
<tr>
<td className={style.labelNote} style={{ width: '30%' }}>
Variable
</td>
<td className={style.labelNote}>Value</td>
</tr>
{ontimeVars.map((v) => (
<tr>
<td className={style.noteItem}>{v.name}</td>
<td className={style.labelNote}>{v.name}</td>
<td>{v.description}</td>
</tr>
))}
</table>
</div>
<>
<FormLabel>
On Load
<span className={style.notes}>When a new event loads</span>
</FormLabel>
<FormControl id='onLoad' className={style.modalInline}>
<Input
{...integrationInputProps}
name='onLoadURL'
value={formData?.onLoad?.url}
onChange={(event) => {
setChanged(true);
setFormData({
...formData,
onLoad: {
...formData.onLoad,
url: event.target.value,
},
});
}}
/>
<Switch
colorScheme='green'
id='onLoadEnable'
value={formData?.onLoad?.enabled}
onChange={(event) => {
setChanged(true);
setFormData({
...formData,
onLoad: {
...formData.onLoad,
enabled: event.target.value,
},
});
}}
/>
</FormControl>
<FormLabel>
On Start
<span className={style.notes}>
When an timer starts / resumes
</span>
</FormLabel>
<FormControl id='onStart' className={style.modalInline}>
<Input
{...integrationInputProps}
name='onStartURL'
value={formData?.onStart?.url}
onChange={(event) => {
setChanged(true);
setFormData({
...formData,
onStart: {
...formData.onStart,
url: event.target.value,
},
});
}}
/>
<Switch
colorScheme='green'
id='onStartEnable'
value={formData?.onStart?.enabled}
onChange={(event) => {
setChanged(true);
setFormData({
...formData,
onStart: {
...formData.onStart,
enabled: event.target.value,
},
});
}}
/>
</FormControl>
<FormLabel>
On Update
<span className={style.notes}>At every clock tick</span>
</FormLabel>
<FormControl id='onUpdate' className={style.modalInline}>
<Input
{...integrationInputProps}
name='onUpdateURL'
value={formData?.onUpdate?.url}
onChange={(event) => {
setChanged(true);
setFormData({
...formData,
onUpdate: {
...formData.onUpdate,
url: event.target.value,
},
});
}}
/>
<Switch
colorScheme='green'
id='onUpdateEnable'
value={formData?.onUpdate?.enabled}
onChange={(event) => {
setChanged(true);
setFormData({
...formData,
onUpdate: {
...formData.onUpdate,
enabled: event.target.value,
},
});
}}
/>
</FormControl>
<FormLabel>
On Pause
<span className={style.notes}>When a timer pauses</span>
</FormLabel>
<FormControl id='onPause' className={style.modalInline}>
<Input
{...integrationInputProps}
name='onPauseURL'
value={formData?.onPause?.url}
onChange={(event) => {
setChanged(true);
setFormData({
...formData,
onPause: {
...formData.onPause,
url: event.target.value,
},
});
}}
/>
<Switch
colorScheme='green'
id='onPauseEnable'
value={formData?.onPause?.enabled}
onChange={(event) => {
setChanged(true);
setFormData({
...formData,
onPause: {
...formData.onPause,
enabled: event.target.value,
},
});
}}
/>
</FormControl>
<FormLabel>
On Stop
<span className={style.notes}>When an event is unloaded</span>
</FormLabel>
<FormControl id='onStop' className={style.modalInline}>
<Input
{...integrationInputProps}
name='onStopURL'
value={formData?.onStop?.url}
onChange={(event) => {
setChanged(true);
setFormData({
...formData,
onStop: {
...formData.onStop,
url: event.target.value,
},
});
}}
/>
<Switch
colorScheme='green'
id='onStopEnable'
value={formData?.onStop?.enabled}
onChange={(event) => {
setChanged(true);
setFormData({
...formData,
onStop: {
...formData.onStop,
enabled: event.target.value,
},
});
}}
/>
</FormControl>
<FormLabel>
On Finish
<span className={style.notes}>When an event is finished</span>
</FormLabel>
<FormControl id='onFinish' className={style.modalInline}>
<Input
{...integrationInputProps}
name='onFinishURL'
value={formData?.onFinish?.url}
onChange={(event) => {
setChanged(true);
setFormData({
...formData,
onStop: {
...formData.onFinish,
url: event.target.value,
},
});
}}
/>
<Switch
colorScheme='green'
id='onFinishEnable'
value={formData?.onFinish?.enabled}
onChange={(event) => {
setChanged(true);
setFormData({
...formData,
onStop: {
...formData.onStop,
enabled: event.target.value,
},
});
}}
/>
</FormControl>
</>
</>
<div className={style.submitContainer}>
<Button
colorScheme='blue'
type='submit'
isLoading={submitting}
disabled={!changed || !ready}
>
Save
</Button>
</tbody>
</table>
</div>
</ModalBody>
<div className={style.hSeparator}>Send HTTP</div>
<FormLabel style={{paddingLeft:'0.5em'}}>
On Load
<span className={style.labelNote}>
<br />
When a new event loads
</span>
</FormLabel>
<div className={style.modalInline}>
<Input
{...inputProps}
name='onLoadURL'
value={formData?.onLoad?.url}
onChange={(event) => {
setChanged(true);
setFormData({
...formData,
onLoad: {
...formData.onLoad,
url: event.target.value,
},
});
}}
/>
<Switch
colorScheme='green'
id='onLoadEnable'
value={formData?.onLoad?.enabled}
onChange={(event) => {
setChanged(true);
setFormData({
...formData,
onLoad: {
...formData.onLoad,
enabled: event.target.value,
},
});
}}
/>
</div>
<FormLabel style={{paddingLeft:'0.5em'}}>
On Start
<span className={style.labelNote}>
<br />
When an timer starts / resumes{' '}
</span>
</FormLabel>
<div className={style.modalInline}>
<Input
{...inputProps}
name='onStartURL'
value={formData?.onStart?.url}
onChange={(event) => {
setChanged(true);
setFormData({
...formData,
onStart: {
...formData.onStart,
url: event.target.value,
},
});
}}
/>
<Switch
colorScheme='green'
id='onStartEnable'
value={formData?.onStart?.enabled}
onChange={(event) => {
setChanged(true);
setFormData({
...formData,
onStart: {
...formData.onStart,
enabled: event.target.value,
},
});
}}
/>
</div>
<FormLabel style={{paddingLeft:'0.5em'}}>
On Update
<span className={style.labelNote}>
<br />
At every clock tick
</span>
</FormLabel>
<FormControl id='onUpdate' className={style.modalInline}>
<Input
{...inputProps}
name='onUpdateURL'
value={formData?.onUpdate?.url}
onChange={(event) => {
setChanged(true);
setFormData({
...formData,
onUpdate: {
...formData.onUpdate,
url: event.target.value,
},
});
}}
/>
<Switch
colorScheme='green'
id='onUpdateEnable'
value={formData?.onUpdate?.enabled}
onChange={(event) => {
setChanged(true);
setFormData({
...formData,
onUpdate: {
...formData.onUpdate,
enabled: event.target.value,
},
});
}}
/>
</FormControl>
<FormLabel style={{paddingLeft:'0.5em'}}>
On Pause
<span className={style.labelNote}>
<br />
When a timer pauses
</span>
</FormLabel>
<FormControl id='onPause' className={style.modalInline}>
<Input
{...inputProps}
name='onPauseURL'
value={formData?.onPause?.url}
onChange={(event) => {
setChanged(true);
setFormData({
...formData,
onPause: {
...formData.onPause,
url: event.target.value,
},
});
}}
/>
<Switch
colorScheme='green'
id='onPauseEnable'
value={formData?.onPause?.enabled}
onChange={(event) => {
setChanged(true);
setFormData({
...formData,
onPause: {
...formData.onPause,
enabled: event.target.value,
},
});
}}
/>
</FormControl>
<FormLabel style={{paddingLeft:'0.5em'}}>
On Stop
<span className={style.labelNote}>
<br />
When an event is unloaded
</span>
</FormLabel>
<FormControl id='onStop' className={style.modalInline}>
<Input
{...inputProps}
name='onStopURL'
value={formData?.onStop?.url}
onChange={(event) => {
setChanged(true);
setFormData({
...formData,
onStop: {
...formData.onStop,
url: event.target.value,
},
});
}}
/>
<Switch
colorScheme='green'
id='onStopEnable'
value={formData?.onStop?.enabled}
onChange={(event) => {
setChanged(true);
setFormData({
...formData,
onStop: {
...formData.onStop,
enabled: event.target.value,
},
});
}}
/>
</FormControl>
<FormLabel style={{paddingLeft:'0.5em'}}>
On Finish
<span className={style.labelNote}>
<br />
When an event is finished
</span>
</FormLabel>
<FormControl id='onFinish' className={style.modalInline}>
<Input
{...inputProps}
name='onFinishURL'
value={formData?.onFinish?.url}
onChange={(event) => {
setChanged(true);
setFormData({
...formData,
onStop: {
...formData.onFinish,
url: event.target.value,
},
});
}}
/>
<Switch
colorScheme='green'
id='onFinishEnable'
value={formData?.onFinish?.enabled}
onChange={(event) => {
setChanged(true);
setFormData({
...formData,
onStop: {
...formData.onStop,
enabled: event.target.value,
},
});
}}
/>
</FormControl>
</div>
<SubmitContainer
revert={revert}
submitting={submitting}
changed={changed}
status={status}
/>
</form>
</>
</ModalBody>
);
}
+12 -6
View File
@@ -8,9 +8,10 @@ import {
} from '@chakra-ui/modal';
import { Tab, TabList, TabPanel, TabPanels, Tabs } from '@chakra-ui/tabs';
import EventSettingsModal from './EventSettingsModal';
import AppSettingsModal from './AppSettingsModal';
import OscSettingsModal from './OscSettingsModal';
import AliasesModal from './AliasesModal';
import IntegrationSettingsModal from './IntegrationSettingsModal';
import AppSettingsModal from './AppSettingsModal';
export default function ModalManager(props) {
const { isOpen, onClose } = props;
@@ -20,7 +21,8 @@ export default function ModalManager(props) {
onClose={onClose}
closeOnOverlayClick={false}
motionPreset={'slideInBottom'}
size='lg'
size='xl'
scrollBehavior='inside'
>
<ModalOverlay />
<ModalContent>
@@ -29,21 +31,25 @@ export default function ModalManager(props) {
<Tabs size='sm' isLazy>
<TabList>
<Tab style={{ fontSize: '0.9em' }}>App Settings</Tab>
<Tab style={{ fontSize: '0.9em' }}>Event Data</Tab>
<Tab style={{ fontSize: '0.9em' }}>Application Settings</Tab>
<Tab style={{ fontSize: '0.9em' }}>URL Aliases</Tab>
<Tab style={{ fontSize: '0.9em' }}>OSC</Tab>
{/*<Tab style={{ fontSize: '0.9em' }}>Integration</Tab>*/}
</TabList>
<TabPanels>
<TabPanel>
<EventSettingsModal />
</TabPanel>
<TabPanel>
<AppSettingsModal />
</TabPanel>
<TabPanel>
<EventSettingsModal />
</TabPanel>
<TabPanel>
<AliasesModal />
</TabPanel>
<TabPanel>
<OscSettingsModal />
</TabPanel>
{/*<TabPanel>*/}
{/* <IntegrationSettingsModal />*/}
{/*</TabPanel>*/}
+113 -36
View File
@@ -1,83 +1,155 @@
@use '../../styles/variables' as *;
@use '../../styles/main' as *;
//////////////////////////////////// main
.modalBody,
.modalBodyDisabled {
.modalBody {
font-weight: 400;
.notes {
font-weight: 400;
color: $light-bg;
display: grid;
place-items: center;
height: 4em;
}
.separator {
border: 1px solid $light-bg-transparent;
width: 50%;
margin: 0.5em auto;
.modalFields {
min-height: 45vh;
max-height: 45vh;
overflow-y: auto;
scrollbar-color: rgba($light-bg, 0.35) rgba($light-bg, 0.15);
padding-right: 6px;
label {
//font-weight: 400;
font-size: 0.8em;
}
.inlineAlias,
.inlineAliasPlaceholder {
display: grid;
grid-template-columns: 20% 1fr 1em 1.5em 1.5em;
gap: 8px;
align-items: center;
}
.error {
font-size: 0.8em;
color: $error-red;
}
.inlineAliasPlaceholder {
grid-template-columns: 20% 1fr 4em;
.placeholder {
background: $light-text;
width: 100%;
height: 24px;
}
}
}
/* Track */
::-webkit-scrollbar-track {
background: rgba($light-bg, 0.15);
border-radius: 4px;
}
/* Handle */
::-webkit-scrollbar-thumb {
background: rgba($light-bg, 0.35);
border-radius: 4px;
}
/* Handle on hover */
::-webkit-scrollbar-thumb:hover {
background: rgba($light-bg, 0.45);
}
.modalInline {
display: flex;
gap: 2em;
align-items: center;
padding: 0 0.5em 0.5em 0.5em;
}
.spacedEntry {
padding: 0 0.5em 0.5em 0.5em;
}
.pin {
display: flex;
gap: 0.5em;
border-radius: 50%;
input {
border-radius: 50%;
}
}
.submitContainer {
margin-top: 2em;
display: flex;
flex-direction: row-reverse;
button {
margin-top: 1em;
}
justify-content: flex-end;
gap: 1em;
}
}
.modalBody > *,
.modalBodyDisabled > * {
.modalBody > * {
margin-top: 0.5em;
}
//////////////////////////////////// notes
p {
&.notes {
text-align: center;
border-color: $light-bg-transparent;
border-width: 0 2px;
font-size: 0.9em;
margin-bottom: 1em;
text-align: center;
border-color: $light-bg-transparent;
border-width: 0 2px;
font-size: 0.9em;
margin-bottom: 1em;
}
}
span {
&.notes {
font-size: 0.8em;
font-size: 0.9em;
padding-left: 0.4em;
}
}
.highNotes {
background-color: $light-text;
.blockNotes {
background-color: $bg-gray;
margin: 1em 0;
padding: 0.3em;
font-size: 0.9em;
padding: 0.5em;
font-size: 0.8em;
border-radius: 2px;
table {
background-color: $light-text;
background-color: #fff;
border-left: 4px solid lighten($ontime-pink, 5%);
width: 100%;
margin-top: 0.3em;
margin: 0.5em 0;
border-radius: 2px;
:first-child {
padding-left: 1em;
}
td {
user-select: text;
}
}
.noteItem {
user-select: text;
font-weight: 700;
font-weight: 600;
padding-right: 2em;
}
.flexNote {
user-select: text;
padding-bottom: 0.3em;
display: block;
}
.emNote {
@@ -87,13 +159,18 @@ span {
}
}
// Define style for a link
a {
&::after {
content: ' \2197';
color: $accent;
}
&:hover {
color: $accent;
}
.labelNote {
color: $light-bg;
padding-right: 1em;
}
.labelNoteInline {
color: $light-bg;
}
.inlineFlex {
display: flex;
gap: 1em;
align-items: center;
margin-bottom: 1em;
}
@@ -0,0 +1,168 @@
import { ModalBody } from '@chakra-ui/modal';
import { FormControl, FormLabel, Input } from '@chakra-ui/react';
import { getOSC, oscPlaceholderSettings, postOSC } from 'app/api/ontimeApi';
import { useContext, useEffect, useState } from 'react';
import { useFetch } from 'app/hooks/useFetch';
import { OSC_SETTINGS } from 'app/api/apiConstants';
import style from './Modals.module.scss';
import { LoggingContext } from '../../app/context/LoggingContext';
import SubmitContainer from './SubmitContainer';
import { inputProps, portInputProps } from './modalHelper';
export default function OscSettingsModal() {
const { data, status, refetch } = useFetch(OSC_SETTINGS, getOSC);
const { emitError } = useContext(LoggingContext);
const [formData, setFormData] = useState(oscPlaceholderSettings);
const [changed, setChanged] = useState(false);
const [submitting, setSubmitting] = useState(false);
/**
* Set formdata from server state
*/
useEffect(() => {
if (data == null) return;
if (changed) return;
setFormData({ ...data });
}, [changed, data]);
/**
* Validate and submit data
*/
const submitHandler = async (event) => {
event.preventDefault();
setSubmitting(true);
const f = formData;
let e = { status: false, message: '' };
// Validate fields
if (f.port < 1024 || f.port > 65535) {
// Port in incorrect range
e.status = true;
e.message += 'OSC IN Port in incorrect range (1024 - 65535)';
} else if (f.portOut < 1024 || f.portOut > 65535) {
// Port in incorrect range
e.status = true;
e.message += 'OSC OUT Port in incorrect range (1024 - 65535)';
} else if (f.port === f.portOut) {
// Cant use the same port
e.status = true;
e.message += 'OSC IN and OUT Ports cant be the same';
}
// set fields with error
if (e.status) {
emitError(`Invalid Input: ${e.message}`);
} else {
// Post here
await postOSC(formData);
setChanged(false);
}
setSubmitting(false);
};
/**
* Reverts local state equals to server state
*/
const revert = async () => {
setChanged(false);
await refetch();
};
/**
* Handles change of input field in local state
* @param {string} field - object parameter to update
* @param {(string | number)} value - new object parameter value
*/
const handleChange = (field, value) => {
const temp = { ...formData };
temp[field] = value;
setFormData(temp);
setChanged(true);
};
return (
<ModalBody className={style.modalBody}>
<p className={style.notes}>
Options related to Open Sound Control
<br />
🔥 Changes take effect after app restart 🔥
</p>
<form onSubmit={submitHandler}>
<div className={style.modalFields}>
<div className={style.hSeparator}>OSC Input (control)</div>
<div className={style.spacedEntry}>
<FormLabel htmlFor='port'>
OSC In Port
<span className={style.labelNote}>
<br />
Open port for 3rd party control over OSC - Default 8888
</span>
</FormLabel>
<Input
{...portInputProps}
name='port'
placeholder='8888'
value={formData.port}
onChange={(event) =>
handleChange('port', parseInt(event.target.value))
}
style={{ width: '6em', textAlign: 'center' }}
/>
</div>
<div className={style.hSeparator}>OSC Output (feedback)</div>
<div className={style.modalInline}>
<FormControl id='targetIP'>
<FormLabel htmlFor='targetIP'>
OSC Out Target IP
<span className={style.labelNote}>
<br />
Default 127.0.0.1
</span>
</FormLabel>
<Input
{...inputProps}
size='sm'
name='targetIP'
placeholder='127.0.0.1'
autoComplete='off'
value={formData.targetIP}
onChange={(event) =>
handleChange('targetIP', event.target.value)
}
isDisabled={submitting}
style={{ width: '12em', textAlign: 'right' }}
/>
</FormControl>
<FormControl id='portOut'>
<FormLabel htmlFor='portOut'>
OSC Out Port
<span className={style.labelNote}>
<br />
Default 9999
</span>
</FormLabel>
<Input
{...portInputProps}
name='portOut'
placeholder='9999'
value={formData.portOut}
onChange={(event) =>
handleChange('portOut', parseInt(event.target.value))
}
style={{ width: '6em', textAlign: 'left' }}
/>
</FormControl>
</div>
</div>
<SubmitContainer
revert={revert}
submitting={submitting}
changed={changed}
status={status}
/>
</form>
</ModalBody>
);
}
@@ -0,0 +1,35 @@
import style from './Modals.module.scss';
import { Button } from '@chakra-ui/button';
import PropTypes from 'prop-types';
export default function SubmitContainer(props) {
const { submitting, changed, revert, status } = props;
return (
<div className={style.submitContainer}>
<Button
type='submit'
isDisabled={submitting || !changed}
variant='ghosted'
onClick={() => revert()}
>
Revert
</Button>
<Button
colorScheme='blue'
type='submit'
isLoading={submitting}
disabled={!changed || status !== 'success'}
>
Save
</Button>
</div>
);
}
SubmitContainer.propTypes = {
submitting: PropTypes.bool,
changed: PropTypes.bool,
status: PropTypes.string,
revert: PropTypes.func.isRequired,
};
+11
View File
@@ -0,0 +1,11 @@
export const inputProps = {
size: 'sm',
autoComplete: 'off',
};
export const portInputProps = {
...inputProps,
type: 'number',
min: '1024',
max: '65535',
};
+1 -1
View File
@@ -2,7 +2,7 @@ import { useEffect, useState } from 'react';
import { fetchAllEvents } from 'app/api/eventsApi';
import { fetchEvent } from 'app/api/eventApi';
import { useSocket } from 'app/context/socketContext';
import { stringFromMillis } from 'ontime-server/utils/time';
import { stringFromMillis } from 'ontime-utils/time';
import { useFetch } from 'app/hooks/useFetch';
import { EVENTS_TABLE, EVENT_TABLE } from 'app/api/apiConstants';
@@ -1,6 +1,7 @@
import { memo, useEffect, useState } from 'react';
import LowerClean from './LowerClean';
import LowerLines from './LowerLines';
import { useSearchParams } from 'react-router-dom';
const isEqual = require('react-fast-compare');
const areEqual = (prevProps, nextProps) => {
@@ -12,6 +13,7 @@ const areEqual = (prevProps, nextProps) => {
const Lower = (props) => {
const { title } = props;
const [searchParams,] = useSearchParams();
const [titles, setTitles] = useState({
titleNow: '',
titleNext: '',
@@ -61,61 +63,59 @@ const Lower = (props) => {
// eg. http://localhost:3000/lower?bg=ff2&text=f00&size=0.6&transition=5
// Check for user options
useEffect(() => {
// get parameters
const params = new URLSearchParams(props.location.search);
// create aux
let options = {};
// preset: selector
// Should be a number 1-n
let p = parseInt(params.get('preset'));
let p = parseInt(searchParams.get('preset'));
if (!isNaN(p)) setPreset(p);
// size: multiplier
// Should be a number 0.0-n
let s = params.get('size');
let s = searchParams.get('size');
if (s) options.size = s;
// transitionIn: seconds
// Should be a number 0-n
let t = parseInt(params.get('transition'));
let t = parseInt(searchParams.get('transition'));
if (!isNaN(t)) options.transitionIn = t;
// textColour: string
// Should be a hex string '#ffffff'
let c = params.get('text');
let c = searchParams.get('text');
if (c) options.textColour = `#${c}`;
// bgColour: string
// Should be a hex string '#ffffff'
let b = params.get('bg');
let b = searchParams.get('bg');
if (b) options.bgColour = `#${b}`;
// key: string
// Should be a hex string '#00FF00' with key colour
let k = params.get('key');
let k = searchParams.get('key');
if (k) options.keyColour = `#${k}`;
// fadeOut: seconds
// Should be a number 0-n
let f = parseInt(params.get('fadeout'));
let f = parseInt(searchParams.get('fadeout'));
if (!isNaN(f)) options.fadeOut = f;
// x: pixels
// Should be a number 0-n
let x = parseInt(params.get('x'));
let x = parseInt(searchParams.get('x'));
if (!isNaN(x)) options.posX = x;
// y: pixels
// Should be a number 0-n
let y = parseInt(params.get('y'));
let y = parseInt(searchParams.get('y'));
if (!isNaN(y)) options.posY = y;
setLowerOptions({
...options,
set: true,
});
}, [props.location.search]);
}, [searchParams]);
// Defer rendering until we have data ready
if (!lowerOptions.set) return null;
+13 -3
View File
@@ -5,16 +5,26 @@ import App from './App';
import reportWebVitals from './reportWebVitals';
import { BrowserRouter } from 'react-router-dom';
import { ChakraProvider } from '@chakra-ui/react';
import { QueryClient, QueryClientProvider } from 'react-query';
import { AppContextProvider } from './app/context/AppContext';
import SocketProvider from './app/context/socketContext';
// Load Open Sans typeface
require('typeface-open-sans');
const queryClient = new QueryClient();
ReactDOM.render(
<React.StrictMode>
<ChakraProvider resetCSS>
<BrowserRouter>
<App />
</BrowserRouter>
<SocketProvider>
<QueryClientProvider client={queryClient}>
<AppContextProvider>
<BrowserRouter>
<App />
</BrowserRouter>
</AppContextProvider>
</QueryClientProvider>
</SocketProvider>
</ChakraProvider>
</React.StrictMode>,
document.getElementById('root')
+44 -1
View File
@@ -1,4 +1,22 @@
//////////////////////////////////// general app style
//////////////////////////////////// general app colours
$ontime-accent: #4bffabcc;
$ontime-pink: #ff7597;
$ontime-roll: #2b6cb0;
$notes-color: #d69e2e;
$header-gray: #ccc;
$label-gray: #aaa;
$bg-gray: #f4f4f8;
$light-bg: #2b6cb0;
$light-bg-transparent: #2b6cb055;
$light-text: #2b6cb022;
$error-red: #E53E3E;
//////////////////////////////////// general app element overriders
// no decoration on lists
ul {
@@ -8,4 +26,29 @@ ul {
// no resizing on text areas
textarea {
resize: none !important;
}
// Define style for a link
a {
&::after {
content: ' \2197';
color: $ontime-pink;
}
&:hover {
color: $ontime-pink;
}
}
// horizontal separator
.hSeparator {
width: 100%;
border-bottom: 1px solid $light-text;
margin: 1em auto;
display: flex;
align-items: center;
}
// inline vertical separator
.vSpan {
margin: 0 0.5em;
}
+9
View File
@@ -0,0 +1,9 @@
//////////////////////////////////// general app elements
@mixin container-bg {
background-color: rgba(0, 0, 0, 0.13);
border-radius: 2px;
padding: 0 0.5em;
margin: 0 0.5em;
}
-4
View File
@@ -1,4 +0,0 @@
$light-bg: #2b6cb0;
$light-bg-transparent: #2b6cb055;
$light-text: #2b6cb022;
$accent: #ff7597;
+232 -282
View File
@@ -1786,7 +1786,7 @@
dependencies:
regenerator-runtime "^0.13.4"
"@babel/runtime@^7.1.2", "@babel/runtime@^7.10.2", "@babel/runtime@^7.11.2", "@babel/runtime@^7.12.1", "@babel/runtime@^7.8.4", "@babel/runtime@^7.9.2":
"@babel/runtime@^7.10.2", "@babel/runtime@^7.11.2", "@babel/runtime@^7.12.1", "@babel/runtime@^7.8.4", "@babel/runtime@^7.9.2":
version "7.13.10"
resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.13.10.tgz#47d42a57b6095f4468da440388fdbad8bebf0d7d"
integrity sha512-4QPkjJq6Ns3V/RgpEahRk+AGfL0eO6RHHtTWoNNr5mO49G6B5+X6d6THgWEAvTrznU5xYpbAlVKRYcsCgh/Akw==
@@ -1800,7 +1800,7 @@
dependencies:
regenerator-runtime "^0.13.4"
"@babel/runtime@^7.16.3":
"@babel/runtime@^7.16.3", "@babel/runtime@^7.7.6":
version "7.16.5"
resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.16.5.tgz#7f3e34bf8bdbbadf03fbb7b1ea0d929569c9487a"
integrity sha512-TXWihFIS3Pyv5hzR7j6ihmeLkZfrXGxAr5UfSl8CHf+6q/wpiYDkUau0czckpYG8QmnCIuPpdLtuA9VmuGGyMA==
@@ -1877,24 +1877,24 @@
resolved "https://registry.yarnpkg.com/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz#75a2e8b51cb758a7553d6804a5932d7aace75c39"
integrity sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==
"@chakra-ui/accordion@1.4.1":
version "1.4.1"
resolved "https://registry.yarnpkg.com/@chakra-ui/accordion/-/accordion-1.4.1.tgz#bd5373dcfd11572e5e2c535eca51aee4fc24c828"
integrity sha512-/E0FW5YHNVD6WwMGiuQuXpA70P2CKAV+MzcMITnSGPWsh9XD0mcXvMkIALVojfFk9tcCFdIGnxX/HWr41LzgIg==
"@chakra-ui/accordion@1.4.2":
version "1.4.2"
resolved "https://registry.yarnpkg.com/@chakra-ui/accordion/-/accordion-1.4.2.tgz#e851d5e0f748646e20efde909b5f70a93679283b"
integrity sha512-BAGMvcm2sFE5Ft7jwC9nF03/Yv7qztuhzwKBBy4iL0p1nCPh6kV54RBXUcoj3VWe+yrmNiAVYKRTdqQBTJFwOw==
dependencies:
"@chakra-ui/descendant" "2.1.1"
"@chakra-ui/hooks" "1.7.1"
"@chakra-ui/icon" "1.2.1"
"@chakra-ui/icon" "2.0.0"
"@chakra-ui/react-utils" "1.2.1"
"@chakra-ui/transition" "1.4.1"
"@chakra-ui/transition" "1.4.2"
"@chakra-ui/utils" "1.9.1"
"@chakra-ui/alert@1.3.1":
version "1.3.1"
resolved "https://registry.yarnpkg.com/@chakra-ui/alert/-/alert-1.3.1.tgz#d091b36ee4a7e3684220cd6f86fd2e08ca71cb2a"
integrity sha512-BeR6l/1CLZarA3uAe+5Q3hioYf7SixYfy9rOte/29ck1lx9PLjjuPYYmuDPtZNbGibhUCh48z4U/uK2x8mbpKQ==
"@chakra-ui/alert@1.3.2":
version "1.3.2"
resolved "https://registry.yarnpkg.com/@chakra-ui/alert/-/alert-1.3.2.tgz#6c038b0bd58f3964e0a3af213c162c108176a8b0"
integrity sha512-+OMeVeGtydpj6nry0zH7qFDt36zEaxckRnufx1BGiCfWdUg6ahVwKXl8qX93Q8w82od7eAoBKMgGJz7IVL5NPw==
dependencies:
"@chakra-ui/icon" "1.2.1"
"@chakra-ui/icon" "2.0.0"
"@chakra-ui/react-utils" "1.2.1"
"@chakra-ui/utils" "1.9.1"
@@ -1950,18 +1950,18 @@
"@chakra-ui/react-utils" "1.2.1"
"@chakra-ui/utils" "1.9.1"
"@chakra-ui/close-button@1.2.1":
version "1.2.1"
resolved "https://registry.yarnpkg.com/@chakra-ui/close-button/-/close-button-1.2.1.tgz#1109665e73cddcb643bd66e208b2aac75e8af40f"
integrity sha512-A/cuFtJPF8rp5p6tCIGlQdHB89gLCSOzxWssoTXAGJnmlwY2YunFHxgkYZXwPbDqFrM8ndya7Ys+AuL1JZsa3g==
"@chakra-ui/close-button@1.2.2":
version "1.2.2"
resolved "https://registry.yarnpkg.com/@chakra-ui/close-button/-/close-button-1.2.2.tgz#43e91b34d51f75dc08ff9a56cbfc7ed5ad158b8a"
integrity sha512-SqeLib0qgMjK3OsO1g5OnAHUmdCC8GMjToNEea7TeSrA44bH9EXVhFTkMMu2PnDVHbQmi4Ee1cuulNJt0UhQ3g==
dependencies:
"@chakra-ui/icon" "1.2.1"
"@chakra-ui/icon" "2.0.0"
"@chakra-ui/utils" "1.9.1"
"@chakra-ui/color-mode@1.3.1":
version "1.3.1"
resolved "https://registry.yarnpkg.com/@chakra-ui/color-mode/-/color-mode-1.3.1.tgz#08402d28a9c5b4dd3e1a64f69faad319d55dc9d6"
integrity sha512-UuuYMfYxaBE5fPtIR6dwIJn3snT0tVX67RN2RHv4/LQgUeiddq4VziXT3bd+tyIe9+zcsqrZhV7J7YP0h/Id/Q==
"@chakra-ui/color-mode@1.3.2":
version "1.3.2"
resolved "https://registry.yarnpkg.com/@chakra-ui/color-mode/-/color-mode-1.3.2.tgz#e87f9c17ae891c426fa8c9dbdd3c2ba80669fa48"
integrity sha512-/rWcbrzbaWCyyUnT07Qjz0xf/ltHS31CHOKtVCWr2uTgfn2gOQpdxsKRbjrLYPOYZGTMdINUHNiAsqQjLoAoTQ==
dependencies:
"@chakra-ui/hooks" "1.7.1"
"@chakra-ui/react-env" "1.1.1"
@@ -2011,13 +2011,13 @@
"@chakra-ui/utils" "1.9.1"
react-focus-lock "2.5.2"
"@chakra-ui/form-control@1.5.1":
version "1.5.1"
resolved "https://registry.yarnpkg.com/@chakra-ui/form-control/-/form-control-1.5.1.tgz#235267088026690d6994fa406c9f2618fa0351ec"
integrity sha512-ASZYQFOs5mAoaNXAN/ZaesMy3XV07F0/Eba5PQ7Dejdn91aep6lqF889hmr8yqcR646xCOY7ISyYsskfh9QHrQ==
"@chakra-ui/form-control@1.5.2":
version "1.5.2"
resolved "https://registry.yarnpkg.com/@chakra-ui/form-control/-/form-control-1.5.2.tgz#4ce34f03165cfafacecabdce3be82011b74308a7"
integrity sha512-uWv0/f+JEM0ZE5Hnj3TzCnJ09EB+A+DSs9QgyECOuxx9Ju6gnns2uaRki2BfxksQL9ZZomPCkMtXazY9Wa81ag==
dependencies:
"@chakra-ui/hooks" "1.7.1"
"@chakra-ui/icon" "1.2.1"
"@chakra-ui/icon" "2.0.0"
"@chakra-ui/react-utils" "1.2.1"
"@chakra-ui/utils" "1.9.1"
@@ -2031,10 +2031,10 @@
compute-scroll-into-view "1.0.14"
copy-to-clipboard "3.3.1"
"@chakra-ui/icon@1.2.1":
version "1.2.1"
resolved "https://registry.yarnpkg.com/@chakra-ui/icon/-/icon-1.2.1.tgz#b47c0016531da8bef81061d2b96a2d5282b07ff8"
integrity sha512-uZxFsiY4Tld+LvGIX7cky0H6oMRac8udPMQRzIk/UQeNZcsWisGetatbQsew3y1lWV/iH/8+TlDuW13GWGyGGQ==
"@chakra-ui/icon@2.0.0":
version "2.0.0"
resolved "https://registry.yarnpkg.com/@chakra-ui/icon/-/icon-2.0.0.tgz#a2468736117139f94c6ed65eb86b9838e554c90e"
integrity sha512-/GuU+xIcOIy9uSUUUCu249ZJB/nLDbjWGkfpoSdBwqT4+ytJrKt+0Ckh3Ub14sz3BJD+Z6IiIt6ySOA9+7lbsA==
dependencies:
"@chakra-ui/utils" "1.9.1"
@@ -2046,21 +2046,21 @@
"@chakra-ui/hooks" "1.7.1"
"@chakra-ui/utils" "1.9.1"
"@chakra-ui/input@1.3.1":
version "1.3.1"
resolved "https://registry.yarnpkg.com/@chakra-ui/input/-/input-1.3.1.tgz#a407242239f7bb5fdf3b237ab90355c7d77aff9e"
integrity sha512-Z+LqkwVPMeUBuvB9dLDPKkBnWV52Q1PVl3KW9ouDIFg7SoemeYkBt3p4ttEKE+eIPsPlrcH1u2A/RGcCTZOe1g==
"@chakra-ui/input@1.3.2":
version "1.3.2"
resolved "https://registry.yarnpkg.com/@chakra-ui/input/-/input-1.3.2.tgz#59d49ab5c8645c51cd591d46ca23d5cb95e03392"
integrity sha512-VMxmQgFiQ2UnBlkgLX/336G0IfYfw8YWF2ZoEFj5WL9kDSrrL1FXSBgjFGxrper74G4W20tESBCfU1S891y6cg==
dependencies:
"@chakra-ui/form-control" "1.5.1"
"@chakra-ui/form-control" "1.5.2"
"@chakra-ui/react-utils" "1.2.1"
"@chakra-ui/utils" "1.9.1"
"@chakra-ui/layout@1.5.1":
version "1.5.1"
resolved "https://registry.yarnpkg.com/@chakra-ui/layout/-/layout-1.5.1.tgz#be33e8fb5fb187f13c5c0ef66fe94cd293094da6"
integrity sha512-nKiyZ5adjNTbBV3oFIUGIPijwutO1NGdev1jHtnZc3xo2urCIkBvKU8+mVjlX04IwZ7oLKoP3EiDDv0g7+o41Q==
"@chakra-ui/layout@1.6.0":
version "1.6.0"
resolved "https://registry.yarnpkg.com/@chakra-ui/layout/-/layout-1.6.0.tgz#fa45d72420f01f4f6082f5a5a0c202bf7b09a059"
integrity sha512-WUfQ104y1wNueU33/hPlZsMzYJGjO0dXMpVkQf5ZNhNX3IGDO+5+MO2x2xloP+j45yNPi3p8ti/HBnm3dXI+3Q==
dependencies:
"@chakra-ui/icon" "1.2.1"
"@chakra-ui/icon" "2.0.0"
"@chakra-ui/react-utils" "1.2.1"
"@chakra-ui/utils" "1.9.1"
@@ -2071,51 +2071,51 @@
dependencies:
"@chakra-ui/utils" "1.9.1"
"@chakra-ui/media-query@1.2.1":
version "1.2.1"
resolved "https://registry.yarnpkg.com/@chakra-ui/media-query/-/media-query-1.2.1.tgz#c1df05cbe8bd909a410d134240f9bceee760ec09"
integrity sha512-Ho/qiPGTjNukFTE9WBdYV9FIXU7KFTJPqdRQPWANkz+j275n6sqSE1j5LRJllP+ett21KeuWLN4zL33pP0Ox+g==
"@chakra-ui/media-query@1.2.2":
version "1.2.2"
resolved "https://registry.yarnpkg.com/@chakra-ui/media-query/-/media-query-1.2.2.tgz#46db9059de1367a3aa85c1fb973a78f95fca6cc5"
integrity sha512-xSmDVleE1drWiGH/MX3RqyVm29x/8Vf6G0UGaI2kCpbNmon+Q1zHW/yDHvptIuctLrPHYO8LOBxuUjfnIXwC2g==
dependencies:
"@chakra-ui/react-env" "1.1.1"
"@chakra-ui/utils" "1.9.1"
"@chakra-ui/menu@1.8.1":
version "1.8.1"
resolved "https://registry.yarnpkg.com/@chakra-ui/menu/-/menu-1.8.1.tgz#ad26e12843914747de1c36228f4f45c2be668a4d"
integrity sha512-fgzzFukBj4sQzTRf4q/+nHiVTKhrMtJdofnluqce/SCRJ1G+bbovUySblTzfI8iFlTSZt/eWc/Nju4JB1S+3Yg==
"@chakra-ui/menu@1.8.2":
version "1.8.2"
resolved "https://registry.yarnpkg.com/@chakra-ui/menu/-/menu-1.8.2.tgz#849f51beef6220667eb47c831ddf2c65919ded0e"
integrity sha512-u2GfkwTqbWa8L/7i/kOFbU3JANiT2HStR+gsYKuiuOPiuBcUb8OlgfJfP70OtVKegNKmVEMjvzXtld3wCCo/1g==
dependencies:
"@chakra-ui/clickable" "1.2.1"
"@chakra-ui/descendant" "2.1.1"
"@chakra-ui/hooks" "1.7.1"
"@chakra-ui/popper" "2.4.1"
"@chakra-ui/react-utils" "1.2.1"
"@chakra-ui/transition" "1.4.1"
"@chakra-ui/transition" "1.4.2"
"@chakra-ui/utils" "1.9.1"
"@chakra-ui/modal@1.10.1":
version "1.10.1"
resolved "https://registry.yarnpkg.com/@chakra-ui/modal/-/modal-1.10.1.tgz#849243b6f700845ef04e8247d3326e3e0324b789"
integrity sha512-cboC2ITm+5FjhrBc6yJ5cW4VXnfwlLhFa1EkPqF1k4kvYGyUHArvPN1q8AiPYOIrupHYu2Iu6YmQPg7TJwNImg==
"@chakra-ui/modal@1.10.2":
version "1.10.2"
resolved "https://registry.yarnpkg.com/@chakra-ui/modal/-/modal-1.10.2.tgz#578c30812c63863bef25f4b5c279cacbb34528ec"
integrity sha512-ZlmYetPHwHW4CAM09j4/+Ui54dXR1nzU6mOwhWe4/IzLvEyoEU6fHJeKyGxVUpYTG/7wltG/wKFRJpYa77tiBg==
dependencies:
"@chakra-ui/close-button" "1.2.1"
"@chakra-ui/close-button" "1.2.2"
"@chakra-ui/focus-lock" "1.2.1"
"@chakra-ui/hooks" "1.7.1"
"@chakra-ui/portal" "1.3.1"
"@chakra-ui/react-utils" "1.2.1"
"@chakra-ui/transition" "1.4.1"
"@chakra-ui/transition" "1.4.2"
"@chakra-ui/utils" "1.9.1"
aria-hidden "^1.1.1"
react-remove-scroll "2.4.1"
"@chakra-ui/number-input@1.3.1":
version "1.3.1"
resolved "https://registry.yarnpkg.com/@chakra-ui/number-input/-/number-input-1.3.1.tgz#1c46b2748240c780617955da4f2f20a5414edf5c"
integrity sha512-4vBRSShT5pedElgP9YGVC+9RHzQGmUVZqu3p0gZW0fLGVVQ9C1EGrO7djL+k3tgklyu8RvSwkRDJqEPvbQKDgQ==
"@chakra-ui/number-input@1.3.2":
version "1.3.2"
resolved "https://registry.yarnpkg.com/@chakra-ui/number-input/-/number-input-1.3.2.tgz#580035e56a13c30b04034ab02cb9c1132a1cf09c"
integrity sha512-7x7AoqwPXU1odyDcqIwjBwf0MJUwYMM2fa+6YZ52F941GKlvkDiiJOhK6xfhhBzkLUQD6DN8zgAmmGhaZ6UQXw==
dependencies:
"@chakra-ui/counter" "1.2.1"
"@chakra-ui/form-control" "1.5.1"
"@chakra-ui/form-control" "1.5.2"
"@chakra-ui/hooks" "1.7.1"
"@chakra-ui/icon" "1.2.1"
"@chakra-ui/icon" "2.0.0"
"@chakra-ui/react-utils" "1.2.1"
"@chakra-ui/utils" "1.9.1"
@@ -2129,12 +2129,12 @@
"@chakra-ui/react-utils" "1.2.1"
"@chakra-ui/utils" "1.9.1"
"@chakra-ui/popover@1.10.1":
version "1.10.1"
resolved "https://registry.yarnpkg.com/@chakra-ui/popover/-/popover-1.10.1.tgz#03a172201d46bde73eeff7e40e141d82876df493"
integrity sha512-/dMUQfd+h9j3GBtkA/nYaQ5xeu4vk0psUClFvLOAJRwXGN3aMrzn/mhrvHWQ/cJuwQrO1WzxH2+g6pwsFOm9ng==
"@chakra-ui/popover@1.11.0":
version "1.11.0"
resolved "https://registry.yarnpkg.com/@chakra-ui/popover/-/popover-1.11.0.tgz#3ad9faf26019c750a5f8c619ee80dd97bcc4ee3e"
integrity sha512-cCHXAfhIRir+M0ehlYIjDw3mHpiCxDTJ9WV0H1zHQV8nDYVIlZw3nEntaq8oJrv0wpIzq2WCW5ss+bBR7nLZ1A==
dependencies:
"@chakra-ui/close-button" "1.2.1"
"@chakra-ui/close-button" "1.2.2"
"@chakra-ui/hooks" "1.7.1"
"@chakra-ui/popper" "2.4.1"
"@chakra-ui/react-utils" "1.2.1"
@@ -2165,24 +2165,24 @@
"@chakra-ui/theme-tools" "1.3.1"
"@chakra-ui/utils" "1.9.1"
"@chakra-ui/provider@1.7.1":
version "1.7.1"
resolved "https://registry.yarnpkg.com/@chakra-ui/provider/-/provider-1.7.1.tgz#d49c91abd91df8cd2824255f10353a7c17b8df9e"
integrity sha512-E2z56BAYytg+sA7arxUtg54AzhYPpR5QUcR4L679aahgiod+5y6H5TSZcTnxTFRLXVLZbBbP/eH63seIcRqCRQ==
"@chakra-ui/provider@1.7.3":
version "1.7.3"
resolved "https://registry.yarnpkg.com/@chakra-ui/provider/-/provider-1.7.3.tgz#625c0b7c1265515399bb501bc929d8851bba2958"
integrity sha512-D1SrQ7do4yzAv9/OTF3yj/BkLm7kFo5DdeuOCyvXGpVJumnvbtjltRmC7rFQH4R+y9qXPvfQP4LKMNBqSxPNng==
dependencies:
"@chakra-ui/css-reset" "1.1.1"
"@chakra-ui/hooks" "1.7.1"
"@chakra-ui/portal" "1.3.1"
"@chakra-ui/react-env" "1.1.1"
"@chakra-ui/system" "1.8.1"
"@chakra-ui/system" "1.8.3"
"@chakra-ui/utils" "1.9.1"
"@chakra-ui/radio@1.4.1":
version "1.4.1"
resolved "https://registry.yarnpkg.com/@chakra-ui/radio/-/radio-1.4.1.tgz#62de496295f30a6b264cc3906dcfd17bc85a0e85"
integrity sha512-Rq7goauffMNqwGVuPh4zh0kX0AuvTll66PhOwPu+GYHzsYO5+L9X875Q9QBnJEFtDjNF/MVIrH9WrKWjdWLqWw==
"@chakra-ui/radio@1.4.3":
version "1.4.3"
resolved "https://registry.yarnpkg.com/@chakra-ui/radio/-/radio-1.4.3.tgz#d87788fe688d0d58252e9efeb926d0417a70e158"
integrity sha512-TQdyfdUD3BLklOP67n82JN8ksQv1BYjvaYsK0m6WCa0LDJr9aCC+XtUPgVq/1L2t4HqHdiGOrGBooF4vvy/+BA==
dependencies:
"@chakra-ui/form-control" "1.5.1"
"@chakra-ui/form-control" "1.5.2"
"@chakra-ui/hooks" "1.7.1"
"@chakra-ui/react-utils" "1.2.1"
"@chakra-ui/utils" "1.9.1"
@@ -2202,81 +2202,81 @@
dependencies:
"@chakra-ui/utils" "^1.9.1"
"@chakra-ui/react@^1.7.1":
version "1.7.1"
resolved "https://registry.yarnpkg.com/@chakra-ui/react/-/react-1.7.1.tgz#f88417edb4d08766bf102a8d8cc294cbd38443de"
integrity sha512-mbU6M/lxD9BCuXXSEcscPpWqR4mWxXyTk0vkWcgLG59lMFAZ4/Ll+rG1xRHaCuRsAndWNws8h4+NcxY0eECfLQ==
"@chakra-ui/react@^1.7.3":
version "1.7.3"
resolved "https://registry.yarnpkg.com/@chakra-ui/react/-/react-1.7.3.tgz#358a3ff9847b78a798384db5c64cc222c9a48ae8"
integrity sha512-6mrfDUOa9MoQ44Xvi7xgdDq48jTTTjW9BupCGf2R3DI+z6RbUKIHzbcoDJZt2HGY6j9EarMVNRoQJzvzGUKpoQ==
dependencies:
"@chakra-ui/accordion" "1.4.1"
"@chakra-ui/alert" "1.3.1"
"@chakra-ui/accordion" "1.4.2"
"@chakra-ui/alert" "1.3.2"
"@chakra-ui/avatar" "1.3.1"
"@chakra-ui/breadcrumb" "1.3.1"
"@chakra-ui/button" "1.5.1"
"@chakra-ui/checkbox" "1.6.1"
"@chakra-ui/close-button" "1.2.1"
"@chakra-ui/close-button" "1.2.2"
"@chakra-ui/control-box" "1.1.1"
"@chakra-ui/counter" "1.2.1"
"@chakra-ui/css-reset" "1.1.1"
"@chakra-ui/editable" "1.3.1"
"@chakra-ui/form-control" "1.5.1"
"@chakra-ui/form-control" "1.5.2"
"@chakra-ui/hooks" "1.7.1"
"@chakra-ui/icon" "1.2.1"
"@chakra-ui/icon" "2.0.0"
"@chakra-ui/image" "1.1.1"
"@chakra-ui/input" "1.3.1"
"@chakra-ui/layout" "1.5.1"
"@chakra-ui/input" "1.3.2"
"@chakra-ui/layout" "1.6.0"
"@chakra-ui/live-region" "1.1.1"
"@chakra-ui/media-query" "1.2.1"
"@chakra-ui/menu" "1.8.1"
"@chakra-ui/modal" "1.10.1"
"@chakra-ui/number-input" "1.3.1"
"@chakra-ui/media-query" "1.2.2"
"@chakra-ui/menu" "1.8.2"
"@chakra-ui/modal" "1.10.2"
"@chakra-ui/number-input" "1.3.2"
"@chakra-ui/pin-input" "1.7.1"
"@chakra-ui/popover" "1.10.1"
"@chakra-ui/popover" "1.11.0"
"@chakra-ui/popper" "2.4.1"
"@chakra-ui/portal" "1.3.1"
"@chakra-ui/progress" "1.2.1"
"@chakra-ui/provider" "1.7.1"
"@chakra-ui/radio" "1.4.1"
"@chakra-ui/provider" "1.7.3"
"@chakra-ui/radio" "1.4.3"
"@chakra-ui/react-env" "1.1.1"
"@chakra-ui/select" "1.2.1"
"@chakra-ui/skeleton" "1.2.1"
"@chakra-ui/slider" "1.5.1"
"@chakra-ui/select" "1.2.2"
"@chakra-ui/skeleton" "1.2.3"
"@chakra-ui/slider" "1.5.2"
"@chakra-ui/spinner" "1.2.1"
"@chakra-ui/stat" "1.2.1"
"@chakra-ui/stat" "1.2.2"
"@chakra-ui/switch" "1.3.1"
"@chakra-ui/system" "1.8.1"
"@chakra-ui/system" "1.8.3"
"@chakra-ui/table" "1.3.1"
"@chakra-ui/tabs" "1.6.1"
"@chakra-ui/tag" "1.2.1"
"@chakra-ui/textarea" "1.2.1"
"@chakra-ui/theme" "1.12.1"
"@chakra-ui/toast" "1.4.1"
"@chakra-ui/tooltip" "1.4.1"
"@chakra-ui/transition" "1.4.1"
"@chakra-ui/tag" "1.2.2"
"@chakra-ui/textarea" "1.2.2"
"@chakra-ui/theme" "1.12.2"
"@chakra-ui/toast" "1.5.0"
"@chakra-ui/tooltip" "1.4.2"
"@chakra-ui/transition" "1.4.2"
"@chakra-ui/utils" "1.9.1"
"@chakra-ui/visually-hidden" "1.1.1"
"@chakra-ui/select@1.2.1":
version "1.2.1"
resolved "https://registry.yarnpkg.com/@chakra-ui/select/-/select-1.2.1.tgz#a014c44efdc992ced96a4a74364148a4846c3d1f"
integrity sha512-GqRmYGjVnw/Z/2RQiW7Ywuu9O5E0spmMUBjeE/v0rqjixBqrmdApjg5pmJ4YmUMvUI/WkGtR3FR5W9Y5PpvfKw==
"@chakra-ui/select@1.2.2":
version "1.2.2"
resolved "https://registry.yarnpkg.com/@chakra-ui/select/-/select-1.2.2.tgz#1c2956c21a012e7c9ea294db42d3cab0d05bed24"
integrity sha512-EchJW3St1DtSWHe//DHwKjGsQYL2zbKcNCLnJWQKGMPZsQhAD2wsm4xjowFrV8AkY7jbVM/U2v68puN7YTC3hg==
dependencies:
"@chakra-ui/form-control" "1.5.1"
"@chakra-ui/form-control" "1.5.2"
"@chakra-ui/utils" "1.9.1"
"@chakra-ui/skeleton@1.2.1":
version "1.2.1"
resolved "https://registry.yarnpkg.com/@chakra-ui/skeleton/-/skeleton-1.2.1.tgz#6083aadb3ad62e8d63eb64d3985b00a1fdcd9746"
integrity sha512-08yOmINorbxtv1xgZpHiTE7YCYdicZ5dCkc7nCVxvYQihH5efkEiLbP3EefZzFCTqa2KFN8BvgD5eA76TqleXA==
"@chakra-ui/skeleton@1.2.3":
version "1.2.3"
resolved "https://registry.yarnpkg.com/@chakra-ui/skeleton/-/skeleton-1.2.3.tgz#f7be28e3af214473fb6cdec45d0715a049f82575"
integrity sha512-u5ASkzPiBjfvKxKuBienUfmyYDTHziSWQ8Ny6k83LbwLv9IcmBNGsSkmsp7hesgi9cMHGBQ3hY2GTqG9ljndIg==
dependencies:
"@chakra-ui/hooks" "1.7.1"
"@chakra-ui/media-query" "1.2.1"
"@chakra-ui/system" "1.8.1"
"@chakra-ui/media-query" "1.2.2"
"@chakra-ui/system" "1.8.3"
"@chakra-ui/utils" "1.9.1"
"@chakra-ui/slider@1.5.1":
version "1.5.1"
resolved "https://registry.yarnpkg.com/@chakra-ui/slider/-/slider-1.5.1.tgz#2bb3ef0321d81577132a67ef2d0aa1f594d2242a"
integrity sha512-3C0DnUapVdVq9hU1VWAsk0qc6ws1TesjQl/KTkxlP4yBJxKAja1HtyeN9c1x4jizl1a1Q7BJrYICupy70Ju76A==
"@chakra-ui/slider@1.5.2":
version "1.5.2"
resolved "https://registry.yarnpkg.com/@chakra-ui/slider/-/slider-1.5.2.tgz#173218b55befe0f7200fe7f332234b3824d19b08"
integrity sha512-zP07TMew61GkJe47Nu7zEg/SUEwPHpN4alW6VUM6Y8UaVpQaDx7InarbWTc/bXdTP03SfE+hQ6WD9Oy7noe4hQ==
dependencies:
"@chakra-ui/hooks" "1.7.1"
"@chakra-ui/react-utils" "1.2.1"
@@ -2290,19 +2290,19 @@
"@chakra-ui/utils" "1.9.1"
"@chakra-ui/visually-hidden" "1.1.1"
"@chakra-ui/stat@1.2.1":
version "1.2.1"
resolved "https://registry.yarnpkg.com/@chakra-ui/stat/-/stat-1.2.1.tgz#76be860aa5ae7ef66717ab7a7fcf5862268a05e7"
integrity sha512-BTZFeh/8VdgUX080taCQj1g/rS4wGc+y3GQnklqlZ9N/bEv0gyLqQga7TFC/NkVl3cvjRiMnCCPj6vRih9x+Og==
"@chakra-ui/stat@1.2.2":
version "1.2.2"
resolved "https://registry.yarnpkg.com/@chakra-ui/stat/-/stat-1.2.2.tgz#4a690b9a01986a838eb38ff20ee9cfaccdff0cdd"
integrity sha512-0StsPDC56MjzhdlBl0R8wU0uwj9L1tvhQzge/ELSDn4tQDI7VovrxpFzVH0qsj7EZDwZa0BRQaSrstzWvgmJ/Q==
dependencies:
"@chakra-ui/icon" "1.2.1"
"@chakra-ui/icon" "2.0.0"
"@chakra-ui/utils" "1.9.1"
"@chakra-ui/visually-hidden" "1.1.1"
"@chakra-ui/styled-system@1.14.1":
version "1.14.1"
resolved "https://registry.yarnpkg.com/@chakra-ui/styled-system/-/styled-system-1.14.1.tgz#78f49de1842a3675c44b17c9f392d7172880209a"
integrity sha512-dgXFYZdJicsddUnPV1X7lQksgMD0z5EvwGaIh2JHJERqNRIvth/CBAnVLQQvy/xSJK5YaSEmeuVVU0veUOQcXg==
"@chakra-ui/styled-system@1.15.0":
version "1.15.0"
resolved "https://registry.yarnpkg.com/@chakra-ui/styled-system/-/styled-system-1.15.0.tgz#fe117ace12b452379e4df69f4779395ead55917f"
integrity sha512-LnsKeiYkUuJ+NMTwueiX0Mj8CW9XAMJrJxpQm/X3GY5L5PO7Hv6wW725Ovqdy4mhG3IK7S8444FthpsDv/luHw==
dependencies:
"@chakra-ui/utils" "1.9.1"
csstype "^3.0.9"
@@ -2315,14 +2315,14 @@
"@chakra-ui/checkbox" "1.6.1"
"@chakra-ui/utils" "1.9.1"
"@chakra-ui/system@1.8.1":
version "1.8.1"
resolved "https://registry.yarnpkg.com/@chakra-ui/system/-/system-1.8.1.tgz#ce6c5af67840e20f261894c0310a0547314b262c"
integrity sha512-CFHdAjXuEDIAvNBatTBo1Tfu/HJ1LfbmjGWI4uTRfA4sDIt+cCRc/VKSfl0IKQwy70yJOMoTehZBjS8g3RTymQ==
"@chakra-ui/system@1.8.3":
version "1.8.3"
resolved "https://registry.yarnpkg.com/@chakra-ui/system/-/system-1.8.3.tgz#bad447f4fda427fc28e234470d9fcc5a9b524d62"
integrity sha512-6MaevsT7A2ifgOGQQCQsfvzPVd0kEXqFrX1Oxd842bawaqthmbFdo2bBTdaia/+Ivq/8Xot2uAQSbU+3NuRiUA==
dependencies:
"@chakra-ui/color-mode" "1.3.1"
"@chakra-ui/color-mode" "1.3.2"
"@chakra-ui/react-utils" "1.2.1"
"@chakra-ui/styled-system" "1.14.1"
"@chakra-ui/styled-system" "1.15.0"
"@chakra-ui/utils" "1.9.1"
react-fast-compare "3.2.0"
@@ -2344,20 +2344,20 @@
"@chakra-ui/react-utils" "1.2.1"
"@chakra-ui/utils" "1.9.1"
"@chakra-ui/tag@1.2.1":
version "1.2.1"
resolved "https://registry.yarnpkg.com/@chakra-ui/tag/-/tag-1.2.1.tgz#2e5b7bd9895ece35d495b83bb29c34a3142cfe01"
integrity sha512-O068n+qBc+CSyvpRBJ6Lwep6SydQ9UysRqw1ETF+4fJSp9dMrBp8vOcl2SVacKaCu13qdv8UdRMBxUiTz3lh7A==
"@chakra-ui/tag@1.2.2":
version "1.2.2"
resolved "https://registry.yarnpkg.com/@chakra-ui/tag/-/tag-1.2.2.tgz#25519544cdbc0a32b2f8f44c7a8bae3bda912cb1"
integrity sha512-H25y9nEyUAUdwQDND9P4mMXKf1wf9UH4A3DyP237qVKIyYBpa4aCH8eJU4dunh2yIzASB0DWcr7lsul/HAHxmg==
dependencies:
"@chakra-ui/icon" "1.2.1"
"@chakra-ui/icon" "2.0.0"
"@chakra-ui/utils" "1.9.1"
"@chakra-ui/textarea@1.2.1":
version "1.2.1"
resolved "https://registry.yarnpkg.com/@chakra-ui/textarea/-/textarea-1.2.1.tgz#cf1594ce6496817530106f70a5d787c1a9e40f59"
integrity sha512-3xDsL1qQ+eY5r4GcRL4bg90vtV/xxVlw0Z3PFehFP5JW7VwXNZIRjauR/+HlOA8eYq0cF6ch2boR1GPso6rQtw==
"@chakra-ui/textarea@1.2.2":
version "1.2.2"
resolved "https://registry.yarnpkg.com/@chakra-ui/textarea/-/textarea-1.2.2.tgz#3e62612e4b1bc0724ba084b0b08b247ac2bbdbab"
integrity sha512-DoLdKxHk0DyrQDnj1la9wjl2AW3/SK62nfWDYLAm0ouFsw1VKPw9nU+Yyj0dPruQTzI19nLaYF26i97rtnT27g==
dependencies:
"@chakra-ui/form-control" "1.5.1"
"@chakra-ui/form-control" "1.5.2"
"@chakra-ui/utils" "1.9.1"
"@chakra-ui/theme-tools@1.3.1", "@chakra-ui/theme-tools@^1.3.1":
@@ -2368,32 +2368,32 @@
"@chakra-ui/utils" "1.9.1"
"@ctrl/tinycolor" "^3.4.0"
"@chakra-ui/theme@1.12.1":
version "1.12.1"
resolved "https://registry.yarnpkg.com/@chakra-ui/theme/-/theme-1.12.1.tgz#48a61fe5b2b0f8f422255d3049e16bb0b2dd6e60"
integrity sha512-8yDril3rSzv42eKR0x7KdnrpN1ubY0m6q37CVUADgtboJqoJwWWX2/hqkv8CX6WJf8ZwPwFL5QIwS2FPSGgi+g==
"@chakra-ui/theme@1.12.2":
version "1.12.2"
resolved "https://registry.yarnpkg.com/@chakra-ui/theme/-/theme-1.12.2.tgz#191e168d83e7a66bc3e65be802ad4e4e2a9bbeb5"
integrity sha512-LVjSf16yYHD40ILrsDEd3idVQRvJSY7JY8lvTGWo2p6v+JQESWF+zXlYi9Le+TXRpZuFvJuuQ1SEvoqVwdcJ8Q==
dependencies:
"@chakra-ui/anatomy" "1.2.1"
"@chakra-ui/theme-tools" "1.3.1"
"@chakra-ui/utils" "1.9.1"
"@chakra-ui/toast@1.4.1":
version "1.4.1"
resolved "https://registry.yarnpkg.com/@chakra-ui/toast/-/toast-1.4.1.tgz#3e3885dd039e19f8ce60446f4843e0d8ec074b24"
integrity sha512-vzQkYwnGq2nx0bOKIQ6XpJaGzUwnWKmUjcVrz9NzGwVI4g93PS7+13515R0m1NrDp30132OeDXQ+tmQwCRRe6w==
"@chakra-ui/toast@1.5.0":
version "1.5.0"
resolved "https://registry.yarnpkg.com/@chakra-ui/toast/-/toast-1.5.0.tgz#c6456c0a4f24669e16946de318ad568e24a5d497"
integrity sha512-rTsFx/Qos5oVPN6aZMbT/wTxwZlFNSXQqrTpJYaRcRFQGzxIDDxmGkKYfPnyJjRP9i6EqynJhXEIyhMA0xO0dw==
dependencies:
"@chakra-ui/alert" "1.3.1"
"@chakra-ui/close-button" "1.2.1"
"@chakra-ui/alert" "1.3.2"
"@chakra-ui/close-button" "1.2.2"
"@chakra-ui/hooks" "1.7.1"
"@chakra-ui/theme" "1.12.1"
"@chakra-ui/transition" "1.4.1"
"@chakra-ui/theme" "1.12.2"
"@chakra-ui/transition" "1.4.2"
"@chakra-ui/utils" "1.9.1"
"@reach/alert" "0.13.2"
"@chakra-ui/tooltip@1.4.1":
version "1.4.1"
resolved "https://registry.yarnpkg.com/@chakra-ui/tooltip/-/tooltip-1.4.1.tgz#21e148b7107a92abbf90c19a3eead718b875a544"
integrity sha512-KvTuqSqIpIgE+YNUwN7ONDRkSGR6SK9+dgSx2PfKy0Sel7UgDPVtxByuZ6tfJ9O1VTRYEdF9k+s6Gf8eRFQbNA==
"@chakra-ui/tooltip@1.4.2":
version "1.4.2"
resolved "https://registry.yarnpkg.com/@chakra-ui/tooltip/-/tooltip-1.4.2.tgz#fce61c7ad4c3e6e5833487ddf75f03c774913fb9"
integrity sha512-+wyYXG8qenKkFy2YSFfOBf3rlWADnu6S9EUxP+3Rmm78unOWXDuTJWzqy2QlXs2BwoQoifaz1LVwzmMb7WLVgQ==
dependencies:
"@chakra-ui/hooks" "1.7.1"
"@chakra-ui/popper" "2.4.1"
@@ -2402,10 +2402,10 @@
"@chakra-ui/utils" "1.9.1"
"@chakra-ui/visually-hidden" "1.1.1"
"@chakra-ui/transition@1.4.1":
version "1.4.1"
resolved "https://registry.yarnpkg.com/@chakra-ui/transition/-/transition-1.4.1.tgz#be31c02a285d182cc9c6dcd95548f8f35de64952"
integrity sha512-s/VFucc6grNdP1bxw0oQLzy167gjAgyl/GiGH9nt54nioDEiSsvn70qKg7sjajNTvpoot+urQUdr4Qh+fIUFZQ==
"@chakra-ui/transition@1.4.2":
version "1.4.2"
resolved "https://registry.yarnpkg.com/@chakra-ui/transition/-/transition-1.4.2.tgz#de59833aa5727c0dd328b828145e4b0dabd005b3"
integrity sha512-S+BNmpErHlntl//uaqv0sJegzMsQms0OnJapeZaRsvZL4s1SVYrR8kMrXigkdpeh4lAUqGsLpQHPKkzaKGbBOw==
dependencies:
"@chakra-ui/utils" "1.9.1"
@@ -2454,16 +2454,16 @@
source-map "^0.5.7"
stylis "^4.0.3"
"@emotion/cache@^11.5.0":
version "11.5.0"
resolved "https://registry.yarnpkg.com/@emotion/cache/-/cache-11.5.0.tgz#a5eb78cbef8163939ee345e3ddf0af217b845e62"
integrity sha512-mAZ5QRpLriBtaj/k2qyrXwck6yeoz1V5lMt/jfj6igWU35yYlNKs2LziXVgvH81gnJZ+9QQNGelSsnuoAy6uIw==
"@emotion/cache@^11.7.1":
version "11.7.1"
resolved "https://registry.yarnpkg.com/@emotion/cache/-/cache-11.7.1.tgz#08d080e396a42e0037848214e8aa7bf879065539"
integrity sha512-r65Zy4Iljb8oyjtLeCuBH8Qjiy107dOYC6SJq7g7GV5UCQWMObY4SJDPGFjiiVpPrOJ2hmJOoBiYTC7hwx9E2A==
dependencies:
"@emotion/memoize" "^0.7.4"
"@emotion/sheet" "^1.0.3"
"@emotion/sheet" "^1.1.0"
"@emotion/utils" "^1.0.0"
"@emotion/weak-memoize" "^0.2.5"
stylis "^4.0.10"
stylis "4.0.13"
"@emotion/hash@^0.8.0":
version "0.8.0"
@@ -2477,10 +2477,10 @@
dependencies:
"@emotion/memoize" "0.7.4"
"@emotion/is-prop-valid@^1.1.0":
version "1.1.0"
resolved "https://registry.yarnpkg.com/@emotion/is-prop-valid/-/is-prop-valid-1.1.0.tgz#29ef6be1e946fb4739f9707def860f316f668cde"
integrity sha512-9RkilvXAufQHsSsjQ3PIzSns+pxuX4EW8EbGeSPjZMHuMx6z/MOzb9LpqNieQX4F3mre3NWS2+X3JNRHTQztUQ==
"@emotion/is-prop-valid@^1.1.1":
version "1.1.1"
resolved "https://registry.yarnpkg.com/@emotion/is-prop-valid/-/is-prop-valid-1.1.1.tgz#cbd843d409dfaad90f9404e7c0404c55eae8c134"
integrity sha512-bW1Tos67CZkOURLc0OalnfxtSXQJMrAMV0jZTVGJUPSOd4qgjF3+tTD5CwJM13PHA8cltGW1WGbbvV9NpvUZPw==
dependencies:
"@emotion/memoize" "^0.7.4"
@@ -2494,15 +2494,15 @@
resolved "https://registry.yarnpkg.com/@emotion/memoize/-/memoize-0.7.5.tgz#2c40f81449a4e554e9fc6396910ed4843ec2be50"
integrity sha512-igX9a37DR2ZPGYtV6suZ6whr8pTFtyHL3K/oLUotxpSVO2ASaprmAe2Dkq7tBo7CRY7MMDrAa9nuQP9/YG8FxQ==
"@emotion/react@^11.5.0":
version "11.5.0"
resolved "https://registry.yarnpkg.com/@emotion/react/-/react-11.5.0.tgz#19b5771bbfbda5e8517e948a2d9064810f0022bd"
integrity sha512-MYq/bzp3rYbee4EMBORCn4duPQfgpiEB5XzrZEBnUZAL80Qdfr7CEv/T80jwaTl/dnZmt9SnTa8NkTrwFNpLlw==
"@emotion/react@^11.7.1":
version "11.7.1"
resolved "https://registry.yarnpkg.com/@emotion/react/-/react-11.7.1.tgz#3f800ce9b20317c13e77b8489ac4a0b922b2fe07"
integrity sha512-DV2Xe3yhkF1yT4uAUoJcYL1AmrnO5SVsdfvu+fBuS7IbByDeTVx9+wFmvx9Idzv7/78+9Mgx2Hcmr7Fex3tIyw==
dependencies:
"@babel/runtime" "^7.13.10"
"@emotion/cache" "^11.5.0"
"@emotion/cache" "^11.7.1"
"@emotion/serialize" "^1.0.2"
"@emotion/sheet" "^1.0.3"
"@emotion/sheet" "^1.1.0"
"@emotion/utils" "^1.0.0"
"@emotion/weak-memoize" "^0.2.5"
hoist-non-react-statics "^3.3.1"
@@ -2518,19 +2518,19 @@
"@emotion/utils" "^1.0.0"
csstype "^3.0.2"
"@emotion/sheet@^1.0.3":
version "1.0.3"
resolved "https://registry.yarnpkg.com/@emotion/sheet/-/sheet-1.0.3.tgz#00c326cd7985c5ccb8fe2c1b592886579dcfab8f"
integrity sha512-YoX5GyQ4db7LpbmXHMuc8kebtBGP6nZfRC5Z13OKJMixBEwdZrJ914D6yJv/P+ZH/YY3F5s89NYX2hlZAf3SRQ==
"@emotion/sheet@^1.1.0":
version "1.1.0"
resolved "https://registry.yarnpkg.com/@emotion/sheet/-/sheet-1.1.0.tgz#56d99c41f0a1cda2726a05aa6a20afd4c63e58d2"
integrity sha512-u0AX4aSo25sMAygCuQTzS+HsImZFuS8llY8O7b9MDRzbJM0kVJlAz6KNDqcG7pOuQZJmj/8X/rAW+66kMnMW+g==
"@emotion/styled@^11.3.0":
version "11.3.0"
resolved "https://registry.yarnpkg.com/@emotion/styled/-/styled-11.3.0.tgz#d63ee00537dfb6ff612e31b0e915c5cf9925a207"
integrity sha512-fUoLcN3BfMiLlRhJ8CuPUMEyKkLEoM+n+UyAbnqGEsCd5IzKQ7VQFLtzpJOaCD2/VR2+1hXQTnSZXVJeiTNltA==
"@emotion/styled@^11.6.0":
version "11.6.0"
resolved "https://registry.yarnpkg.com/@emotion/styled/-/styled-11.6.0.tgz#9230d1a7bcb2ebf83c6a579f4c80e0664132d81d"
integrity sha512-mxVtVyIOTmCAkFbwIp+nCjTXJNgcz4VWkOYQro87jE2QBTydnkiYusMrRGFtzuruiGK4dDaNORk4gH049iiQuw==
dependencies:
"@babel/runtime" "^7.13.10"
"@emotion/babel-plugin" "^11.3.0"
"@emotion/is-prop-valid" "^1.1.0"
"@emotion/is-prop-valid" "^1.1.1"
"@emotion/serialize" "^1.0.2"
"@emotion/utils" "^1.0.0"
@@ -5301,10 +5301,10 @@ encodeurl@~1.0.2:
resolved "https://registry.yarnpkg.com/encodeurl/-/encodeurl-1.0.2.tgz#ad3ff4c86ec2d029322f5a02c3a9a606c95b3f59"
integrity sha1-rT/0yG7C0CkyL1oCw6mmBslbP1k=
engine.io-client@~6.0.1:
version "6.0.2"
resolved "https://registry.yarnpkg.com/engine.io-client/-/engine.io-client-6.0.2.tgz#ccfc059051e65ca63845e65929184757754cc34e"
integrity sha512-cAep9lhZV6Q8jMXx3TNSU5cydMzMed8/O7Tz5uzyqZvpNPtQ3WQXrLYGADxlsuaFmOLN7wZLmT7ImiFhUOku8g==
engine.io-client@~6.1.1:
version "6.1.1"
resolved "https://registry.yarnpkg.com/engine.io-client/-/engine.io-client-6.1.1.tgz#800d4b9db5487d169686729e5bd887afa78d36b0"
integrity sha512-V05mmDo4gjimYW+FGujoGmmmxRaDsrVr7AXA3ZIfa04MWM1jOfZfUwou0oNqhNwy/votUDvGDt4JA4QF4e0b4g==
dependencies:
"@socket.io/component-emitter" "~3.0.0"
debug "~4.3.1"
@@ -6306,19 +6306,14 @@ hey-listen@^1.0.8:
resolved "https://registry.yarnpkg.com/hey-listen/-/hey-listen-1.0.8.tgz#8e59561ff724908de1aa924ed6ecc84a56a9aa68"
integrity sha512-COpmrF2NOg4TBWUJ5UVyaCU2A88wEMkUPK4hNqyCkqHbxT92BbvfjoSozkAIIm6XhicGlJHhFdullInrdhwU8Q==
history@^4.9.0:
version "4.10.1"
resolved "https://registry.yarnpkg.com/history/-/history-4.10.1.tgz#33371a65e3a83b267434e2b3f3b1b4c58aad4cf3"
integrity sha512-36nwAD620w12kuzPAsyINPWJqlNbij+hpK1k9XRloDtym8mxzGYl2c17LnV6IAGB2Dmg4tEa7G7DlawS0+qjew==
history@^5.2.0:
version "5.2.0"
resolved "https://registry.yarnpkg.com/history/-/history-5.2.0.tgz#7cdd31cf9bac3c5d31f09c231c9928fad0007b7c"
integrity sha512-uPSF6lAJb3nSePJ43hN3eKj1dTWpN9gMod0ZssbFTIsen+WehTmEadgL+kg78xLJFdRfrrC//SavDzmRVdE+Ig==
dependencies:
"@babel/runtime" "^7.1.2"
loose-envify "^1.2.0"
resolve-pathname "^3.0.0"
tiny-invariant "^1.0.2"
tiny-warning "^1.0.0"
value-equal "^1.0.1"
"@babel/runtime" "^7.7.6"
hoist-non-react-statics@^3.1.0, hoist-non-react-statics@^3.3.0, hoist-non-react-statics@^3.3.1, hoist-non-react-statics@^3.3.2:
hoist-non-react-statics@^3.3.0, hoist-non-react-statics@^3.3.1, hoist-non-react-statics@^3.3.2:
version "3.3.2"
resolved "https://registry.yarnpkg.com/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz#ece0acaf71d62c2969c2ec59feff42a4b1a85b45"
integrity sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==
@@ -6853,11 +6848,6 @@ is-wsl@^2.2.0:
dependencies:
is-docker "^2.0.0"
isarray@0.0.1:
version "0.0.1"
resolved "https://registry.yarnpkg.com/isarray/-/isarray-0.0.1.tgz#8a18acfca9a8f4177e09abfc6038939b05d1eedf"
integrity sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8=
isarray@~1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/isarray/-/isarray-1.0.0.tgz#bb935d48582cba168c06834957a54a3e07124f11"
@@ -7670,7 +7660,7 @@ lodash@^4.17.14, lodash@^4.17.15, lodash@^4.17.19, lodash@^4.17.20, lodash@^4.17
resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c"
integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==
loose-envify@^1.0.0, loose-envify@^1.1.0, loose-envify@^1.2.0, loose-envify@^1.3.1, loose-envify@^1.4.0:
loose-envify@^1.0.0, loose-envify@^1.1.0, loose-envify@^1.4.0:
version "1.4.0"
resolved "https://registry.yarnpkg.com/loose-envify/-/loose-envify-1.4.0.tgz#71ee51fa7be4caec1a63839f7e682d8132d30caf"
integrity sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==
@@ -7832,14 +7822,6 @@ min-indent@^1.0.0:
resolved "https://registry.yarnpkg.com/min-indent/-/min-indent-1.0.1.tgz#a63f681673b30571fbe8bc25686ae746eefa9869"
integrity sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==
mini-create-react-context@^0.4.0:
version "0.4.1"
resolved "https://registry.yarnpkg.com/mini-create-react-context/-/mini-create-react-context-0.4.1.tgz#072171561bfdc922da08a60c2197a497cc2d1d5e"
integrity sha512-YWCYEmd5CQeHGSAKrYvXgmzzkrvssZcuuQDDeqkT+PziKGMgE+0MCCtcKbROzocGBG1meBLl2FotlRwf4gAzbQ==
dependencies:
"@babel/runtime" "^7.12.1"
tiny-warning "^1.0.3"
mini-css-extract-plugin@^2.4.5:
version "2.4.5"
resolved "https://registry.yarnpkg.com/mini-css-extract-plugin/-/mini-css-extract-plugin-2.4.5.tgz#191d6c170226037212c483af1180b4010b7b9eef"
@@ -8133,6 +8115,10 @@ onetime@^5.1.2:
dependencies:
mimic-fn "^2.1.0"
"ontime-utils@link: ../server/utils":
version "0.0.0"
uid ""
open@^8.0.9, open@^8.4.0:
version "8.4.0"
resolved "https://registry.yarnpkg.com/open/-/open-8.4.0.tgz#345321ae18f8138f82565a910fdc6b39e8c244f8"
@@ -8323,13 +8309,6 @@ path-to-regexp@0.1.7:
resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-0.1.7.tgz#df604178005f522f15eb4490e7247a1bfaa67f8c"
integrity sha1-32BBeABfUi8V60SQ5yR6G/qmf4w=
path-to-regexp@^1.7.0:
version "1.8.0"
resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-1.8.0.tgz#887b3ba9d84393e87a0a0b9f4cb756198b53548a"
integrity sha512-n43JRhlUKUAlibEJhPeir1ncUID16QnEjNpwzNdO3Lm4ywrBpBZ5oLD0I6br9evr1Y9JTqwRtAh7JLoOzAQdVA==
dependencies:
isarray "0.0.1"
path-type@^4.0.0:
version "4.0.0"
resolved "https://registry.yarnpkg.com/path-type/-/path-type-4.0.0.tgz#84ed01c0a7ba380afe09d90a8c180dcd9d03043b"
@@ -9220,15 +9199,15 @@ react-icons@^4.3.1:
resolved "https://registry.yarnpkg.com/react-is/-/react-is-17.0.2.tgz#e691d4a8e9c789365655539ab372762b0efb54f0"
integrity sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==
react-is@^16.13.1, react-is@^16.6.0, react-is@^16.7.0, react-is@^16.8.1:
react-is@^16.13.1, react-is@^16.7.0, react-is@^16.8.1:
version "16.13.1"
resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4"
integrity sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==
react-qr-code@^2.0.2:
version "2.0.2"
resolved "https://registry.yarnpkg.com/react-qr-code/-/react-qr-code-2.0.2.tgz#64107c869079aceb897c97496d163720ab2820e8"
integrity sha512-73VGe81MgeE5FJNFgdY42ez/wPPJTHuooU3iE4CX+6F8M88O1Gg4zNA0L4bKEpoySQ0QjqreJgyXjFrG/QfsdA==
react-qr-code@2.0.3:
version "2.0.3"
resolved "https://registry.yarnpkg.com/react-qr-code/-/react-qr-code-2.0.3.tgz#cc80785e08f817d1ab066ca4035262f77d049648"
integrity sha512-6GDH0l53lksf2JgZwwcoS0D60a1OAal/GQRyNFkMBW19HjSqvtD5S20scmSQsKl+BgWM85Wd5DCcUYoHd+PZnQ==
dependencies:
prop-types "^15.7.2"
qr.js "0.0.0"
@@ -9278,34 +9257,20 @@ react-remove-scroll@2.4.1:
use-callback-ref "^1.2.3"
use-sidecar "^1.0.1"
react-router-dom@^5.2.0:
version "5.2.0"
resolved "https://registry.yarnpkg.com/react-router-dom/-/react-router-dom-5.2.0.tgz#9e65a4d0c45e13289e66c7b17c7e175d0ea15662"
integrity sha512-gxAmfylo2QUjcwxI63RhQ5G85Qqt4voZpUXSEqCwykV0baaOTQDR1f0PmY8AELqIyVc0NEZUj0Gov5lNGcXgsA==
react-router-dom@^6.2.1:
version "6.2.1"
resolved "https://registry.yarnpkg.com/react-router-dom/-/react-router-dom-6.2.1.tgz#32ec81829152fbb8a7b045bf593a22eadf019bec"
integrity sha512-I6Zax+/TH/cZMDpj3/4Fl2eaNdcvoxxHoH1tYOREsQ22OKDYofGebrNm6CTPUcvLvZm63NL/vzCYdjf9CUhqmA==
dependencies:
"@babel/runtime" "^7.1.2"
history "^4.9.0"
loose-envify "^1.3.1"
prop-types "^15.6.2"
react-router "5.2.0"
tiny-invariant "^1.0.2"
tiny-warning "^1.0.0"
history "^5.2.0"
react-router "6.2.1"
react-router@5.2.0, react-router@^5.2.0:
version "5.2.0"
resolved "https://registry.yarnpkg.com/react-router/-/react-router-5.2.0.tgz#424e75641ca8747fbf76e5ecca69781aa37ea293"
integrity sha512-smz1DUuFHRKdcJC0jobGo8cVbhO3x50tCL4icacOlcwDOEQPq4TMqwx3sY1TP+DvtTgz4nm3thuo7A+BK2U0Dw==
react-router@6.2.1:
version "6.2.1"
resolved "https://registry.yarnpkg.com/react-router/-/react-router-6.2.1.tgz#be2a97a6006ce1d9123c28934e604faef51448a3"
integrity sha512-2fG0udBtxou9lXtK97eJeET2ki5//UWfQSl1rlJ7quwe6jrktK9FCCc8dQb5QY6jAv3jua8bBQRhhDOM/kVRsg==
dependencies:
"@babel/runtime" "^7.1.2"
history "^4.9.0"
hoist-non-react-statics "^3.1.0"
loose-envify "^1.3.1"
mini-create-react-context "^0.4.0"
path-to-regexp "^1.7.0"
prop-types "^15.6.2"
react-is "^16.6.0"
tiny-invariant "^1.0.2"
tiny-warning "^1.0.0"
history "^5.2.0"
react-scripts@5.0.0:
version "5.0.0"
@@ -9577,11 +9542,6 @@ resolve-from@^5.0.0:
resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-5.0.0.tgz#c35225843df8f776df21c57557bc087e9dfdfc69"
integrity sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==
resolve-pathname@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/resolve-pathname/-/resolve-pathname-3.0.0.tgz#99d02224d3cf263689becbb393bc560313025dcd"
integrity sha512-C7rARubxI8bXFNB/hqcp/4iUeIXJhJZvFPFPiSPRnhU5UPxzMFIl+2E6yY6c4k9giDJAhtV+enfA+G89N6Csng==
resolve-url-loader@^4.0.0:
version "4.0.0"
resolved "https://registry.yarnpkg.com/resolve-url-loader/-/resolve-url-loader-4.0.0.tgz#d50d4ddc746bb10468443167acf800dcd6c3ad57"
@@ -9903,15 +9863,15 @@ slash@^4.0.0:
resolved "https://registry.yarnpkg.com/slash/-/slash-4.0.0.tgz#2422372176c4c6c5addb5e2ada885af984b396a7"
integrity sha512-3dOsAHXXUkQTpOYcoAxLIorMTp4gIQr5IW3iVb7A7lFIp0VHhnynm9izx6TssdrIcVIESAlVjtnO2K8bg+Coew==
socket.io-client@^4.3.2:
version "4.3.2"
resolved "https://registry.yarnpkg.com/socket.io-client/-/socket.io-client-4.3.2.tgz#9cfdb8fecac8a24d5723daf8c8749e70c8fdeb25"
integrity sha512-2B9LqSunN60yV8F7S84CCEEcgbYNfrn7ejIInZtLZ7ppWtiX8rGZAjvdCvbnC8bqo/9RlCNOUsORLyskxSFP1g==
socket.io-client@4.4.0:
version "4.4.0"
resolved "https://registry.yarnpkg.com/socket.io-client/-/socket.io-client-4.4.0.tgz#d6568ebd79ac12e2d6b628e7e90e60f1d48d99ff"
integrity sha512-g7riSEJXi7qCFImPow98oT8X++MSsHz6MMFRXkWNJ6uEROSHOa3kxdrsYWMq85dO+09CFMkcqlpjvbVXQl4z6g==
dependencies:
"@socket.io/component-emitter" "~3.0.0"
backo2 "~1.0.2"
debug "~4.3.2"
engine.io-client "~6.0.1"
engine.io-client "~6.1.1"
parseuri "0.0.6"
socket.io-parser "~4.1.1"
@@ -10219,10 +10179,10 @@ stylehacks@^5.0.1:
browserslist "^4.16.0"
postcss-selector-parser "^6.0.4"
stylis@^4.0.10:
version "4.0.10"
resolved "https://registry.yarnpkg.com/stylis/-/stylis-4.0.10.tgz#446512d1097197ab3f02fb3c258358c3f7a14240"
integrity sha512-m3k+dk7QeJw660eIKRRn3xPF6uuvHs/FFzjX3HQ5ove0qYsiygoAhwn5a3IYKaZPo5LrYD0rfVmtv1gNY1uYwg==
stylis@4.0.13:
version "4.0.13"
resolved "https://registry.yarnpkg.com/stylis/-/stylis-4.0.13.tgz#f5db332e376d13cc84ecfe5dace9a2a51d954c91"
integrity sha512-xGPXiFVl4YED9Jh7Euv2V220mriG9u4B2TA6Ybjc1catrstKD2PpIdU3U0RKpkVBC2EhmL/F0sPCr9vrFTNRag==
stylis@^4.0.3:
version "4.0.9"
@@ -10409,16 +10369,11 @@ timsort@^0.3.0:
resolved "https://registry.yarnpkg.com/timsort/-/timsort-0.3.0.tgz#405411a8e7e6339fe64db9a234de11dc31e02bd4"
integrity sha1-QFQRqOfmM5/mTbmiNN4R3DHgK9Q=
tiny-invariant@^1.0.2, tiny-invariant@^1.0.6:
tiny-invariant@^1.0.6:
version "1.1.0"
resolved "https://registry.yarnpkg.com/tiny-invariant/-/tiny-invariant-1.1.0.tgz#634c5f8efdc27714b7f386c35e6760991d230875"
integrity sha512-ytxQvrb1cPc9WBEI/HSeYYoGD0kWnGEOR8RY6KomWLBVhqz0RgTwVO9dLrGz7dC+nN9llyI7OKAgRq8Vq4ZBSw==
tiny-warning@^1.0.0, tiny-warning@^1.0.3:
version "1.0.3"
resolved "https://registry.yarnpkg.com/tiny-warning/-/tiny-warning-1.0.3.tgz#94a30db453df4c643d0fd566060d60a875d84754"
integrity sha512-lBN9zLN/oAf68o3zNXYrdCt1kP8WsiGW8Oo2ka41b2IM5JL/S1CTyX1rW0mb/zSuJun0ZUrDxx4sqvYS2FWzPA==
tmp@^0.2.1:
version "0.2.1"
resolved "https://registry.yarnpkg.com/tmp/-/tmp-0.2.1.tgz#8457fc3037dcf4719c251367a1af6500ee1ccf14"
@@ -10729,11 +10684,6 @@ v8-to-istanbul@^8.1.0:
convert-source-map "^1.6.0"
source-map "^0.7.3"
value-equal@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/value-equal/-/value-equal-1.0.1.tgz#1e0b794c734c5c0cade179c437d356d931a34d6c"
integrity sha512-NOJ6JZCAWr0zlxZt+xqCHNTEKOsrks2HQd4MqhP1qy4z1SkbEP467eNx6TgDKXMvUOb+OENfJCZwM+16n7fRfw==
vary@~1.1.2:
version "1.1.2"
resolved "https://registry.yarnpkg.com/vary/-/vary-1.1.2.tgz#2299f02c6ded30d4a5961b0b9f74524a18f634fc"
+17 -6
View File
@@ -118,10 +118,21 @@
"app": "ontime",
"version": 1,
"serverPort": 4001,
"oscInPort": 8888,
"oscOutPort": 9999,
"oscOutIP": "127.0.0.1",
"oscEnabled": true,
"lock": false
}
"lock": null,
"pinCode": "1234"
},
"osc": {
"port": 8888,
"portOut": 9999,
"targetIP": "127.0.0.1",
"enabled": true
},
"aliases": [
{
"id": "0b0b3",
"enabled": true,
"alias": "test",
"pathAndParams": "lower?bg=ff2&text=f00&size=0.6&transition=5"
}
]
}
+15 -3
View File
@@ -1,6 +1,6 @@
{
"name": "ontime",
"version": "0.4.8",
"version": "0.5.0",
"author": "Carlos Valente",
"description": "Time keeping for live events",
"repository": "https://github.com/cpvalente/ontime",
@@ -13,17 +13,18 @@
"main": "main.js",
"devDependencies": {
"electron": "^13.6.1",
"electron-builder": "^22.11.3",
"electron-builder": "^22.14.5",
"eslint": "^8.5.0",
"eslint-config-prettier": "^8.3.0",
"eslint-plugin-prettier": "^4.0.0",
"jest": "^27.0.4",
"jest": "^27.4.5",
"prettier": "^2.5.1"
},
"scripts": {
"nodestart": "NODE_ENV=development node src/app.js",
"setdb": "cp data/db.json src/data/db.json",
"clean": "rm -rf ../client/build/ && rm -rf ../client/node_modules && rm -rf src/node_modules && rm -rf ./node_modules && rm -rf ./dist",
"cheat": "rm src/node_modules/ontime-utils && cp -R utils src/node_modules/ontime-utils",
"prep": "yarn clean && yarn setdb",
"test": "node --experimental-vm-modules node_modules/jest/bin/jest.js",
"start": "NODE_ENV=development electron .",
@@ -34,6 +35,8 @@
"dist-all": "electron-builder -mw"
},
"jest": {
"testEnvironment": "node",
"testRunner": "jasmine2",
"testPathIgnorePatterns": [
"dist"
]
@@ -94,6 +97,15 @@
"!**/{yarn.lock,yarn-error.log}",
"!**/{test,tests,__test__,__tests__,mock,mocks,__mock__,__mocks__}"
]
},
{
"from": "utils",
"to": "extraResources/utils",
"filter": [
"**/*",
"!**/{yarn.lock,yarn-error.log}",
"!**/{test,tests,__test__,__tests__,mock,mocks,__mock__,__mocks__}"
]
}
]
},
+9 -7
View File
@@ -1,6 +1,7 @@
// get environment vars
import 'dotenv/config';
import { sessionId, user } from './utils/analytics.js';
user.screenview('Node service', 'ontime').send();
user.event('NODE', 'started', 'starting node service').send();
@@ -12,6 +13,7 @@ import { Low, JSONFile } from 'lowdb';
import path from 'path';
import { fileURLToPath } from 'url';
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
@@ -26,7 +28,7 @@ import express from 'express';
import http from 'http';
import cors from 'cors';
import { dbModelv1 as dbModel } from './models/dataModel.js';
import { parseJsonv1 as parseJson, validateFile } from './utils/parser.js';
import { parseJson_v1 as parseJson, validateFile } from './utils/parser.js';
import ua from 'universal-analytics';
// validate JSON before attempting read
@@ -86,8 +88,8 @@ app.use('/playback', playbackRouter);
// serve react
app.use(
express.static(
path.join(__dirname, env === 'prod' ? '../' : '../../', 'client/build')
)
path.join(__dirname, env === 'prod' ? '../' : '../../', 'client/build'),
),
);
app.get('*', (req, res) => {
@@ -97,8 +99,8 @@ app.get('*', (req, res) => {
env === 'prod' ? '../' : '../../',
'client',
'build',
'index.html'
)
'index.html',
),
);
});
@@ -159,8 +161,8 @@ export const startServer = async (overrideConfig = null) => {
// OSC Config
const oscConfig = {
ip: oscIP,
port: overrideConfig?.port || oscOutPort
}
port: overrideConfig?.port || oscOutPort,
};
// init timer
global.timer = new EventTimer(server, config.timer, oscConfig, data.http);
+15 -8
View File
@@ -10,8 +10,8 @@ import { OSCIntegration } from './integrations/Osc.js';
import { HTTPIntegration } from './integrations/Http.js';
import { cleanURL } from '../utils/url.js';
import getRandomName from '../utils/getRandomName.js';
import { stringFromMillis } from '../utils/time.js';
import { generateId } from '../utils/generate_id.js';
import { stringFromMillis } from 'ontime-utils/time.js';
import { generateId } from 'ontime-utils/generate_id.js';
/*
* Class EventTimer adds functions specific to APP
@@ -111,13 +111,20 @@ export class EventTimer extends Timer {
* @description Shutdown process
*/
shutdown() {
clearInterval(this._interval);
this.info('SERVER', 'Shutting down ontime');
this.info('TX', '... Closing socket server');
this.io.close();
this.info('TX', '... Closing OSC Client');
this.osc.shutdown();
this.info('TX', '... Closing HTTP Client');
this.http.shutdown();
if (this.io != null) {
this.info('TX', '... Closing socket server');
this.io.close();
}
if (this.osc != null) {
this.info('TX', '... Closing OSC Client');
this.osc.shutdown();
}
if (this.http != null) {
this.info('TX', '... Closing HTTP Client');
this.http.shutdown();
}
}
/**
+1 -1
View File
@@ -4,7 +4,7 @@
*
*/
import { stringFromMillis } from '../utils/time.js';
import { stringFromMillis } from 'ontime-utils/time.js';
export class Timer {
constructor() {
+20 -23
View File
@@ -1,24 +1,18 @@
import {EventTimer} from "../EventTimer";
import { EventTimer } from '../EventTimer';
import http from 'http';
import express from "express";
import express from 'express';
// Create server
const app = express();
const server = http.createServer(app);
// necessary config
const timerConfig = {refresh: 1000};
const timerConfig = { refresh: 1000 };
beforeEach(async () => {
server.listen(0, '0.0.0.0');
});
afterEach(async () => {
afterAll(async () => {
await server.close();
});
test('object instantiates correctly', async () => {
const t = new EventTimer(server, timerConfig);
@@ -41,16 +35,15 @@ test('object instantiates correctly', async () => {
// and its own properties
expect(t.ontimeCycle).toBe('idle');
expect(t.prevCycle).toBeNull();
expect(t.lastUpdate).toBeNull();
expect(t.io).not.toBeNull();
expect(t.osc).toBeNull();
expect(t.http).toBeNull();
expect(t._numClients).toBe(0);
expect(t._interval).not.toBeNull();
expect(t.presenter).toStrictEqual({text: '', visible: false});
expect(t.public).toStrictEqual({text: '', visible: false});
expect(t.lower).toStrictEqual({text: '', visible: false});
expect(t.lower).toStrictEqual({text: '', visible: false});
expect(t.presenter).toStrictEqual({ text: '', visible: false });
expect(t.public).toStrictEqual({ text: '', visible: false });
expect(t.lower).toStrictEqual({ text: '', visible: false });
expect(t.lower).toStrictEqual({ text: '', visible: false });
const expectTitlesPublic = {
titleNow: null,
@@ -78,19 +71,20 @@ test('object instantiates correctly', async () => {
expect(t.numEvents).toBe(0);
expect(t._eventlist).toBeNull();
expect(t.onAir).toBeFalsy();
});
t.shutdown();
});
describe('test triggers behaviour', () => {
const t = new EventTimer(server, timerConfig);
it('ignores bad commands', () => {
test('ignores bad commands', async(done) => {
const success = t.trigger('test');
expect(success).toBeFalsy();
})
it('does not allow triggering events with an empty list', () => {
done();
});
test('does not allow triggering events with an empty list', async(done) => {
expect(t.numEvents).toBe(0);
expect(t.trigger('start')).toBeFalsy();
@@ -106,10 +100,10 @@ describe('test triggers behaviour', () => {
expect(t.onAir).toBeTruthy();
expect(t.trigger('offAir')).toBeTruthy();
expect(t.onAir).toBeFalsy();
done();
});
it('...and is consistent by calling the class methods', () => {
test('...and is consistent by calling the class methods', async (done) => {
expect(t.numEvents).toBe(0);
expect(t.state).toBe('stop');
@@ -133,5 +127,8 @@ describe('test triggers behaviour', () => {
t.reload();
expect(t.state).toBe('stop');
done();
});
})
t.shutdown();
});
+1 -1
View File
@@ -2,7 +2,7 @@
import { db, data } from '../app.js';
// utils
import { generateId } from '../utils/generate_id.js';
import { generateId } from 'ontime-utils/generate_id.js';
import {
event as eventDef,
delay as delayDef,
@@ -6,6 +6,7 @@ import { fileURLToPath } from 'url';
import { db, data } from '../app.js';
import { networkInterfaces } from 'os';
import { fileHandler } from '../utils/parser.js';
import { generateId } from 'ontime-utils/generate_id.js';
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
@@ -116,6 +117,81 @@ export const getInfo = async (req, res) => {
});
};
// Create controller for POST request to '/ontime/aliases'
// Returns -
export const getAliases = async (req, res) => {
// send aliases array
res.status(200).send(data.aliases);
};
// Create controller for POST request to '/ontime/aliases'
// Returns ACK message
export const postAliases = async (req, res) => {
if (!req.body) {
res.status(400).send('No object found in request');
return;
}
// TODO: validate data
try {
const newAliases = [];
req.body.forEach((a) => {
newAliases.push({
id: generateId(),
enabled: a.enabled,
alias: a.alias,
pathAndParams: a.pathAndParams,
});
});
data.aliases = newAliases;
await db.write();
res.sendStatus(200);
} catch (error) {
res.status(400).send(error);
}
};
// Create controller for POST request to '/ontime/settings'
// Returns -
export const getSettings = async (req, res) => {
const version = data.settings.version;
const serverPort = data.settings.serverPort;
const pinCode = data.settings.pinCode;
// send object with network information
res.status(200).send({
version,
serverPort,
pinCode,
});
};
// Create controller for POST request to '/ontime/settings'
// Returns ACK message
export const postSettings = async (req, res) => {
if (!req.body) {
res.status(400).send('No object found in request');
return;
}
try {
let pin = data.settings.pinCode;
if (typeof req.body?.pinCode === 'string') {
if (req.body?.pinCode.length === 0) {
pin = null;
} else if (req.body?.pinCode.length <= 4) {
pin = req.body?.pinCode;
}
}
data.settings = {
...data.settings,
pinCode: pin,
};
await db.write();
res.sendStatus(200);
} catch (error) {
res.status(400).send(error);
}
};
// Create controller for POST request to '/ontime/info'
// Returns ACK message
export const postInfo = async (req, res) => {
+5
View File
@@ -0,0 +1,5 @@
export const alias = {
enabled: false,
alias: '',
pathAndParams: '',
};
+6 -4
View File
@@ -12,7 +12,9 @@ export const dbModelv1 = {
version: 1,
serverPort: 4001,
lock: null,
pinCode: null,
},
aliases: [],
osc: {
port: 8888,
portOut: 9999,
@@ -27,7 +29,7 @@ export const dbModelv1 = {
url: '',
enabled: false,
},
onStart: {
onStart: {
url: '',
enabled: false,
},
@@ -35,15 +37,15 @@ export const dbModelv1 = {
url: '',
enabled: false,
},
onPause: {
onPause: {
url: '',
enabled: false,
},
onStop: {
onStop: {
url: '',
enabled: false,
},
onFinish: {
onFinish: {
url: '',
enabled: false,
},
+5 -4
View File
@@ -7,13 +7,14 @@
"express": "~4.17.1",
"express-session": "~1.17.1",
"lowdb": "3.0.0",
"multer": "^1.4.2",
"nanoid": "^3.1.22",
"node-osc": "6.1.10",
"multer": "^1.4.4",
"nanoid": "^3.1.30",
"node-osc": "6.1.11",
"node-xlsx": "^0.17.2",
"ontime-utils": "link: ../server/utils/",
"passport": "~0.4.1",
"passport-local": "~1.0.0",
"socket.io": "^4.3.1",
"socket.io": "4.4.0",
"universal-analytics": "^0.4.23"
},
"devDependencies": {
+17
View File
@@ -1,5 +1,6 @@
import express from 'express';
import { uploadFile } from '../utils/upload.js';
export const router = express.Router();
import {
@@ -10,6 +11,10 @@ import {
dbPathToUpload,
getOSC,
postOSC,
getSettings,
postSettings,
getAliases,
postAliases,
} from '../controllers/ontimeController.js';
// create route between controller and '/ontime/db' endpoint
@@ -18,6 +23,18 @@ router.get('/db', dbDownload);
// create route between controller and '/ontime/db' endpoint
router.post('/db', uploadFile, dbUpload);
// create route between controller and '/ontime/settings' endpoint
router.get('/settings', getSettings);
// create route between controller and '/ontime/settings' endpoint
router.post('/settings', postSettings);
// create route between controller and '/ontime/aliases' endpoint
router.get('/aliases', getAliases);
// create route between controller and '/ontime/aliases' endpoint
router.post('/aliases', postAliases);
// create route between controller and '/ontime/info' endpoint
router.get('/info', getInfo);
@@ -1,20 +0,0 @@
import { generateId } from '../generate_id.js';
describe('generate a valid id', () => {
it('generates a 5 digit id', () => {
const id = generateId();
expect(id.length).toBe(5);
});
});
describe('generate 100', () => {
it('all ids are unique', () => {
let ids = [];
for (let i = 0; i < 100; i++) {
ids.push(generateId());
}
const unique = [...new Set(ids)];
expect(ids.length).toBe(unique.length);
});
});
@@ -1,11 +1,11 @@
import getRandomName from '../getRandomName.js';
test('generates 500 unique names', () => {
let names = [];
for (let i = 0; i < 500; i++) {
names.push(getRandomName());
test('generates 100 unique names', () => {
const names = new Set();
let attempts = 1;
while (names.size < 100) {
names.add(getRandomName());
attempts++;
}
const unique = [...new Set(names)];
expect(names.length).toBe(unique.length);
});
expect(attempts).toBeLessThan(105);
});
+44 -19
View File
@@ -1,9 +1,10 @@
import jest from 'jest-mock';
import {
makeString,
parseExcelv1,
parseJsonv1,
validateEventv1,
parseAliases_v1,
parseExcel_v1,
parseJson_v1,
validateEvent_v1,
} from '../parser.js';
import { dbModelv1 as dbModel } from '../../models/dataModel.js';
@@ -110,7 +111,7 @@ describe('test json parser with valid def', () => {
let parseResponse;
beforeEach(async () => {
parseResponse = await parseJsonv1(testData);
parseResponse = await parseJson_v1(testData);
});
it('has 7 events', () => {
@@ -169,7 +170,7 @@ describe('test parser edge cases', () => {
],
};
const parseResponse = await parseJsonv1(testData);
const parseResponse = await parseJson_v1(testData);
expect(parseResponse.events[0].id).toBeDefined();
});
@@ -190,7 +191,7 @@ describe('test parser edge cases', () => {
],
};
const parseResponse = await parseJsonv1(testData);
const parseResponse = await parseJson_v1(testData);
expect(console.log).toHaveBeenCalledWith(
'ERROR: ID collision on import, skipping'
);
@@ -212,7 +213,7 @@ describe('test parser edge cases', () => {
],
};
const parseResponse = await parseJsonv1(testData);
const parseResponse = await parseJson_v1(testData);
expect(console.log).toHaveBeenCalledWith(
'ERROR: undefined event type, skipping'
);
@@ -228,7 +229,7 @@ describe('test parser edge cases', () => {
},
};
await parseJsonv1(testData);
await parseJson_v1(testData);
expect(console.log).toHaveBeenCalledWith(
'ERROR: unknown app version, skipping'
);
@@ -271,7 +272,7 @@ describe('test corrupt data', () => {
},
};
const parsedDef = await parseJsonv1(emptyEvents);
const parsedDef = await parseJson_v1(emptyEvents);
expect(parsedDef.events.length).toBe(2);
});
@@ -293,7 +294,7 @@ describe('test corrupt data', () => {
},
};
const parsedDef = await parseJsonv1(emptyEvents);
const parsedDef = await parseJson_v1(emptyEvents);
expect(parsedDef.events.length).toBe(0);
});
@@ -309,7 +310,7 @@ describe('test corrupt data', () => {
},
};
const parsedDef = await parseJsonv1(emptyEventData);
const parsedDef = await parseJson_v1(emptyEventData);
expect(parsedDef.event).toStrictEqual(dbModel.event);
});
@@ -323,14 +324,14 @@ describe('test corrupt data', () => {
},
};
const parsedDef = await parseJsonv1(missingSettings);
const parsedDef = await parseJson_v1(missingSettings);
expect(parsedDef.settings).toStrictEqual(dbModel.settings);
});
it('fails with invalid JSON', async () => {
console.log = jest.fn();
const invalidJSON = 'some random dataset';
const parsedDef = await parseJsonv1(invalidJSON);
const parsedDef = await parseJson_v1(invalidJSON);
expect(console.log).toHaveBeenCalledWith('ERROR: Invalid JSON format');
expect(parsedDef).toBe(-1);
});
@@ -341,7 +342,7 @@ describe('test event validator', () => {
const event = {
title: 'test',
};
const validated = validateEventv1(event);
const validated = validateEvent_v1(event);
expect(validated).toEqual(
expect.objectContaining({
@@ -361,7 +362,7 @@ describe('test event validator', () => {
it('fails an empty object', () => {
const event = {};
const validated = validateEventv1(event);
const validated = validateEvent_v1(event);
expect(validated).toEqual(null);
});
@@ -372,7 +373,7 @@ describe('test event validator', () => {
presenter: 3.2,
note: '1899-12-30T08:00:10.000Z',
};
const validated = validateEventv1(event);
const validated = validateEvent_v1(event);
expect(typeof validated.title).toEqual('string');
expect(typeof validated.subtitle).toEqual('string');
expect(typeof validated.presenter).toEqual('string');
@@ -384,7 +385,7 @@ describe('test event validator', () => {
timeStart: false,
timeEnd: '2',
};
const validated = validateEventv1(event);
const validated = validateEvent_v1(event);
expect(typeof validated.timeStart).toEqual('number');
expect(validated.timeStart).toEqual(0);
expect(typeof validated.timeEnd).toEqual('number');
@@ -395,7 +396,7 @@ describe('test event validator', () => {
const event = {
title: {},
};
const validated = validateEventv1(event);
const validated = validateEvent_v1(event);
expect(typeof validated.title).toEqual('string');
});
});
@@ -485,10 +486,34 @@ describe('test parseExcel function', () => {
},
];
const parsedData = await parseExcelv1(testdata);
const parsedData = await parseExcel_v1(testdata);
expect(parsedData.events).toBeDefined();
expect(parsedData.events).toStrictEqual(expectedParsedEvents);
expect(parsedData.events).toStrictEqual(expectedParsedEvents);
});
});
describe('test aliases import', () => {
it('imports a well defined alias', () => {});
const testData = {
events: [],
settings: {
app: 'ontime',
version: 1,
},
aliases: [
{
enabled: false,
alias: 'testalias',
pathAndParams: 'testpathAndParams',
},
],
};
const parsed = parseAliases_v1(testData, true);
expect(parsed.length).toBe(1);
// generates missing id
expect(parsed[0].id).toBeDefined();
});
+210 -128
View File
@@ -6,8 +6,8 @@ import {
block as blockDef,
} from '../models/eventsDefinition.js';
import { dbModelv1 } from '../models/dataModel.js';
import { generateId } from './generate_id.js';
import { excelDateStringToMillis } from './time.js';
import { generateId } from 'ontime-utils/generate_id.js';
import { excelDateStringToMillis } from 'ontime-utils/time.js';
export const EXCEL_MIME =
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet';
@@ -36,8 +36,8 @@ export const fileHandler = async (file) => {
// we only look at worksheets called ontime or event schedule
if (excelData?.data) {
const dataFromExcel = await parseExcelv1(excelData.data);
res.data = await parseJsonv1(dataFromExcel);
const dataFromExcel = await parseExcel_v1(excelData.data);
res.data = await parseJson_v1(dataFromExcel);
res.message = 'success';
} else {
console.log('Error: No sheets found named ontime or event schedule');
@@ -64,7 +64,7 @@ export const fileHandler = async (file) => {
if (uploadedJson.settings.version === 1) {
try {
res.data = await parseJsonv1(uploadedJson);
res.data = await parseJson_v1(uploadedJson);
res.message = 'success';
} catch (error) {
res = { error: true, message: `Error parsing file: ${error}` };
@@ -75,7 +75,7 @@ export const fileHandler = async (file) => {
}
// delete file
deleteFile(file);
await deleteFile(file);
return res;
};
@@ -85,7 +85,7 @@ export const fileHandler = async (file) => {
* @param {array} excelData - array with excel sheet
* @returns {object} - parsed object
*/
export const parseExcelv1 = async (excelData) => {
export const parseExcel_v1 = async (excelData) => {
let eventData = {
title: '',
url: '',
@@ -200,136 +200,27 @@ export const parseExcelv1 = async (excelData) => {
* @param {boolean} [enforce=false] - flag, tells to create an object anyway
* @returns {object} - parsed object
*/
export const parseJsonv1 = async (jsonData, enforce=false) => {
export const parseJson_v1 = async (jsonData, enforce = false) => {
if (!jsonData || typeof jsonData !== 'object') {
console.log('ERROR: Invalid JSON format');
return -1;
}
let numEntries = 0;
// object containing the parsed data
let returnData = {};
if ('events' in jsonData) {
console.log('Found events definition, importing...');
let events = [];
let ids = [];
for (const e of jsonData.events) {
// double check unique ids
if (ids.indexOf(e?.id) !== -1) {
console.log('ERROR: ID collision on import, skipping');
continue;
}
if (e.type === 'event') {
let event = validateEventv1(e);
if (event != null) {
events.push(event);
ids.push(event.id);
numEntries++;
}
} else if (e.type === 'delay') {
events.push({
...delayDef,
duration: e.duration,
id: e.id || generateId(),
});
numEntries++;
} else if (e.type === 'block') {
events.push({ ...blockDef, id: e.id || generateId() });
numEntries++;
} else {
console.log('ERROR: undefined event type, skipping');
}
}
// write to db
returnData.events = events;
console.log(`Uploaded file with ${numEntries} entries`);
} else if (enforce) {
returnData.events = [];
console.log(`Created events object in db`);
}
if ('event' in jsonData) {
console.log('Found event data, importing...');
const e = jsonData.event;
// filter known properties and write to db
returnData.event = {
...dbModelv1.event,
title: e.title || dbModelv1.event.title,
url: e.url || dbModelv1.event.url,
publicInfo: e.publicInfo || dbModelv1.event.publicInfo,
backstageInfo: e.backstageInfo || dbModelv1.event.backstageInfo,
endMessage: e.endMessage || dbModelv1.event.endMessage,
};
} else if (enforce) {
returnData.event = dbModelv1.event;
console.log(`Created event object in db`);
}
// parse Events
returnData.events = parseEvents_v1(jsonData);
// parse Event
returnData.event = parseEvent_v1(jsonData, enforce);
// Settings handled partially
if ('settings' in jsonData) {
console.log('Found settings definition, importing...');
const s = jsonData.settings;
// skip if file definition is missing
if (s.app == null || s.version == null) {
console.log('ERROR: unknown app version, skipping');
} else {
let settings = {
lock: s.lock || null,
};
// write to db
returnData.settings = {
...dbModelv1.settings,
...settings,
};
}
} else if (enforce) {
returnData.settings = dbModelv1.settings;
console.log(`Created settings object in db`);
}
returnData.settings = parseSettings_v1(jsonData, enforce);
// Import OSC settings if any
if ('osc' in jsonData) {
console.log('Found OSC definition, importing...');
const s = jsonData.osc;
let osc = {};
if (s.port) osc.port = s.port;
if (s.portOut) osc.portOut = s.portOut;
if (s.targetIP) osc.targetIP = s.targetIP;
if (s.enabled) osc.enabled = s.enabled;
// write to db
returnData.osc = {
...dbModelv1.osc,
...osc,
};
} else if (enforce) {
returnData.osc = dbModelv1.osc;
console.log(`Created osc object in db`);
}
returnData.osc = parseOsc_v1(jsonData, enforce);
// Import HTTP settings if any
if ('http' in jsonData) {
console.log('Found HTTP definition, importing...');
const h = jsonData.osc;
let http = {};
if (h.user) http.user = h.user;
if (h.pwd) http.pwd = h.pwd;
// write to db
returnData.http = {
...dbModelv1.http,
...http,
};
} else if (enforce) {
returnData.http = dbModelv1.http;
console.log(`Created http object in db`);
}
returnData.http = parseHttp_v1(jsonData, enforce);
// Import Aliases if any
returnData.aliases = parseAliases_v1(jsonData);
return returnData;
};
@@ -352,7 +243,7 @@ export const makeString = (val, fallback = '') => {
* @returns {object|null} - formatted object or null in case is invalid
*/
export const validateEventv1 = (eventArgs) => {
export const validateEvent_v1 = (eventArgs) => {
// ensure id is defined and unique
const id = eventArgs.id || generateId();
let event = null;
@@ -418,3 +309,194 @@ export const validateFile = (file) => {
return false;
}
};
/**
* Parse events array of an entry
* @param {object} data - data object
* @returns {object} - event object data
*/
export const parseEvents_v1 = (data) => {
let newEvents = [];
if ('events' in data) {
console.log('Found events definition, importing...');
let events = [];
let ids = [];
for (const e of data.events) {
// double check unique ids
if (ids.indexOf(e?.id) !== -1) {
console.log('ERROR: ID collision on import, skipping');
continue;
}
if (e.type === 'event') {
let event = validateEvent_v1(e);
if (event != null) {
events.push(event);
ids.push(event.id);
}
} else if (e.type === 'delay') {
events.push({
...delayDef,
duration: e.duration,
id: e.id || generateId(),
});
} else if (e.type === 'block') {
events.push({ ...blockDef, id: e.id || generateId() });
} else {
console.log('ERROR: undefined event type, skipping');
}
}
// write to db
newEvents = events;
console.log(`Uploaded file with ${events.length} entries`);
}
return newEvents;
};
/**
* Parse event portion of an entry
* @param {object} data - data object
* @param {boolean} enforce - whether to create a definition if one is missing
* @returns {object} - event object data
*/
export const parseEvent_v1 = (data, enforce) => {
let newEvent = {};
if ('event' in data) {
console.log('Found event data, importing...');
const e = data.event;
// filter known properties and write to db
newEvent = {
...dbModelv1.event,
title: e.title || dbModelv1.event.title,
url: e.url || dbModelv1.event.url,
publicInfo: e.publicInfo || dbModelv1.event.publicInfo,
backstageInfo: e.backstageInfo || dbModelv1.event.backstageInfo,
endMessage: e.endMessage || dbModelv1.event.endMessage,
};
} else if (enforce) {
newEvent = dbModelv1.event;
console.log(`Created event object in db`);
}
return newEvent;
};
/**
* Parse settings portion of an entry
* @param {object} data - data object
* @param {boolean} enforce - whether to create a definition if one is missing
* @returns {object} - event object data
*/
export const parseSettings_v1 = (data, enforce) => {
let newSettings = {};
if ('settings' in data) {
console.log('Found settings definition, importing...');
const s = data.settings;
// skip if file definition is missing
if (s.app == null || s.version == null) {
console.log('ERROR: unknown app version, skipping');
} else {
let settings = {
lock: s.lock || null,
pinCode: s.pinCode || null,
};
// write to db
newSettings = {
...dbModelv1.settings,
...settings,
};
}
} else if (enforce) {
newSettings = dbModelv1.settings;
console.log(`Created settings object in db`);
}
return newSettings;
};
/**
* Parse osc portion of an entry
* @param {object} data - data object
* @param {boolean} enforce - whether to create a definition if one is missing
* @returns {object} - event object data
*/
export const parseOsc_v1 = (data, enforce) => {
let newOsc = {};
if ('osc' in data) {
console.log('Found OSC definition, importing...');
const s = data.osc;
let osc = {};
if (s.port) osc.port = s.port;
if (s.portOut) osc.portOut = s.portOut;
if (s.targetIP) osc.targetIP = s.targetIP;
if (s.enabled) osc.enabled = s.enabled;
// write to db
newOsc = {
...dbModelv1.osc,
...osc,
};
} else if (enforce) {
newOsc = dbModelv1.osc;
console.log(`Created OSC object in db`);
}
return newOsc;
};
/**
* Parse Http portion of an entry
* @param {object} data - data object
* @param {boolean} enforce - whether to create a definition if one is missing
* @returns {object} - event object data
*/
export const parseHttp_v1 = (data, enforce) => {
let newHttp = {};
if ('http' in data) {
console.log('Found HTTP definition, importing...');
const h = data.osc;
let http = {};
if (h.user) http.user = h.user;
if (h.pwd) http.pwd = h.pwd;
// write to db
newHttp.http = {
...dbModelv1.http,
...http,
};
} else if (enforce) {
newHttp.http = dbModelv1.http;
console.log(`Created http object in db`);
}
return newHttp;
};
/**
* Parse aliases portion of an entry
* @param {object} data - data object
* @returns {object} - event object data
*/
export const parseAliases_v1 = (data) => {
let newAliases = [];
if ('aliases' in data) {
console.log('Found Aliases definition, importing...');
const ids = [];
for (const a of data.aliases) {
// double check unique ids
if (ids.indexOf(a?.id) !== -1) {
console.log('ERROR: ID collision on import, skipping');
continue;
}
const newAlias = {
id: a.id || generateId(),
enabled: a.enabled || false,
alias: a.alias || '',
pathAndParams: a.pathAndParams || '',
};
ids.push(newAlias.id);
newAliases.push(newAlias);
}
console.log(`Uploaded ${newAliases?.length || 0} alias(es)`);
}
return newAliases;
};
+27 -23
View File
@@ -460,10 +460,10 @@ engine.io-parser@~5.0.0:
dependencies:
base64-arraybuffer "~1.0.1"
engine.io@~6.0.0:
version "6.0.0"
resolved "https://registry.yarnpkg.com/engine.io/-/engine.io-6.0.0.tgz#2b993fcd73e6b3a6abb52b40b803651cd5747cf0"
integrity sha512-Ui7yl3JajEIaACg8MOUwWvuuwU7jepZqX3BKs1ho7NQRuP4LhN4XIykXhp8bEy+x/DhA0LBZZXYSCkZDqrwMMg==
engine.io@~6.1.0:
version "6.1.0"
resolved "https://registry.yarnpkg.com/engine.io/-/engine.io-6.1.0.tgz#459eab0c3724899d7b63a20c3a6835cf92857939"
integrity sha512-ErhZOVu2xweCjEfYcTdkCnEYUiZgkAcBBAhW4jbIvNG8SLU3orAqoJCiytZjYF7eTpVmmCrLDjLIEaPlUAs1uw==
dependencies:
"@types/cookie" "^0.4.1"
"@types/cors" "^2.8.12"
@@ -1059,10 +1059,10 @@ ms@2.1.2:
resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.2.tgz#d09d1f357b443f493382a8eb3ccd183872ae6009"
integrity sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==
multer@^1.4.2:
version "1.4.3"
resolved "https://registry.yarnpkg.com/multer/-/multer-1.4.3.tgz#4db352d6992e028ac0eacf7be45c6efd0264297b"
integrity sha512-np0YLKncuZoTzufbkM6wEKp68EhWJXcU6fq6QqrSwkckd2LlMgd1UqhUJLj6NS/5sZ8dE8LYDWslsltJznnXlg==
multer@^1.4.4:
version "1.4.4"
resolved "https://registry.yarnpkg.com/multer/-/multer-1.4.4.tgz#e2bc6cac0df57a8832b858d7418ccaa8ebaf7d8c"
integrity sha512-2wY2+xD4udX612aMqMcB8Ws2Voq6NIUPEtD1be6m411T4uDH/VtL9i//xvcyFlTVfRdaBsk7hV5tgrGQqhuBiw==
dependencies:
append-field "^1.0.0"
busboy "^0.2.11"
@@ -1073,7 +1073,7 @@ multer@^1.4.2:
type-is "^1.6.4"
xtend "^4.0.0"
nanoid@^3.1.22:
nanoid@^3.1.30:
version "3.1.30"
resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.1.30.tgz#63f93cc548d2a113dc5dfbc63bfa09e2b9b64362"
integrity sha512-zJpuPDwOv8D2zq2WRoMe1HsfZthVewpel9CAvTfc/2mBD1uUT/agc5f7GHGWXlYkFvi1mVxe4IjvP2HNrop7nQ==
@@ -1088,10 +1088,10 @@ negotiator@0.6.2:
resolved "https://registry.yarnpkg.com/negotiator/-/negotiator-0.6.2.tgz#feacf7ccf525a77ae9634436a64883ffeca346fb"
integrity sha512-hZXc7K2e+PgeI1eDBe/10Ard4ekbfrrqG8Ep+8Jmf4JID2bNg7NvCPOZN+kfF574pFQI7mum2AUqDidoKqcTOw==
node-osc@6.1.10:
version "6.1.10"
resolved "https://registry.yarnpkg.com/node-osc/-/node-osc-6.1.10.tgz#f618281f1f4fe91a41bc1414f58322369011e1ed"
integrity sha512-HiDNie6meSzfUcmso8jGyeQ1TTWC2G9FI/vO57H5JaNGAUCCh9X8wDzTZy0KpDTSBxxNC2Rgr8kCC7f0oJ/gww==
node-osc@6.1.11:
version "6.1.11"
resolved "https://registry.yarnpkg.com/node-osc/-/node-osc-6.1.11.tgz#c1d0700cb74e7fe141f4f99531318b8615e38d2f"
integrity sha512-bwFV/UKBBaSNK5pLYeA8Qx0GVc49fKRjmfMwMoypfPwb7RdhyRccW+tTNHcwr/OJ2b01uVKpnTPurSNs4fWnvw==
dependencies:
osc-min "^1.1.1"
@@ -1133,6 +1133,10 @@ once@^1.3.0:
dependencies:
wrappy "1"
"ontime-utils@link: ../server/utils":
version "0.0.0"
uid ""
optionator@^0.9.1:
version "0.9.1"
resolved "https://registry.yarnpkg.com/optionator/-/optionator-0.9.1.tgz#4f236a6373dae0566a6d43e1326674f50c291499"
@@ -1426,10 +1430,10 @@ shebang-regex@^3.0.0:
resolved "https://registry.yarnpkg.com/shebang-regex/-/shebang-regex-3.0.0.tgz#ae16f1644d873ecad843b0307b143362d4c42172"
integrity sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==
socket.io-adapter@~2.3.2:
version "2.3.2"
resolved "https://registry.yarnpkg.com/socket.io-adapter/-/socket.io-adapter-2.3.2.tgz#039cd7c71a52abad984a6d57da2c0b7ecdd3c289"
integrity sha512-PBZpxUPYjmoogY0aoaTmo1643JelsaS1CiAwNjRVdrI0X9Seuc19Y2Wife8k88avW6haG8cznvwbubAZwH4Mtg==
socket.io-adapter@~2.3.3:
version "2.3.3"
resolved "https://registry.yarnpkg.com/socket.io-adapter/-/socket.io-adapter-2.3.3.tgz#4d6111e4d42e9f7646e365b4f578269821f13486"
integrity sha512-Qd/iwn3VskrpNO60BeRyCyr8ZWw9CPZyitW4AQwmRZ8zCiyDiL+znRnWX6tDHXnWn1sJrM1+b6Mn6wEDJJ4aYQ==
socket.io-parser@~4.0.4:
version "4.0.4"
@@ -1440,16 +1444,16 @@ socket.io-parser@~4.0.4:
component-emitter "~1.3.0"
debug "~4.3.1"
socket.io@^4.3.1:
version "4.3.1"
resolved "https://registry.yarnpkg.com/socket.io/-/socket.io-4.3.1.tgz#c0aa14f3f916a8ab713e83a5bd20c16600245763"
integrity sha512-HC5w5Olv2XZ0XJ4gOLGzzHEuOCfj3G0SmoW3jLHYYh34EVsIr3EkW9h6kgfW+K3TFEcmYy8JcPWe//KUkBp5jA==
socket.io@4.4.0:
version "4.4.0"
resolved "https://registry.yarnpkg.com/socket.io/-/socket.io-4.4.0.tgz#8140a0db2c22235f88a6dceb867e4d5c9bd70507"
integrity sha512-bnpJxswR9ov0Bw6ilhCvO38/1WPtE3eA2dtxi2Iq4/sFebiDJQzgKNYA7AuVVdGW09nrESXd90NbZqtDd9dzRQ==
dependencies:
accepts "~1.3.4"
base64id "~2.0.0"
debug "~4.3.2"
engine.io "~6.0.0"
socket.io-adapter "~2.3.2"
engine.io "~6.1.0"
socket.io-adapter "~2.3.3"
socket.io-parser "~4.0.4"
ssf@~0.11.2:
@@ -0,0 +1,28 @@
import { generateId } from '../generate_id.js';
test('generate a valid 5 digit id', () => {
const id = generateId();
expect(id.length).toBe(5);
});
test('generate 100 with less than 110 attempts', () => {
const ids = new Set();
let attempts = 1;
while (ids.size < 100) {
ids.add(generateId());
attempts++;
}
expect(attempts).toBeLessThan(105);
});
describe('generate 1000 with less than 1020 attempts', () => {
const ids = new Set();
let attempts = 1;
while (ids.size < 1000) {
ids.add(generateId());
attempts++;
}
expect(attempts).toBeLessThan(1020);
});
@@ -1,6 +1,6 @@
import { excelDateStringToMillis, stringFromMillis } from '../time.js';
describe('test string to milis function', () => {
describe('test string to millis function', () => {
it('test with null values', () => {
const t = { val: null, result: '...' };
expect(stringFromMillis(t.val)).toBe(t.result);
@@ -74,6 +74,6 @@ describe('test excel date parser', () => {
it('handles an invalid date string', () => {
const s = 'hello';
expect(excelDateStringToMillis(s)).toBe(null);
expect(excelDateStringToMillis(s)).toBe(0);
});
});
+11
View File
@@ -0,0 +1,11 @@
{
"name": "ontime-utils",
"type": "module",
"private": true,
"version": "1.0.0",
"description": "ontime app utility functions",
"dependencies": {
"nanoid": "^3.1.30"
},
"devDependencies": {}
}
+67
View File
@@ -0,0 +1,67 @@
const mts = 1000; // millis to seconds
const mtm = 1000 * 60; // millis to minutes
const mth = 1000 * 60 * 60; // millis to hours
/**
* Returns current time in milliseconds
* @returns {number}
*/
export const nowInMillis = () => {
const now = new Date();
// extract milliseconds since midnight
let elapsed = now.getHours() * 3600000;
elapsed += now.getMinutes() * 60000;
elapsed += now.getSeconds() * 1000;
elapsed += now.getMilliseconds();
return elapsed;
};
/**
* @description Converts milliseconds to string representing time
* @param {number} ms - time in milliseconds
* @param {boolean} showSeconds - weather to show the seconds
* @param {string} delim - character between HH MM SS
* @param {string} ifNull - what to return if value is null
* @returns {string} String representing time 00:12:02
*/
export const stringFromMillis = (
ms,
showSeconds = true,
delim = ':',
ifNull = '...'
) => {
if (ms == null || isNaN(ms)) return ifNull;
const isNegative = ms < 0 ? '-' : '';
const millis = Math.abs(ms);
const showWith0 = (value) => (value < 10 ? `0${value}` : value);
const hours = showWith0(Math.floor(((millis / mth) % 60) % 24));
const minutes = showWith0(Math.floor((millis / mtm) % 60));
const seconds = showWith0(Math.floor((millis / mts) % 60));
return showSeconds
? `${isNegative}${
parseInt(hours) ? `${hours}${delim}` : `00${delim}`
}${minutes}${delim}${seconds}`
: `${isNegative}${parseInt(hours) ? `${hours}` : '00'}${delim}${minutes}`;
};
/**
* @description Converts an excel date to milliseconds
* @argument {string} excelDate - excel string date
* @returns {number} - time in milliseconds
*/
export const excelDateStringToMillis = (excelDate) => {
const date = new Date(excelDate);
if (date instanceof Date && !isNaN(date)) {
const h = date.getUTCHours();
const m = date.getMinutes();
const s = date.getSeconds();
return h * mth + m * mtm + s * mts;
}
return 0;
};
+8
View File
@@ -0,0 +1,8 @@
# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
# yarn lockfile v1
nanoid@^3.1.30:
version "3.1.30"
resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.1.30.tgz#63f93cc548d2a113dc5dfbc63bfa09e2b9b64362"
integrity sha512-zJpuPDwOv8D2zq2WRoMe1HsfZthVewpel9CAvTfc/2mBD1uUT/agc5f7GHGWXlYkFvi1mVxe4IjvP2HNrop7nQ==
+2 -2
View File
@@ -1562,7 +1562,7 @@ ejs@^3.1.6:
dependencies:
jake "^10.6.1"
electron-builder@^22.11.3:
electron-builder@^22.14.5:
version "22.14.5"
resolved "https://registry.yarnpkg.com/electron-builder/-/electron-builder-22.14.5.tgz#3a25547bd4fe3728d4704da80956a794c5c31496"
integrity sha512-N73hSbXFz6Mz5Z6h6C5ly6CB+dUN6k1LuCDJjI8VF47bMXv/QE0HE+Kkb0GPKqTqM7Hsk/yIYX+kHCfSkR5FGg==
@@ -2857,7 +2857,7 @@ jest-worker@^27.4.5:
merge-stream "^2.0.0"
supports-color "^8.0.0"
jest@^27.0.4:
jest@^27.4.5:
version "27.4.5"
resolved "https://registry.yarnpkg.com/jest/-/jest-27.4.5.tgz#66e45acba44137fac26be9d3cc5bb031e136dc0f"
integrity sha512-uT5MiVN3Jppt314kidCk47MYIRilJjA/l2mxwiuzzxGUeJIvA8/pDaJOAX5KWvjAo7SCydcW0/4WEtgbLMiJkg==