mirror of
https://github.com/cpvalente/ontime.git
synced 2026-08-03 06:28:01 +00:00
Compare commits
5 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| acb5d8ae08 | |||
| 5ca8202368 | |||
| ba782d5d22 | |||
| 48093b8651 | |||
| c3f18feaae |
@@ -9,5 +9,26 @@
|
||||
"extends": [
|
||||
"eslint:recommended"
|
||||
],
|
||||
"rules": {}
|
||||
"rules": {
|
||||
// disallow certain object properties
|
||||
// https://eslint.org/docs/rules/no-restricted-properties
|
||||
"no-restricted-properties": [
|
||||
"error",
|
||||
{
|
||||
"object": "global",
|
||||
"property": "isNaN",
|
||||
"message": "Please use Number.isNaN instead"
|
||||
},
|
||||
{
|
||||
"object": "self",
|
||||
"property": "isNaN",
|
||||
"message": "Please use Number.isNaN instead"
|
||||
},
|
||||
{
|
||||
"object": "window",
|
||||
"property": "isNaN",
|
||||
"message": "Please use Number.isNaN instead"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -56,9 +56,15 @@ jobs:
|
||||
|
||||
# App
|
||||
- name: Electron - Install dependencies
|
||||
run: yarn install
|
||||
run: yarn install && yarn make && yarn setdb
|
||||
working-directory: ./server
|
||||
|
||||
- name: Electron - Run tests
|
||||
run: yarn test
|
||||
working-directory: ./server
|
||||
|
||||
- name: Cypress run
|
||||
uses: cypress-io/github-action@v2
|
||||
with:
|
||||
working-directory: ./server
|
||||
start: yarn cypress
|
||||
@@ -7,6 +7,7 @@ node_modules/
|
||||
|
||||
# testing
|
||||
coverage/
|
||||
*.mp4
|
||||
|
||||
# production
|
||||
build/
|
||||
|
||||
+2
-1
@@ -3,5 +3,6 @@
|
||||
"tabWidth": 2,
|
||||
"semi": true,
|
||||
"singleQuote": true,
|
||||
"jsxSingleQuote": true
|
||||
"jsxSingleQuote": true,
|
||||
"printWidth": 100
|
||||
}
|
||||
@@ -8,5 +8,6 @@
|
||||
"jest/no-mocks-import": "warn",
|
||||
"no-useless-concat": "warn",
|
||||
"prefer-template": "warn"
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -48,6 +48,7 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@testing-library/react-hooks": "^7.0.2",
|
||||
"prop-types": "^15.8.1",
|
||||
"react-test-renderer": "^17.0.2",
|
||||
"sass": "^1.44.0"
|
||||
}
|
||||
|
||||
+24
-8
@@ -9,9 +9,14 @@ 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 TimerView = lazy(() =>
|
||||
import('features/viewers/timer/Timer')
|
||||
);
|
||||
const MinimalTimerView = lazy(() =>
|
||||
import('features/viewers/timer/MinimalTimer')
|
||||
);
|
||||
|
||||
const StageManager = lazy(() =>
|
||||
import('features/viewers/backstage/StageManager')
|
||||
);
|
||||
@@ -22,7 +27,8 @@ const Lower = lazy(() =>
|
||||
const Pip = lazy(() => import('features/viewers/production/Pip'));
|
||||
const StudioClock = lazy(() => import('features/viewers/studio/StudioClock'));
|
||||
|
||||
const SPresenter = withSocket(PresenterView);
|
||||
const STimer = withSocket(TimerView);
|
||||
const SMinimalTimer = withSocket(MinimalTimerView);
|
||||
const SStageManager = withSocket(StageManager);
|
||||
const SPublic = withSocket(Public);
|
||||
const SLowerThird = withSocket(Lower);
|
||||
@@ -36,6 +42,8 @@ function App() {
|
||||
|
||||
// Handle keyboard shortcuts
|
||||
const handleKeyPress = useCallback((e) => {
|
||||
// handle held key
|
||||
if (e.repeat) return;
|
||||
// check if the alt key is pressed
|
||||
if (e.altKey) {
|
||||
if (e.key === 't' || e.key === 'T') {
|
||||
@@ -75,11 +83,19 @@ function App() {
|
||||
<ErrorBoundary>
|
||||
<Suspense fallback={null}>
|
||||
<Routes>
|
||||
<Route path='/' element={<SPresenter />} />
|
||||
<Route path='/' element={<STimer />} />
|
||||
<Route path='/speaker' element={<STimer />} />
|
||||
<Route path='/presenter' element={<STimer />} />
|
||||
<Route path='/stage' element={<STimer />} />
|
||||
<Route path='/timer' element={<STimer />} />
|
||||
|
||||
<Route path='/minimal' element={<SMinimalTimer />} />
|
||||
<Route path='/minimalTimer' element={<SMinimalTimer />} />
|
||||
<Route path='/simpleTimer' element={<SMinimalTimer />} />
|
||||
|
||||
<Route path='/sm' element={<SStageManager />} />
|
||||
<Route path='/speaker' element={<SPresenter />} />
|
||||
<Route path='/presenter' element={<SPresenter />} />
|
||||
<Route path='/stage' element={<SPresenter />} />
|
||||
<Route path='/backstage' element={<SStageManager />} />
|
||||
|
||||
<Route path='/public' element={<SPublic />} />
|
||||
<Route path='/pip' element={<SPip />} />
|
||||
<Route path='/studio' element={<SStudio />} />
|
||||
@@ -95,7 +111,7 @@ function App() {
|
||||
}
|
||||
/>
|
||||
{/* Send to default if nothing found */}
|
||||
<Route path='*' element={<SPresenter />} />
|
||||
<Route path='*' element={<STimer />} />
|
||||
</Routes>
|
||||
</Suspense>
|
||||
</ErrorBoundary>
|
||||
|
||||
@@ -25,7 +25,7 @@ export const oscPlaceholderSettings = {
|
||||
port: '',
|
||||
portOut: '',
|
||||
targetIP: '',
|
||||
enabled: true,
|
||||
enabled: false,
|
||||
};
|
||||
|
||||
export const httpPlaceholder = {
|
||||
@@ -66,7 +66,7 @@ export const ontimeVars = [
|
||||
},
|
||||
{
|
||||
name: '$presenter',
|
||||
description: 'Current presenter',
|
||||
description: 'Current timer',
|
||||
},
|
||||
{
|
||||
name: '$subtitle',
|
||||
@@ -78,7 +78,7 @@ export const ontimeVars = [
|
||||
},
|
||||
{
|
||||
name: '$next-presenter',
|
||||
description: 'Next presenter',
|
||||
description: 'Next timer',
|
||||
},
|
||||
{
|
||||
name: '$next-subtitle',
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
/**
|
||||
* @description Validates two time entries
|
||||
* @param {number} timeStart
|
||||
* @param {number} timeEnd
|
||||
* @returns {{catch: string, value: boolean}}
|
||||
*/
|
||||
export const validateTimes = (timeStart, timeEnd) => {
|
||||
let validate = { value: true, catch: '' };
|
||||
if (timeStart > timeEnd) {
|
||||
validate.catch = 'Start time later than end time';
|
||||
}
|
||||
return validate;
|
||||
};
|
||||
@@ -2,7 +2,7 @@ const dummy = new Date();
|
||||
|
||||
export const sampleData = {
|
||||
presenterMessage: {
|
||||
text: 'Only the presenter sees this',
|
||||
text: 'Only the timer sees this',
|
||||
active: false,
|
||||
},
|
||||
publicMessage: {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { IconButton } from '@chakra-ui/button';
|
||||
import { useState } from 'react';
|
||||
import { FiMinus } from 'react-icons/fi';
|
||||
import { IoRemove } from 'react-icons/io5';
|
||||
import { Tooltip } from '@chakra-ui/tooltip';
|
||||
|
||||
export default function DeleteIconBtn(props) {
|
||||
const { actionHandler, ...rest } = props;
|
||||
@@ -12,15 +13,17 @@ export default function DeleteIconBtn(props) {
|
||||
};
|
||||
|
||||
return (
|
||||
<IconButton
|
||||
size={props.size || 'xs'}
|
||||
icon={<FiMinus />}
|
||||
colorScheme='red'
|
||||
onClick={handleClick}
|
||||
_focus={{ boxShadow: 'none' }}
|
||||
disabled={loading}
|
||||
isLoading={loading}
|
||||
{...rest}
|
||||
/>
|
||||
<Tooltip label='Delete'>
|
||||
<IconButton
|
||||
size={props.size || 'xs'}
|
||||
icon={<IoRemove />}
|
||||
colorScheme='red'
|
||||
onClick={handleClick}
|
||||
_focus={{ boxShadow: 'none' }}
|
||||
disabled={loading}
|
||||
isLoading={loading}
|
||||
{...rest}
|
||||
/>
|
||||
</Tooltip>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
import { IoCloseSharp, IoCheckmarkSharp } from 'react-icons/io5';
|
||||
import { Button } from '@chakra-ui/button';
|
||||
|
||||
export default function EnableBtn(props) {
|
||||
const { active, text, actionHandler } = props;
|
||||
return (
|
||||
<Button
|
||||
size={props.size || 'xs'}
|
||||
leftIcon={active ? <IoCheckmarkSharp /> : <IoCloseSharp />}
|
||||
colorScheme='blue'
|
||||
variant={active ? 'solid' : 'outline'}
|
||||
onClick={actionHandler}
|
||||
_focus={{ boxShadow: 'none' }}
|
||||
>
|
||||
{text}
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
@@ -1,18 +1,21 @@
|
||||
import { IconButton } from '@chakra-ui/button';
|
||||
import { FiSkipForward } from 'react-icons/fi';
|
||||
import { IoPlaySkipForward } from 'react-icons/io5';
|
||||
import { Tooltip } from '@chakra-ui/tooltip';
|
||||
|
||||
export default function NextIconBtn(props) {
|
||||
const { clickhandler, ...rest } = props;
|
||||
return (
|
||||
<IconButton
|
||||
icon={<FiSkipForward />}
|
||||
colorScheme='whiteAlpha'
|
||||
backgroundColor='#ffffff11'
|
||||
variant='outline'
|
||||
onClick={clickhandler}
|
||||
width={90}
|
||||
_focus={{ boxShadow: 'none' }}
|
||||
{...rest}
|
||||
/>
|
||||
<Tooltip label='Next event' openDelay={500} shouldWrapChildren={props.disabled}>
|
||||
<IconButton
|
||||
icon={<IoPlaySkipForward size='22px' />}
|
||||
colorScheme='whiteAlpha'
|
||||
backgroundColor='#ffffff11'
|
||||
variant='outline'
|
||||
onClick={clickhandler}
|
||||
width={90}
|
||||
_focus={{ boxShadow: 'none' }}
|
||||
{...rest}
|
||||
/>
|
||||
</Tooltip>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
import { IconButton } from '@chakra-ui/button';
|
||||
import { IoMicSharp, IoMicOffOutline } from 'react-icons/io5';
|
||||
import { Tooltip } from '@chakra-ui/tooltip';
|
||||
|
||||
export default function OnAirIconBtn(props) {
|
||||
const { actionHandler, active, ...rest } = props;
|
||||
return (
|
||||
<Tooltip label={active ? 'Go Off Air' : 'Go On Air'} openDelay={500}>
|
||||
<IconButton
|
||||
size={props.size || 'xs'}
|
||||
icon={active ? <IoMicSharp size='24px' /> : <IoMicOffOutline size='24px' />}
|
||||
colorScheme='blue'
|
||||
variant={active ? 'solid' : 'outline'}
|
||||
onClick={() =>
|
||||
actionHandler('update', { field: 'isPublic', value: !active })
|
||||
}
|
||||
_focus={{ boxShadow: 'none' }}
|
||||
{...rest}
|
||||
/>
|
||||
</Tooltip>
|
||||
);
|
||||
}
|
||||
@@ -1,17 +1,20 @@
|
||||
import { IconButton } from '@chakra-ui/button';
|
||||
import { FiPause } from 'react-icons/fi';
|
||||
import { IoPause } from 'react-icons/io5';
|
||||
import { Tooltip } from '@chakra-ui/tooltip';
|
||||
|
||||
export default function PauseIconBtn(props) {
|
||||
const { clickhandler, active, ...rest } = props;
|
||||
return (
|
||||
<IconButton
|
||||
icon={<FiPause />}
|
||||
colorScheme='orange'
|
||||
variant={active ? 'solid' : 'outline'}
|
||||
onClick={clickhandler}
|
||||
width={120}
|
||||
_focus={{ boxShadow: 'none' }}
|
||||
{...rest}
|
||||
/>
|
||||
<Tooltip label='Pause timer' openDelay={500} shouldWrapChildren={props.disabled}>
|
||||
<IconButton
|
||||
icon={<IoPause size='24px' />}
|
||||
colorScheme='orange'
|
||||
variant={active ? 'solid' : 'outline'}
|
||||
onClick={clickhandler}
|
||||
width={120}
|
||||
_focus={{ boxShadow: 'none' }}
|
||||
{...rest}
|
||||
/>
|
||||
</Tooltip>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,18 +1,21 @@
|
||||
import { IconButton } from '@chakra-ui/button';
|
||||
import { FiSkipBack } from 'react-icons/fi';
|
||||
import { IoPlaySkipBack } from 'react-icons/io5';
|
||||
import { Tooltip } from '@chakra-ui/tooltip';
|
||||
|
||||
export default function PrevIconBtn(props) {
|
||||
const { clickhandler, ...rest } = props;
|
||||
return (
|
||||
<IconButton
|
||||
icon={<FiSkipBack />}
|
||||
colorScheme='whiteAlpha'
|
||||
backgroundColor='#ffffff11'
|
||||
variant='outline'
|
||||
onClick={clickhandler}
|
||||
width={90}
|
||||
_focus={{ boxShadow: 'none' }}
|
||||
{...rest}
|
||||
/>
|
||||
<Tooltip label='Previous event' openDelay={500} shouldWrapChildren={props.disabled}>
|
||||
<IconButton
|
||||
icon={<IoPlaySkipBack size='22px' />}
|
||||
colorScheme='whiteAlpha'
|
||||
backgroundColor='#ffffff11'
|
||||
variant='outline'
|
||||
onClick={clickhandler}
|
||||
width={90}
|
||||
_focus={{ boxShadow: 'none' }}
|
||||
{...rest}
|
||||
/>
|
||||
</Tooltip>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,18 +1,21 @@
|
||||
import { IconButton } from '@chakra-ui/button';
|
||||
import { FiRefreshCcw } from 'react-icons/fi';
|
||||
import { IoReload } from 'react-icons/io5';
|
||||
import { Tooltip } from '@chakra-ui/tooltip';
|
||||
|
||||
export default function ReloadIconButton(props) {
|
||||
const { clickhandler, ...rest } = props;
|
||||
return (
|
||||
<IconButton
|
||||
icon={<FiRefreshCcw />}
|
||||
colorScheme='whiteAlpha'
|
||||
backgroundColor='#ffffff05'
|
||||
variant='outline'
|
||||
onClick={clickhandler}
|
||||
width={90}
|
||||
_focus={{ boxShadow: 'none' }}
|
||||
{...rest}
|
||||
/>
|
||||
<Tooltip label='Reload event' openDelay={500} shouldWrapChildren={props.disabled}>
|
||||
<IconButton
|
||||
icon={<IoReload size='22px' />}
|
||||
colorScheme='whiteAlpha'
|
||||
backgroundColor='#ffffff05'
|
||||
variant='outline'
|
||||
onClick={clickhandler}
|
||||
width={90}
|
||||
_focus={{ boxShadow: 'none' }}
|
||||
{...rest}
|
||||
/>
|
||||
</Tooltip>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,17 +1,20 @@
|
||||
import { IconButton } from '@chakra-ui/button';
|
||||
import { FiClock } from 'react-icons/fi';
|
||||
import { IoTimeOutline } from 'react-icons/io5';
|
||||
import { Tooltip } from '@chakra-ui/tooltip';
|
||||
|
||||
export default function RollIconBtn(props) {
|
||||
const { clickhandler, active, ...rest } = props;
|
||||
return (
|
||||
<IconButton
|
||||
icon={<FiClock />}
|
||||
colorScheme='blue'
|
||||
variant={active ? 'solid' : 'outline'}
|
||||
onClick={clickhandler}
|
||||
width={120}
|
||||
_focus={{ boxShadow: 'none' }}
|
||||
{...rest}
|
||||
/>
|
||||
<Tooltip label='Roll mode' openDelay={500} shouldWrapChildren={props.disabled}>
|
||||
<IconButton
|
||||
icon={<IoTimeOutline size='24px' />}
|
||||
colorScheme='blue'
|
||||
variant={active ? 'solid' : 'outline'}
|
||||
onClick={clickhandler}
|
||||
width={120}
|
||||
_focus={{ boxShadow: 'none' }}
|
||||
{...rest}
|
||||
/>
|
||||
</Tooltip>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,17 +1,20 @@
|
||||
import { IconButton } from '@chakra-ui/button';
|
||||
import { FiPlay } from 'react-icons/fi';
|
||||
import { IoPlay } from 'react-icons/io5';
|
||||
import { Tooltip } from '@chakra-ui/tooltip';
|
||||
|
||||
export default function StartIconBtn(props) {
|
||||
const { clickhandler, active, ...rest } = props;
|
||||
return (
|
||||
<IconButton
|
||||
icon={<FiPlay />}
|
||||
colorScheme='green'
|
||||
variant={active ? 'solid' : 'outline'}
|
||||
onClick={clickhandler}
|
||||
width={120}
|
||||
_focus={{ boxShadow: 'none' }}
|
||||
{...rest}
|
||||
/>
|
||||
<Tooltip label='Start timer' openDelay={500} shouldWrapChildren={props.disabled}>
|
||||
<IconButton
|
||||
icon={<IoPlay size='24px' />}
|
||||
colorScheme='green'
|
||||
variant={active ? 'solid' : 'outline'}
|
||||
onClick={clickhandler}
|
||||
width={120}
|
||||
_focus={{ boxShadow: 'none' }}
|
||||
{...rest}
|
||||
/>
|
||||
</Tooltip>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,18 +1,20 @@
|
||||
import { IconButton } from '@chakra-ui/button';
|
||||
import { FiXOctagon } from 'react-icons/fi';
|
||||
import { IoStop } from 'react-icons/io5';
|
||||
import { Tooltip } from '@chakra-ui/tooltip';
|
||||
|
||||
export default function UnloadIconBtn(props) {
|
||||
const { clickhandler, ...rest } = props;
|
||||
return (
|
||||
<IconButton
|
||||
icon={<FiXOctagon />}
|
||||
colorScheme='red'
|
||||
backgroundColor='#ff000022'
|
||||
variant='outline'
|
||||
onClick={clickhandler}
|
||||
width={90}
|
||||
_focus={{ boxShadow: 'none' }}
|
||||
{...rest}
|
||||
/>
|
||||
<Tooltip label='Unload event' openDelay={500} shouldWrapChildren={props.disabled}>
|
||||
<IconButton
|
||||
icon={<IoStop size='22px' />}
|
||||
colorScheme='red'
|
||||
variant='outline'
|
||||
onClick={clickhandler}
|
||||
width={90}
|
||||
_focus={{ boxShadow: 'none' }}
|
||||
{...rest}
|
||||
/>
|
||||
</Tooltip>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,19 +1,22 @@
|
||||
import { IconButton } from '@chakra-ui/button';
|
||||
import { FiSun } from 'react-icons/fi';
|
||||
import { IoSunny } from 'react-icons/io5';
|
||||
import { Tooltip } from '@chakra-ui/tooltip';
|
||||
|
||||
export default function VisibleIconBtn(props) {
|
||||
const { actionHandler, active, ...rest } = props;
|
||||
return (
|
||||
<IconButton
|
||||
size={props.size || 'xs'}
|
||||
icon={<FiSun />}
|
||||
colorScheme='blue'
|
||||
variant={active ? 'solid' : 'outline'}
|
||||
onClick={() =>
|
||||
actionHandler('update', { field: 'isPublic', value: !active })
|
||||
}
|
||||
_focus={{ boxShadow: 'none' }}
|
||||
{...rest}
|
||||
/>
|
||||
<Tooltip label={active ? 'Make invisible' : 'Make visible'} openDelay={500}>
|
||||
<IconButton
|
||||
size={props.size || 'xs'}
|
||||
icon={<IoSunny size={'18px'}/>}
|
||||
colorScheme='blue'
|
||||
variant={active ? 'solid' : 'outline'}
|
||||
onClick={() =>
|
||||
actionHandler('update', { field: 'isPublic', value: !active })
|
||||
}
|
||||
_focus={{ boxShadow: 'none' }}
|
||||
{...rest}
|
||||
/>
|
||||
</Tooltip>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,15 +1,16 @@
|
||||
import { memo } from 'react';
|
||||
import { formatDisplay } from 'common/utils/dateConfig';
|
||||
import PropTypes from 'prop-types';
|
||||
import styles from './Countdown.module.css';
|
||||
|
||||
const Countdown = ({ time, small, negative, hideZeroHours }) => {
|
||||
const Countdown = ({ time, small, isNegative, hideZeroHours }) => {
|
||||
// prepare display string
|
||||
const display =
|
||||
time != null && !isNaN(time)
|
||||
? formatDisplay(time, hideZeroHours)
|
||||
: '-- : -- : --';
|
||||
|
||||
const colour = negative ? '#ff7597' : '#fffffa';
|
||||
const colour = isNegative ? '#ff7597' : '#fffffa';
|
||||
|
||||
return (
|
||||
<div
|
||||
@@ -22,3 +23,10 @@ const Countdown = ({ time, small, negative, hideZeroHours }) => {
|
||||
};
|
||||
|
||||
export default memo(Countdown);
|
||||
|
||||
Countdown.propTypes = {
|
||||
time: PropTypes.number.isRequired,
|
||||
small: PropTypes.bool,
|
||||
isNegative: PropTypes.bool,
|
||||
hideZeroHour: PropTypes.bool,
|
||||
};
|
||||
|
||||
@@ -19,7 +19,6 @@ class ErrorBoundary extends React.Component {
|
||||
error: error,
|
||||
errorInfo: info,
|
||||
});
|
||||
// TODO: Log the error to an error reporting service
|
||||
this.context.emitError(error.toString());
|
||||
}
|
||||
|
||||
|
||||
@@ -1,27 +1,33 @@
|
||||
import EditableTimer from 'common/input/EditableTimer';
|
||||
import { useContext } from 'react';
|
||||
import { LoggingContext } from '../../../app/context/LoggingContext';
|
||||
import { validateTimes } from '../../../app/entryValidator';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
export default function EventTimes(props) {
|
||||
const { actionHandler, delay, timeStart, timeEnd } = props;
|
||||
const { actionHandler, delay, timeStart, timeEnd, previousEnd } = props;
|
||||
const { emitWarning } = useContext(LoggingContext);
|
||||
|
||||
const handleValidate = (entry, v) => {
|
||||
// we dont enforce validation here
|
||||
|
||||
if (v == null || timeStart == null || timeEnd == null) return true;
|
||||
const handleValidate = (entry, val) => {
|
||||
if (val == null || timeStart == null || timeEnd == null) return true;
|
||||
if (timeStart === 0) return true;
|
||||
|
||||
let validate = { value: true, catch: '' };
|
||||
if (entry === 'timeStart' && v > timeEnd) {
|
||||
validate.catch = 'Start time later than end time';
|
||||
} else if (entry === 'timeEnd' && v < timeStart) {
|
||||
validate.catch = 'End time earlier than start time';
|
||||
let start = timeStart;
|
||||
let end = timeEnd;
|
||||
if (entry === 'timeStart') {
|
||||
start = val;
|
||||
} else if (entry === 'timeEnd') {
|
||||
end = val;
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
|
||||
if (validate.catch !== '')
|
||||
emitWarning(`Time Input Warning: ${validate.catch}`);
|
||||
return validate.value;
|
||||
const valid = validateTimes(start, end);
|
||||
// give warning but not enforce validation
|
||||
if (!valid.value) {
|
||||
emitWarning(`Time Input Warning: ${valid.catch}`);
|
||||
}
|
||||
return valid.value;
|
||||
};
|
||||
|
||||
return (
|
||||
@@ -32,6 +38,7 @@ export default function EventTimes(props) {
|
||||
actionHandler={actionHandler}
|
||||
time={timeStart}
|
||||
delay={delay}
|
||||
previousEnd={previousEnd}
|
||||
/>
|
||||
<EditableTimer
|
||||
name='timeEnd'
|
||||
@@ -39,7 +46,16 @@ export default function EventTimes(props) {
|
||||
actionHandler={actionHandler}
|
||||
time={timeEnd}
|
||||
delay={delay}
|
||||
previousEnd={previousEnd}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
EventTimes.propTypes = {
|
||||
actionHandler: PropTypes.func.isRequired,
|
||||
delay: PropTypes.number.isRequired,
|
||||
timeStart: PropTypes.number.isRequired,
|
||||
timeEnd: PropTypes.number.isRequired,
|
||||
previousEnd: PropTypes.number.isRequired,
|
||||
};
|
||||
|
||||
@@ -2,6 +2,8 @@ import EditableTimer from 'common/input/EditableTimer';
|
||||
import { stringFromMillis } from 'ontime-utils/time';
|
||||
import { useContext } from 'react';
|
||||
import { LoggingContext } from '../../../app/context/LoggingContext';
|
||||
import { validateTimes } from '../../../app/entryValidator';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
const label = {
|
||||
fontSize: '0.75em',
|
||||
@@ -9,8 +11,7 @@ const label = {
|
||||
};
|
||||
|
||||
const TimesDelayed = (props) => {
|
||||
const { handleValidate, actionHandler, delay, timeStart, timeEnd, duration } =
|
||||
props;
|
||||
const { handleValidate, actionHandler, delay, timeStart, timeEnd, duration, previousEnd } = props;
|
||||
|
||||
const scheduledStart = stringFromMillis(timeStart, false);
|
||||
const scheduledEnd = stringFromMillis(timeEnd, false);
|
||||
@@ -26,6 +27,7 @@ const TimesDelayed = (props) => {
|
||||
actionHandler={actionHandler}
|
||||
time={timeStart}
|
||||
delay={delay}
|
||||
previousEnd={previousEnd}
|
||||
/>
|
||||
<span style={label}>
|
||||
End <span>{scheduledEnd}</span>
|
||||
@@ -36,6 +38,7 @@ const TimesDelayed = (props) => {
|
||||
actionHandler={actionHandler}
|
||||
time={timeEnd}
|
||||
delay={delay}
|
||||
previousEnd={previousEnd}
|
||||
/>
|
||||
<span style={label}>Duration</span>
|
||||
<EditableTimer
|
||||
@@ -44,13 +47,24 @@ const TimesDelayed = (props) => {
|
||||
actionHandler={actionHandler}
|
||||
time={duration}
|
||||
delay={0}
|
||||
previousEnd={previousEnd}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
TimesDelayed.propTypes = {
|
||||
handleValidate: PropTypes.func.isRequired,
|
||||
actionHandler: PropTypes.func.isRequired,
|
||||
delay: PropTypes.number.isRequired,
|
||||
timeStart: PropTypes.number.isRequired,
|
||||
timeEnd: PropTypes.number.isRequired,
|
||||
duration: PropTypes.number.isRequired,
|
||||
previousEnd: PropTypes.number.isRequired,
|
||||
};
|
||||
|
||||
const Times = (props) => {
|
||||
const { handleValidate, actionHandler, timeStart, timeEnd, duration } = props;
|
||||
const { handleValidate, actionHandler, timeStart, timeEnd, duration, previousEnd } = props;
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -61,6 +75,7 @@ const Times = (props) => {
|
||||
actionHandler={actionHandler}
|
||||
time={timeStart}
|
||||
delay={0}
|
||||
previousEnd={previousEnd}
|
||||
/>
|
||||
<span style={label}>End</span>
|
||||
<EditableTimer
|
||||
@@ -69,6 +84,7 @@ const Times = (props) => {
|
||||
actionHandler={actionHandler}
|
||||
time={timeEnd}
|
||||
delay={0}
|
||||
previousEnd={previousEnd}
|
||||
/>
|
||||
<span style={label}>Duration</span>
|
||||
<EditableTimer
|
||||
@@ -77,52 +93,73 @@ const Times = (props) => {
|
||||
actionHandler={actionHandler}
|
||||
time={duration}
|
||||
delay={0}
|
||||
previousEnd={previousEnd}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
Times.propTypes = {
|
||||
handleValidate: PropTypes.func.isRequired,
|
||||
actionHandler: PropTypes.func.isRequired,
|
||||
timeStart: PropTypes.number.isRequired,
|
||||
timeEnd: PropTypes.number.isRequired,
|
||||
duration: PropTypes.number.isRequired,
|
||||
previousEnd: PropTypes.number.isRequired,
|
||||
};
|
||||
|
||||
export default function EventTimesVertical(props) {
|
||||
const { delay, timeStart, timeEnd, duration } = props;
|
||||
const { delay, timeStart, timeEnd, duration, previousEnd } = props;
|
||||
const { emitWarning } = useContext(LoggingContext);
|
||||
|
||||
const handleValidate = (entry, v) => {
|
||||
// we dont enforce validation here
|
||||
|
||||
if (v == null || timeStart == null || timeEnd == null) return true;
|
||||
const handleValidate = (entry, val) => {
|
||||
if (val == null || timeStart == null || timeEnd == null) return true;
|
||||
if (timeStart === 0) return true;
|
||||
|
||||
let validate = { value: true, catch: '' };
|
||||
if (entry === 'timeStart' && v > timeEnd) {
|
||||
validate.catch = 'Start time later than end time';
|
||||
} else if (entry === 'timeEnd' && v < timeStart) {
|
||||
validate.catch = 'End time earlier than start time';
|
||||
let start = timeStart;
|
||||
let end = timeEnd;
|
||||
if (entry === 'timeStart') {
|
||||
start = val;
|
||||
} else if (entry === 'timeEnd') {
|
||||
end = val;
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
|
||||
if (validate.catch !== '') {
|
||||
emitWarning(`Time Input Warning: ${validate.catch}`);
|
||||
const valid = validateTimes(start, end);
|
||||
// give warning but not enforce validation
|
||||
if (!valid.value) {
|
||||
emitWarning(`Time Input Warning: ${valid.catch}`);
|
||||
}
|
||||
return validate.value;
|
||||
return valid.value;
|
||||
};
|
||||
|
||||
return (
|
||||
(delay != null) && (delay > 0) ? (
|
||||
<TimesDelayed
|
||||
handleValidate={handleValidate}
|
||||
actionHandler={props.actionHandler}
|
||||
delay={delay}
|
||||
timeStart={timeStart}
|
||||
timeEnd={timeEnd}
|
||||
duration={duration}
|
||||
/>
|
||||
) : (
|
||||
<Times
|
||||
handleValidate={handleValidate}
|
||||
actionHandler={props.actionHandler}
|
||||
timeStart={timeStart}
|
||||
timeEnd={timeEnd}
|
||||
duration={duration}
|
||||
/>
|
||||
)
|
||||
)
|
||||
return delay != null && delay > 0 ? (
|
||||
<TimesDelayed
|
||||
handleValidate={handleValidate}
|
||||
actionHandler={props.actionHandler}
|
||||
delay={delay}
|
||||
timeStart={timeStart}
|
||||
timeEnd={timeEnd}
|
||||
duration={duration}
|
||||
previousEnd={previousEnd}
|
||||
/>
|
||||
) : (
|
||||
<Times
|
||||
handleValidate={handleValidate}
|
||||
actionHandler={props.actionHandler}
|
||||
timeStart={timeStart}
|
||||
timeEnd={timeEnd}
|
||||
duration={duration}
|
||||
previousEnd={previousEnd}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
EventTimesVertical.propTypes = {
|
||||
delay: PropTypes.number.isRequired,
|
||||
timeStart: PropTypes.number.isRequired,
|
||||
timeEnd: PropTypes.number.isRequired,
|
||||
duration: PropTypes.number.isRequired,
|
||||
previousEnd: PropTypes.number.isRequired,
|
||||
};
|
||||
@@ -16,6 +16,8 @@ export default function NavLogo(props) {
|
||||
|
||||
// Handle keyboard shortcuts
|
||||
const handleKeyPress = useCallback((e) => {
|
||||
// handle held key
|
||||
if (e.repeat) return;
|
||||
// Space bar
|
||||
if (e.keyCode === 32) {
|
||||
setShowNav((s) => !s);
|
||||
@@ -54,44 +56,51 @@ export default function NavLogo(props) {
|
||||
className={showNav ? style.nav : style.navHidden}
|
||||
>
|
||||
<Link
|
||||
to='/presenter'
|
||||
to='/timer'
|
||||
className={style.navItem}
|
||||
tabIndex={1}
|
||||
>
|
||||
Presenter
|
||||
Timer
|
||||
</Link>
|
||||
<Link
|
||||
to='/minimal'
|
||||
className={style.navItem}
|
||||
tabIndex={2}
|
||||
>
|
||||
Minimal Timer
|
||||
</Link>
|
||||
<Link
|
||||
to='/sm'
|
||||
className={style.navItem}
|
||||
tabIndex={2}
|
||||
tabIndex={3}
|
||||
>
|
||||
Backstage
|
||||
</Link>
|
||||
<Link
|
||||
to='/public'
|
||||
className={style.navItem}
|
||||
tabIndex={3}
|
||||
tabIndex={4}
|
||||
>
|
||||
Public
|
||||
</Link>
|
||||
<Link
|
||||
to='/lower'
|
||||
className={style.navItem}
|
||||
tabIndex={4}
|
||||
tabIndex={5}
|
||||
>
|
||||
Lower Thirds
|
||||
</Link>
|
||||
<Link
|
||||
to='/pip'
|
||||
className={style.navItem}
|
||||
tabIndex={4}
|
||||
tabIndex={6}
|
||||
>
|
||||
PIP
|
||||
</Link>
|
||||
<Link
|
||||
to='/studio'
|
||||
className={style.navItem}
|
||||
tabIndex={5}
|
||||
tabIndex={7}
|
||||
>
|
||||
Studio Clock
|
||||
</Link>
|
||||
|
||||
@@ -1,10 +0,0 @@
|
||||
import styles from './SmallTimer.module.css';
|
||||
|
||||
export default function SmallTimer({ label, time }) {
|
||||
return (
|
||||
<div className={styles.SmallTimer}>
|
||||
<div className={styles.label}>{label}</div>
|
||||
<div className={styles.timer}>{time}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,23 +0,0 @@
|
||||
.smallTimer {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.label,
|
||||
.timer {
|
||||
font-size: 1vw;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.label {
|
||||
font-size: 1.5vw;
|
||||
color: #888;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.timer {
|
||||
font-family: 'Open Sans', sans-serif;
|
||||
|
||||
font-size: 4vw;
|
||||
letter-spacing: 0.125em;
|
||||
}
|
||||
@@ -1,22 +0,0 @@
|
||||
import { FormErrorMessage } from '@chakra-ui/form-control';
|
||||
import { FormLabel } from '@chakra-ui/form-control';
|
||||
import { FormControl } from '@chakra-ui/form-control';
|
||||
import { Input } from '@chakra-ui/input';
|
||||
import { Field } from 'formik';
|
||||
|
||||
export default function ChakraInput(props) {
|
||||
const { label, name, ...rest } = props;
|
||||
return (
|
||||
<Field name={name}>
|
||||
{({ field, form }) => {
|
||||
return (
|
||||
<FormControl isInvalid={form.errors[name] && form.touched[name]}>
|
||||
<FormLabel htmlFor={name}>{label}</FormLabel>
|
||||
<Input id={name} {...rest} {...field} />
|
||||
<FormErrorMessage>{form.errors[name]}</FormErrorMessage>
|
||||
</FormControl>
|
||||
);
|
||||
}}
|
||||
</Field>
|
||||
);
|
||||
}
|
||||
@@ -1,15 +1,13 @@
|
||||
import { Editable, EditableInput, EditablePreview } from '@chakra-ui/editable';
|
||||
import { useContext, useEffect, useState } from 'react';
|
||||
import {
|
||||
isTimeString,
|
||||
timeStringToMillis,
|
||||
} from '../utils/dateConfig';
|
||||
import { forgivingStringToMillis } from '../utils/dateConfig';
|
||||
import { stringFromMillis } from 'ontime-utils/time';
|
||||
import style from './EditableTimer.module.css';
|
||||
import { LoggingContext } from '../../app/context/LoggingContext';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
export default function EditableTimer(props) {
|
||||
const { name, actionHandler, time, delay, validate } = props;
|
||||
const { name, actionHandler, time, delay, validate, previousEnd } = props;
|
||||
const { emitError } = useContext(LoggingContext);
|
||||
const [value, setValue] = useState('');
|
||||
|
||||
@@ -33,13 +31,26 @@ export default function EditableTimer(props) {
|
||||
// Check if there is anything there
|
||||
if (value === '') return false;
|
||||
|
||||
// check if its valid time string
|
||||
if (!isTimeString(value)) return false;
|
||||
let newValMillis;
|
||||
|
||||
// convert entered value to milliseconds
|
||||
const newValMillis = timeStringToMillis(value);
|
||||
// check for known aliases
|
||||
if (value === 'p' || value === 'prev' || value === 'previous') {
|
||||
// string to pass should be the time of the end before
|
||||
if (previousEnd != null) {
|
||||
newValMillis = previousEnd;
|
||||
} else {
|
||||
newValMillis = 0;
|
||||
}
|
||||
} else if (value.startsWith('+')) {
|
||||
// string to pass should add to the end before
|
||||
const val = value.substring(1);
|
||||
newValMillis = previousEnd + forgivingStringToMillis(val);
|
||||
} else {
|
||||
// convert entered value to milliseconds
|
||||
newValMillis = forgivingStringToMillis(value);
|
||||
}
|
||||
|
||||
// Time now and time submitedVal
|
||||
// Time now and time submittedVal
|
||||
const originalMillis = time + delay;
|
||||
|
||||
// check if time is different from before
|
||||
@@ -67,3 +78,12 @@ export default function EditableTimer(props) {
|
||||
</Editable>
|
||||
);
|
||||
}
|
||||
|
||||
EditableTimer.propTypes = {
|
||||
name: PropTypes.string.isRequired,
|
||||
actionHandler: PropTypes.func.isRequired,
|
||||
time: PropTypes.number.isRequired,
|
||||
delay: PropTypes.number.isRequired,
|
||||
validate: PropTypes.func.isRequired,
|
||||
previousEnd: PropTypes.number.isRequired,
|
||||
};
|
||||
|
||||
@@ -1,9 +1,12 @@
|
||||
import {
|
||||
formatDisplay,
|
||||
isTimeString,
|
||||
millisToMinutes,
|
||||
millisToSeconds,
|
||||
forgivingStringToMillis,
|
||||
timeStringToMillis,
|
||||
} from '../dateConfig';
|
||||
import { stringFromMillis } from 'ontime-utils/time';
|
||||
|
||||
describe('test string from formatDisplay function', () => {
|
||||
it('test with null values', () => {
|
||||
@@ -47,6 +50,13 @@ describe('test string from formatDisplay function', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('test formatDisplay handles partial secs', () => {
|
||||
it('test with 1795829', () => {
|
||||
const t = { val: 1795829, result: '00:29:55' };
|
||||
expect(stringFromMillis(t.val)).toBe(t.result);
|
||||
});
|
||||
});
|
||||
|
||||
describe('test string from formatDisplay function with hidezero', () => {
|
||||
it('test with null values', () => {
|
||||
const t = { val: null, result: '00:00' };
|
||||
@@ -92,22 +102,22 @@ describe('test string from formatDisplay function with hidezero', () => {
|
||||
describe('test millisToSeconds function', () => {
|
||||
it('test with null values', () => {
|
||||
const t = { val: null, result: 0 };
|
||||
expect(millisToSeconds(t.val, false)).toBe(t.result);
|
||||
expect(millisToSeconds(t.val)).toBe(t.result);
|
||||
});
|
||||
|
||||
it('test with valid millis', () => {
|
||||
const t = { val: 3600000, result: 3600 };
|
||||
expect(millisToSeconds(t.val, false)).toBe(t.result);
|
||||
expect(millisToSeconds(t.val)).toBe(t.result);
|
||||
});
|
||||
|
||||
it('test with negative millis', () => {
|
||||
const t = { val: -3600000, result: -3600 };
|
||||
expect(millisToSeconds(t.val, false)).toBe(t.result);
|
||||
expect(millisToSeconds(t.val)).toBe(t.result);
|
||||
});
|
||||
|
||||
it('test with 0', () => {
|
||||
const t = { val: 0, result: 0 };
|
||||
expect(millisToSeconds(t.val, false)).toBe(t.result);
|
||||
expect(millisToSeconds(t.val)).toBe(t.result);
|
||||
});
|
||||
|
||||
it('test with -0', () => {
|
||||
@@ -244,3 +254,59 @@ describe('test timeStringToMillis function', () => {
|
||||
expect(timeStringToMillis(t.val)).toBe(t.result);
|
||||
});
|
||||
});
|
||||
|
||||
describe('test isTimeString() function', () => {
|
||||
test('it validates time strings', () => {
|
||||
const ts = ['2', '2:10', '2:10:22'];
|
||||
for (const s of ts) {
|
||||
expect(isTimeString(s)).toBe(true);
|
||||
}
|
||||
});
|
||||
|
||||
test('it fails overloaded times', () => {
|
||||
const ts = ['70', '89:10', '26:10:22'];
|
||||
for (const s of ts) {
|
||||
expect(isTimeString(s)).toBe(false);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe('test isTimeString() function handle different separators', () => {
|
||||
const ts = ['2:10', '2,10', '2.10'];
|
||||
for (const s of ts) {
|
||||
test(`it handles ${s}`, () => {
|
||||
expect(isTimeString(s)).toBe(true);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
describe('test timeHelper() function handles separators', () => {
|
||||
const ts = ['1:2:3:10', '2,10', '2.10'];
|
||||
for (const s of ts) {
|
||||
test(`it handles ${s}`, () => {
|
||||
expect(typeof forgivingStringToMillis(s)).toBe('number');
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
describe('test timeHelper() parses strings correctly', () => {
|
||||
const ts = [
|
||||
{ value: '', expect: 0 },
|
||||
{ value: '0', expect: 0 },
|
||||
{ value: '-0', expect: 0 },
|
||||
{ value: '1', expect: 60 * 1000 },
|
||||
{ value: '-1', expect: 60 * 1000 },
|
||||
{ value: '1.2', expect: 60 * 1000 + 2 * 1000 },
|
||||
{ value: '1.70', expect: 60 * 1000 + 70 * 1000 },
|
||||
{ value: '1.1.1', expect: 60 * 60 * 1000 + 60 * 1000 + 1000 },
|
||||
{ value: '12.1.1', expect: 12 * 60 * 60 * 1000 + 60 * 1000 + 1000 },
|
||||
{ value: '12.55.1', expect: 12 * 60 * 60 * 1000 + 55 * 60 * 1000 + 1000 },
|
||||
{ value: '12.55.40', expect: 12 * 60 * 60 * 1000 + 55 * 60 * 1000 + 40 * 1000 },
|
||||
];
|
||||
|
||||
for (const s of ts) {
|
||||
test(`it handles ${s.value}`, () => {
|
||||
expect(forgivingStringToMillis(s.value)).toBe(s.expect);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
@@ -5,7 +5,6 @@ const mts = 1000; // millis to seconds
|
||||
const mtm = 1000 * 60; // millis to minutes
|
||||
const mth = 1000 * 60 * 60; // millis to hours
|
||||
|
||||
|
||||
/**
|
||||
* another go at simpler string formatting (counters)
|
||||
* @description Converts seconds to string representing time
|
||||
@@ -13,8 +12,7 @@ const mth = 1000 * 60 * 60; // millis to hours
|
||||
* @param {boolean} [hideZero] - whether to show hours in case its 00
|
||||
* @returns {string} String representing absolute time 00:12:02
|
||||
*/
|
||||
|
||||
export function formatDisplay(seconds, hideZero=false) {
|
||||
export function formatDisplay(seconds, hideZero = false) {
|
||||
// add an extra 0 if necessary
|
||||
const format = (val) => `0${Math.floor(val)}`.slice(-2);
|
||||
|
||||
@@ -31,8 +29,6 @@ export function formatDisplay(seconds, hideZero=false) {
|
||||
* @param {number} millis - time in seconds
|
||||
* @returns {number} Amount in seconds
|
||||
*/
|
||||
|
||||
// millis to seconds
|
||||
export const millisToSeconds = (millis) => {
|
||||
return millis < 0 ? Math.ceil(millis / mts) : Math.floor(millis / mts);
|
||||
};
|
||||
@@ -42,8 +38,6 @@ export const millisToSeconds = (millis) => {
|
||||
* @param {number} millis - time in seconds
|
||||
* @returns {number} Amount in seconds
|
||||
*/
|
||||
|
||||
// millis to minutes
|
||||
export const millisToMinutes = (millis) => {
|
||||
return millis < 0 ? Math.ceil(millis / mtm) : Math.floor(millis / mtm);
|
||||
};
|
||||
@@ -53,15 +47,12 @@ export const millisToMinutes = (millis) => {
|
||||
* @param {string} string - time string "23:00:12"
|
||||
* @returns {number} Amount in milliseconds
|
||||
*/
|
||||
|
||||
// timeStringToMillis
|
||||
export const timeStringToMillis = (string) => {
|
||||
if (typeof string !== 'string') return 0;
|
||||
const time = string.split(':');
|
||||
if (time.length === 1) return Math.abs(time[0] * mts);
|
||||
if (time.length === 2) return Math.abs(time[0]) * mtm + time[1] * mts;
|
||||
if (time.length === 3)
|
||||
return Math.abs(time[0]) * mth + time[1] * mtm + time[2] * mts;
|
||||
if (time.length === 3) return Math.abs(time[0]) * mth + time[1] * mtm + time[2] * mts;
|
||||
else return 0;
|
||||
};
|
||||
|
||||
@@ -70,8 +61,6 @@ export const timeStringToMillis = (string) => {
|
||||
* @param {string} string - time string "23:00:12"
|
||||
* @returns {boolean} string represents time
|
||||
*/
|
||||
|
||||
// isTimeString
|
||||
export const isTimeString = (string) => {
|
||||
// ^ # Start of string
|
||||
// (?: # Try to match...
|
||||
@@ -83,6 +72,42 @@ export const isTimeString = (string) => {
|
||||
// ([0-5]?\d) # SS (required)
|
||||
// $ # End of string
|
||||
|
||||
const regex = /^(?:(?:([01]?\d|2[0-3]):)?([0-5]?\d):)?([0-5]?\d)$/;
|
||||
const regex = /^(?:(?:([01]?\d|2[0-3])[:,.])?([0-5]?\d)[:,.])?([0-5]?\d)$/;
|
||||
return regex.test(string);
|
||||
};
|
||||
|
||||
const parse = (valueAsString) => {
|
||||
const parsed = parseInt(valueAsString, 10);
|
||||
if (isNaN(parsed)) {
|
||||
return 0;
|
||||
}
|
||||
return Math.abs(parsed);
|
||||
};
|
||||
|
||||
/**
|
||||
* @description Parses a time string to millis
|
||||
* @param string - time string
|
||||
* @returns {number} - time string in millis
|
||||
*/
|
||||
export const forgivingStringToMillis = (string) => {
|
||||
let millis = 0;
|
||||
|
||||
// split string at known separators : , .
|
||||
const separatorRegex = /[\s,:.]+/;
|
||||
const [first, second, third] = string.split(separatorRegex);
|
||||
|
||||
if (first != null && second != null && third != null) {
|
||||
// if string has three sections, treat as [hours] [minutes] [seconds]
|
||||
millis = parse(first) * mth;
|
||||
millis += parse(second) * mtm;
|
||||
millis += parse(third) * mts;
|
||||
} else if (third == null) {
|
||||
// if string has two sections, treat as [minutes] [seconds]
|
||||
millis = parse(first) * mtm;
|
||||
millis += parse(second) * mts;
|
||||
} else if (second == null) {
|
||||
// if string has one section, treat as [minutes]
|
||||
millis = parse(first) * mtm;
|
||||
}
|
||||
return millis;
|
||||
};
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import {Editable, EditableInput, EditablePreview} from '@chakra-ui/editable';
|
||||
import {Switch} from "@chakra-ui/react";
|
||||
import {useEffect, useState} from 'react';
|
||||
import {useSocket} from 'app/context/socketContext';
|
||||
import { Editable, EditableInput, EditablePreview } from '@chakra-ui/editable';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useSocket } from 'app/context/socketContext';
|
||||
import VisibleIconBtn from 'common/components/buttons/VisibleIconBtn';
|
||||
import OnAirIconBtn from '../../common/components/buttons/OnAirIconBtn';
|
||||
import style from './MessageControl.module.scss';
|
||||
|
||||
const inputProps = {
|
||||
@@ -10,7 +10,7 @@ const inputProps = {
|
||||
};
|
||||
|
||||
const InputRow = (props) => {
|
||||
const {label, placeholder, text, visible} = props;
|
||||
const { label, placeholder, text, visible } = props;
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -23,8 +23,8 @@ const InputRow = (props) => {
|
||||
className={style.inline}
|
||||
color={text === '' ? '#666' : 'inherit'}
|
||||
>
|
||||
<EditablePreview className={style.padleft}/>
|
||||
<EditableInput className={style.padleft}/>
|
||||
<EditablePreview className={style.padleft} />
|
||||
<EditableInput className={style.padleft} />
|
||||
</Editable>
|
||||
<VisibleIconBtn
|
||||
active={visible || undefined}
|
||||
@@ -55,19 +55,19 @@ export default function MessageControl() {
|
||||
useEffect(() => {
|
||||
if (socket == null) return;
|
||||
|
||||
// Handle presenter messages
|
||||
socket.on('messages-presenter', (data) => {
|
||||
setPres({...data});
|
||||
// Handle timer messages
|
||||
socket.on('messages-timer', (data) => {
|
||||
setPres({ ...data });
|
||||
});
|
||||
|
||||
// Handle public messages
|
||||
socket.on('messages-public', (data) => {
|
||||
setPubl({...data});
|
||||
setPubl({ ...data });
|
||||
});
|
||||
|
||||
// Handle lower third messages
|
||||
socket.on('messages-lower', (data) => {
|
||||
setLower({...data});
|
||||
setLower({ ...data });
|
||||
});
|
||||
|
||||
// Handle lower third messages
|
||||
@@ -83,7 +83,7 @@ export default function MessageControl() {
|
||||
// Clear listeners
|
||||
return () => {
|
||||
socket.off('messages-public');
|
||||
socket.off('messages-presenter');
|
||||
socket.off('messages-timer');
|
||||
socket.off('messages-lower');
|
||||
socket.off('onAir');
|
||||
};
|
||||
@@ -92,10 +92,10 @@ export default function MessageControl() {
|
||||
const messageControl = async (action, payload) => {
|
||||
switch (action) {
|
||||
case 'pres-text':
|
||||
socket.emit('set-presenter-text', payload);
|
||||
socket.emit('set-timer-text', payload);
|
||||
break;
|
||||
case 'toggle-pres-visible':
|
||||
socket.emit('set-presenter-visible', !pres.visible);
|
||||
socket.emit('set-timer-visible', !pres.visible);
|
||||
break;
|
||||
case 'publ-text':
|
||||
socket.emit('set-public-text', payload);
|
||||
@@ -146,13 +146,13 @@ export default function MessageControl() {
|
||||
/>
|
||||
</div>
|
||||
<div className={style.onAirToggle}>
|
||||
<Switch
|
||||
colorScheme='green'
|
||||
<OnAirIconBtn
|
||||
className={style.btn}
|
||||
active={onAir}
|
||||
size='md'
|
||||
isChecked={onAir}
|
||||
onChange={() => messageControl('toggle-onAir')}>
|
||||
On Air?
|
||||
</Switch>
|
||||
actionHandler={() => messageControl('toggle-onAir')}
|
||||
/>
|
||||
<span className={style.onAirLabel}>On Air</span>
|
||||
<span className={style.oscLabel}>
|
||||
{`/ontime/offAir << OSC >> /ontime/onAir`}
|
||||
</span>
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
@use '../../styles/main' as *;
|
||||
@use '../../styles/mixins' as *;
|
||||
|
||||
.messageContainer,
|
||||
.onAirToggle {
|
||||
background-color: rgba(0, 0, 0, 0.05);
|
||||
border: 1px solid rgba(0, 0, 0, 0.05);
|
||||
border-radius: 4px;
|
||||
@include main-container;
|
||||
display: flex;
|
||||
gap: 0.5em;
|
||||
padding: 0.5em;
|
||||
|
||||
}
|
||||
|
||||
.messageContainer {
|
||||
@@ -17,17 +17,20 @@
|
||||
grid-template-columns: 1fr auto;
|
||||
gap: 1em;
|
||||
}
|
||||
|
||||
.label {
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
font-size: 0.9em;
|
||||
color: #ccc;
|
||||
}
|
||||
|
||||
.inline {
|
||||
border-radius: 4px;
|
||||
background-color: rgba(255, 255, 255, 0.05);
|
||||
border: 1px solid rgba(255, 255, 255, 0.05);
|
||||
}
|
||||
|
||||
.padleft {
|
||||
padding-left: 0.5em;
|
||||
}
|
||||
@@ -35,20 +38,25 @@
|
||||
|
||||
.onAirToggle {
|
||||
margin-top: 1em;
|
||||
display: flex;
|
||||
gap: 1em;
|
||||
align-items: center;
|
||||
line-height: 3em;
|
||||
display: grid;
|
||||
grid-template-areas:
|
||||
'btn label'
|
||||
'btn osc';
|
||||
grid-template-columns: 2.5em 1fr;
|
||||
grid-template-rows: 1.2em 0.8em;
|
||||
|
||||
.btn {
|
||||
grid-area: btn;
|
||||
}
|
||||
|
||||
.onAirLabel {
|
||||
grid-area: label;
|
||||
font-size: 1.2em;
|
||||
}
|
||||
|
||||
.oscLabel {
|
||||
color: #4bffabcc;
|
||||
font-size: 0.8em;
|
||||
float: right;
|
||||
padding-right: 1em;
|
||||
-webkit-user-select: text;
|
||||
user-select: text;
|
||||
@include osc-label;
|
||||
grid-area: osc;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,37 +1,37 @@
|
||||
import style from './PlaybackControl.module.scss';
|
||||
import Countdown from 'common/components/countdown/Countdown';
|
||||
import { stringFromMillis } from 'ontime-utils/time';
|
||||
import {Tooltip} from '@chakra-ui/react';
|
||||
import {Button} from '@chakra-ui/button';
|
||||
import {memo} from 'react';
|
||||
import PropTypes from "prop-types";
|
||||
import { Tooltip } from '@chakra-ui/react';
|
||||
import { Button } from '@chakra-ui/button';
|
||||
import { memo } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
const areEqual = (prevProps, nextProps) => {
|
||||
return (
|
||||
prevProps.timer.running === nextProps.timer.running
|
||||
&& prevProps.timer.expectedFinish === nextProps.timer.expectedFinish
|
||||
&& prevProps.timer.startedAt === nextProps.timer.startedAt
|
||||
&& prevProps.playback === nextProps.playback
|
||||
&& prevProps.timer.secondary === nextProps.timer.secondary
|
||||
&& prevProps.selectedId === nextProps.selectedId
|
||||
prevProps.timer.running === nextProps.timer.running &&
|
||||
prevProps.timer.isNegative === nextProps.timer.isNegative &&
|
||||
prevProps.timer.expectedFinish === nextProps.timer.expectedFinish &&
|
||||
prevProps.timer.startedAt === nextProps.timer.startedAt &&
|
||||
prevProps.playback === nextProps.playback &&
|
||||
prevProps.timer.secondary === nextProps.timer.secondary &&
|
||||
prevProps.selectedId === nextProps.selectedId
|
||||
);
|
||||
};
|
||||
|
||||
const PlaybackTimer = (props) => {
|
||||
const {timer, playback, handleIncrement, selectedId} = props;
|
||||
const { timer, playback, handleIncrement, selectedId } = props;
|
||||
const started = stringFromMillis(timer.startedAt, true);
|
||||
const finish = stringFromMillis(timer.expectedFinish, true);
|
||||
const isNegative = timer.running < 0;
|
||||
const isRolling = playback === 'roll';
|
||||
const isWaiting = timer.secondary > 0 && timer.running == null;
|
||||
const disableButtons = (selectedId == null || isRolling);
|
||||
const disableButtons = selectedId == null || isRolling;
|
||||
|
||||
const incrementProps = {
|
||||
size: 'sm',
|
||||
width: '2.9em',
|
||||
colorScheme: 'whiteAlpha',
|
||||
variant: 'outline',
|
||||
_focus: {boxShadow: 'none'},
|
||||
_focus: { boxShadow: 'none' },
|
||||
};
|
||||
|
||||
return (
|
||||
@@ -39,18 +39,18 @@ const PlaybackTimer = (props) => {
|
||||
<div className={style.timeContainer}>
|
||||
<div className={style.indicators}>
|
||||
<Tooltip label='Roll mode active'>
|
||||
<div className={isRolling ? style.indRollActive : style.indRoll}/>
|
||||
<div className={isRolling ? style.indRollActive : style.indRoll} />
|
||||
</Tooltip>
|
||||
<div
|
||||
className={isNegative ? style.indNegativeActive : style.indNegative}
|
||||
className={timer.isNegative ? style.indNegativeActive : style.indNegative}
|
||||
/>
|
||||
<div className={style.indDelay}/>
|
||||
<div className={style.indDelay} />
|
||||
</div>
|
||||
<div className={style.timer}>
|
||||
<Countdown
|
||||
time={isWaiting ? timer.secondary : timer.running}
|
||||
isNegative={timer.isNegative}
|
||||
small
|
||||
negative={isNegative}
|
||||
/>
|
||||
</div>
|
||||
{isWaiting ? (
|
||||
@@ -71,34 +71,58 @@ const PlaybackTimer = (props) => {
|
||||
</>
|
||||
)}
|
||||
<div className={style.btn}>
|
||||
<Button
|
||||
{...incrementProps}
|
||||
disabled={disableButtons}
|
||||
onClick={() => handleIncrement(-1)}
|
||||
<Tooltip
|
||||
label={'Remove 1 minute'}
|
||||
delay={500}
|
||||
shouldWrapChildren={disableButtons}
|
||||
>
|
||||
-1
|
||||
</Button>
|
||||
<Button
|
||||
{...incrementProps}
|
||||
disabled={disableButtons}
|
||||
onClick={() => handleIncrement(1)}
|
||||
<Button
|
||||
{...incrementProps}
|
||||
disabled={disableButtons}
|
||||
onClick={() => handleIncrement(-1)}
|
||||
>
|
||||
-1
|
||||
</Button>
|
||||
</Tooltip>
|
||||
<Tooltip
|
||||
label={'Add 1 minute'}
|
||||
delay={500}
|
||||
shouldWrapChildren={disableButtons}
|
||||
>
|
||||
+1
|
||||
</Button>
|
||||
<Button
|
||||
{...incrementProps}
|
||||
disabled={disableButtons}
|
||||
onClick={() => handleIncrement(-5)}
|
||||
<Button
|
||||
{...incrementProps}
|
||||
disabled={disableButtons}
|
||||
onClick={() => handleIncrement(1)}
|
||||
>
|
||||
+1
|
||||
</Button>
|
||||
</Tooltip>
|
||||
<Tooltip
|
||||
label={'Remove 5 minutes'}
|
||||
delay={500}
|
||||
shouldWrapChildren={disableButtons}
|
||||
>
|
||||
-5
|
||||
</Button>
|
||||
<Button
|
||||
{...incrementProps}
|
||||
disabled={disableButtons}
|
||||
onClick={() => handleIncrement(5)}
|
||||
<Button
|
||||
{...incrementProps}
|
||||
disabled={disableButtons}
|
||||
onClick={() => handleIncrement(-5)}
|
||||
>
|
||||
-5
|
||||
</Button>
|
||||
</Tooltip>
|
||||
<Tooltip
|
||||
label={'Add 5 minutes'}
|
||||
delay={500}
|
||||
shouldWrapChildren={disableButtons}
|
||||
>
|
||||
+5
|
||||
</Button>
|
||||
<Button
|
||||
{...incrementProps}
|
||||
disabled={disableButtons}
|
||||
onClick={() => handleIncrement(5)}
|
||||
>
|
||||
+5
|
||||
</Button>
|
||||
</Tooltip>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
|
||||
+9
-1
@@ -1,8 +1,9 @@
|
||||
import { Draggable } from 'react-beautiful-dnd';
|
||||
import { FiMoreVertical } from 'react-icons/fi';
|
||||
import DeleteIconBtn from 'common/components/buttons/DeleteIconBtn';
|
||||
import ActionButtons from './ActionButtons';
|
||||
import ActionButtons from '../list/ActionButtons';
|
||||
import style from './BlockBlock.module.css';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
export default function BlockBlock(props) {
|
||||
const { index, data, actionHandler } = props;
|
||||
@@ -27,3 +28,10 @@ export default function BlockBlock(props) {
|
||||
</Draggable>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
BlockBlock.propTypes = {
|
||||
index: PropTypes.number.isRequired,
|
||||
data: PropTypes.object.isRequired,
|
||||
actionHandler: PropTypes.func.isRequired,
|
||||
};
|
||||
+12
-14
@@ -1,11 +1,12 @@
|
||||
import { Draggable } from 'react-beautiful-dnd';
|
||||
import { FiMoreVertical } from 'react-icons/fi';
|
||||
import { millisToMinutes } from 'common/utils/dateConfig';
|
||||
import ActionButtons from './ActionButtons';
|
||||
import ActionButtons from '../list/ActionButtons';
|
||||
import DeleteIconBtn from 'common/components/buttons/DeleteIconBtn';
|
||||
import ApplyIconBtn from 'common/components/buttons/ApplyIconBtn';
|
||||
import DelayInput from 'common/input/DelayInput';
|
||||
import style from './DelayBlock.module.css';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
export default function DelayBlock(props) {
|
||||
const { eventsHandler, data, index, actionHandler } = props;
|
||||
@@ -14,25 +15,15 @@ export default function DelayBlock(props) {
|
||||
eventsHandler('applyDelay', { id: data.id, duration: data.duration });
|
||||
};
|
||||
|
||||
let delayValue =
|
||||
data.duration != null ? millisToMinutes(data.duration) : undefined;
|
||||
|
||||
let delayValue = data.duration != null ? millisToMinutes(data.duration) : undefined;
|
||||
return (
|
||||
<Draggable key={data.id} draggableId={data.id} index={index}>
|
||||
{(provided) => (
|
||||
<div
|
||||
className={style.delay}
|
||||
{...provided.draggableProps}
|
||||
ref={provided.innerRef}
|
||||
>
|
||||
<div className={style.delay} {...provided.draggableProps} ref={provided.innerRef}>
|
||||
<span className={style.drag} {...provided.dragHandleProps}>
|
||||
<FiMoreVertical />
|
||||
</span>
|
||||
<DelayInput
|
||||
className={style.input}
|
||||
value={delayValue}
|
||||
actionHandler={actionHandler}
|
||||
/>
|
||||
<DelayInput className={style.input} value={delayValue} actionHandler={actionHandler} />
|
||||
<div className={style.actionOverlay}>
|
||||
<ApplyIconBtn clickhandler={applyDelayHandler} />
|
||||
<DeleteIconBtn actionHandler={actionHandler} />
|
||||
@@ -43,3 +34,10 @@ export default function DelayBlock(props) {
|
||||
</Draggable>
|
||||
);
|
||||
}
|
||||
|
||||
DelayBlock.propTypes = {
|
||||
eventsHandler: PropTypes.func.isRequired,
|
||||
data: PropTypes.object.isRequired,
|
||||
index: PropTypes.number.isRequired,
|
||||
actionHandler: PropTypes.func.isRequired,
|
||||
};
|
||||
+54
-51
@@ -5,17 +5,17 @@ import { Draggable } from 'react-beautiful-dnd';
|
||||
import EventTimes from 'common/components/eventTimes/EventTimes';
|
||||
import EventTimesVertical from 'common/components/eventTimes/EventTimesVertical';
|
||||
import EditableText from 'common/input/EditableText';
|
||||
import ActionButtons from './ActionButtons';
|
||||
import ActionButtons from '../list/ActionButtons';
|
||||
import PublicIconBtn from 'common/components/buttons/PublicIconBtn';
|
||||
import DeleteIconBtn from 'common/components/buttons/DeleteIconBtn';
|
||||
import { millisToMinutes } from 'common/utils/dateConfig';
|
||||
import style from './EventBlock.module.css';
|
||||
import { SelectCollapse, HandleCollapse } from 'app/context/collapseAtom';
|
||||
import { HandleCollapse, SelectCollapse } from 'app/context/collapseAtom';
|
||||
import { useAtom } from 'jotai';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
const ExpandedBlock = (props) => {
|
||||
const { provided, data, eventIndex, next, delay, delayValue, actionHandler } =
|
||||
props;
|
||||
const { provided, data, eventIndex, next, delay, delayValue, previousEnd, actionHandler } = props;
|
||||
|
||||
const oscid = data.id.length > 4 ? '...' : data.id;
|
||||
|
||||
@@ -28,14 +28,12 @@ const ExpandedBlock = (props) => {
|
||||
return (
|
||||
<>
|
||||
<span className={style.drag} {...provided.dragHandleProps}>
|
||||
<FiMoreVertical />
|
||||
<FiMoreVertical />
|
||||
</span>
|
||||
|
||||
<div className={style.indicators}>
|
||||
<span className={next ? style.next : style.nextDisabled}>Next</span>
|
||||
{delayValue != null && (
|
||||
<span className={style.delayValue}>+ {delayValue}</span>
|
||||
)}
|
||||
{delayValue != null && <span className={style.delayValue}>+ {delayValue}</span>}
|
||||
</div>
|
||||
<div className={style.timeExpanded}>
|
||||
<EventTimesVertical
|
||||
@@ -44,6 +42,7 @@ const ExpandedBlock = (props) => {
|
||||
timeEnd={data.timeEnd}
|
||||
duration={duration}
|
||||
delay={delay}
|
||||
previousEnd={previousEnd}
|
||||
className={style.time}
|
||||
/>
|
||||
</div>
|
||||
@@ -53,25 +52,19 @@ const ExpandedBlock = (props) => {
|
||||
label='Title'
|
||||
defaultValue={data.title}
|
||||
placeholder='Add Title'
|
||||
submitHandler={(v) =>
|
||||
actionHandler('update', { field: 'title', value: v })
|
||||
}
|
||||
submitHandler={(v) => actionHandler('update', { field: 'title', value: v })}
|
||||
/>
|
||||
<EditableText
|
||||
label='Presenter'
|
||||
defaultValue={data.presenter}
|
||||
placeholder='Add Presenter name'
|
||||
submitHandler={(v) =>
|
||||
actionHandler('update', { field: 'presenter', value: v })
|
||||
}
|
||||
submitHandler={(v) => actionHandler('update', { field: 'presenter', value: v })}
|
||||
/>
|
||||
<EditableText
|
||||
label='Subtitle'
|
||||
defaultValue={data.subtitle}
|
||||
placeholder='Add Subtitle'
|
||||
submitHandler={(v) =>
|
||||
actionHandler('update', { field: 'subtitle', value: v })
|
||||
}
|
||||
submitHandler={(v) => actionHandler('update', { field: 'subtitle', value: v })}
|
||||
/>
|
||||
<EditableText
|
||||
label='Note'
|
||||
@@ -79,9 +72,7 @@ const ExpandedBlock = (props) => {
|
||||
placeholder='Add Note'
|
||||
style={{ color: '#d69e2e' }}
|
||||
maxchar={160}
|
||||
submitHandler={(v) =>
|
||||
actionHandler('update', { field: 'note', value: v })
|
||||
}
|
||||
submitHandler={(v) => actionHandler('update', { field: 'note', value: v })}
|
||||
/>
|
||||
<span className={style.oscLabel}>
|
||||
{`/ontime/goto ${eventIndex + 1} << OSC >> /ontime/gotoid ${oscid}`}
|
||||
@@ -89,38 +80,43 @@ const ExpandedBlock = (props) => {
|
||||
</div>
|
||||
<div className={style.actionOverlay}>
|
||||
<PublicIconBtn actionHandler={actionHandler} active={data.isPublic} />
|
||||
<ActionButtons
|
||||
showAdd
|
||||
showDelay
|
||||
showBlock
|
||||
actionHandler={actionHandler}
|
||||
/>
|
||||
<ActionButtons showAdd showDelay showBlock actionHandler={actionHandler} />
|
||||
<DeleteIconBtn actionHandler={actionHandler} />
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
ExpandedBlock.propTypes = {
|
||||
provided: PropTypes.any.isRequired,
|
||||
data: PropTypes.object.isRequired,
|
||||
eventIndex: PropTypes.number.isRequired,
|
||||
next: PropTypes.bool.isRequired,
|
||||
delay: PropTypes.number,
|
||||
delayValue: PropTypes.number,
|
||||
previousEnd: PropTypes.number.isRequired,
|
||||
actionHandler: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
const CollapsedBlock = (props) => {
|
||||
const { provided, data, next, delay, delayValue, actionHandler } = props;
|
||||
const { provided, data, next, delay, delayValue, previousEnd, actionHandler } = props;
|
||||
|
||||
return (
|
||||
<>
|
||||
<span className={style.drag} {...provided.dragHandleProps}>
|
||||
<FiMoreVertical />
|
||||
<FiMoreVertical />
|
||||
</span>
|
||||
|
||||
<div className={style.indicators}>
|
||||
<span className={next ? style.next : style.nextDisabled}>Next</span>
|
||||
{delayValue != null && (
|
||||
<span className={style.delayValue}>+ {delayValue}</span>
|
||||
)}
|
||||
{delayValue != null && <span className={style.delayValue}>+ {delayValue}</span>}
|
||||
</div>
|
||||
<EventTimes
|
||||
actionHandler={actionHandler}
|
||||
timeStart={data.timeStart}
|
||||
timeEnd={data.timeEnd}
|
||||
delay={delay}
|
||||
previousEnd={previousEnd}
|
||||
className={style.time}
|
||||
/>
|
||||
<div className={style.titleContainer}>
|
||||
@@ -128,33 +124,32 @@ const CollapsedBlock = (props) => {
|
||||
label='Title'
|
||||
defaultValue={data.title}
|
||||
placeholder='Add Title'
|
||||
submitHandler={(v) =>
|
||||
actionHandler('update', { field: 'title', value: v })
|
||||
}
|
||||
submitHandler={(v) => actionHandler('update', { field: 'title', value: v })}
|
||||
/>
|
||||
</div>
|
||||
<div className={style.actionOverlay}>
|
||||
<PublicIconBtn actionHandler={actionHandler} active={data.isPublic} />
|
||||
<ActionButtons
|
||||
showAdd
|
||||
showDelay
|
||||
showBlock
|
||||
actionHandler={actionHandler}
|
||||
/>
|
||||
<ActionButtons showAdd showDelay showBlock actionHandler={actionHandler} />
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
CollapsedBlock.propTypes = {
|
||||
provided: PropTypes.any.isRequired,
|
||||
data: PropTypes.object.isRequired,
|
||||
next: PropTypes.bool.isRequired,
|
||||
delay: PropTypes.any,
|
||||
delayValue: PropTypes.any,
|
||||
previousEnd: PropTypes.number.isRequired,
|
||||
actionHandler: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
export default function EventBlock(props) {
|
||||
const { data, selected, delay, index, eventIndex, actionHandler } = props;
|
||||
const [collapsed] = useAtom(
|
||||
useMemo(() => SelectCollapse(data.id), [data.id])
|
||||
);
|
||||
const { data, selected, delay, index, eventIndex, previousEnd, actionHandler } = props;
|
||||
const [collapsed] = useAtom(useMemo(() => SelectCollapse(data.id), [data.id]));
|
||||
const [, setCollapsed] = useAtom(HandleCollapse);
|
||||
|
||||
// TODO: should this go inside useEffect()
|
||||
// Would I then need to add this to state?
|
||||
const isSelected = selected ? style.active : '';
|
||||
const isCollapsed = collapsed ? style.collapsed : style.expanded;
|
||||
const classSelect = `${style.event} ${isCollapsed} ${isSelected}`;
|
||||
@@ -169,11 +164,7 @@ export default function EventBlock(props) {
|
||||
return (
|
||||
<Draggable key={data.id} draggableId={data.id} index={index}>
|
||||
{(provided) => (
|
||||
<div
|
||||
className={classSelect}
|
||||
{...provided.draggableProps}
|
||||
ref={provided.innerRef}
|
||||
>
|
||||
<div className={classSelect} {...provided.draggableProps} ref={provided.innerRef}>
|
||||
<Icon
|
||||
className={collapsed ? style.moreCollapsed : style.moreExpanded}
|
||||
as={FiChevronUp}
|
||||
@@ -186,6 +177,7 @@ export default function EventBlock(props) {
|
||||
next={props.next}
|
||||
delay={delay}
|
||||
delayValue={delayValue}
|
||||
previousEnd={previousEnd}
|
||||
actionHandler={actionHandler}
|
||||
/>
|
||||
) : (
|
||||
@@ -196,6 +188,7 @@ export default function EventBlock(props) {
|
||||
next={props.next}
|
||||
delay={delay}
|
||||
delayValue={delayValue}
|
||||
previousEnd={previousEnd}
|
||||
actionHandler={actionHandler}
|
||||
/>
|
||||
)}
|
||||
@@ -204,3 +197,13 @@ export default function EventBlock(props) {
|
||||
</Draggable>
|
||||
);
|
||||
}
|
||||
|
||||
EventBlock.propTypes = {
|
||||
data: PropTypes.object.isRequired,
|
||||
selected: PropTypes.bool.isRequired,
|
||||
delay: PropTypes.number,
|
||||
index: PropTypes.number.isRequired,
|
||||
eventIndex: PropTypes.number.isRequired,
|
||||
previousEnd: PropTypes.number.isRequired,
|
||||
actionHandler: PropTypes.func.isRequired,
|
||||
};
|
||||
@@ -1,6 +1,7 @@
|
||||
import { Menu, MenuButton, MenuItem, MenuList } from '@chakra-ui/menu';
|
||||
import { IconButton } from '@chakra-ui/button';
|
||||
import { FiPlus, FiMinusCircle, FiClock } from 'react-icons/fi';
|
||||
import { FiClock, FiMinusCircle, FiPlus } from 'react-icons/fi';
|
||||
import { Tooltip } from '@chakra-ui/tooltip';
|
||||
|
||||
export default function ActionButtons(props) {
|
||||
const { showAdd, showDelay, showBlock, actionHandler } = props;
|
||||
@@ -12,16 +13,18 @@ export default function ActionButtons(props) {
|
||||
|
||||
return (
|
||||
<Menu isLazy lazyBehavior='unmount'>
|
||||
<MenuButton
|
||||
as={IconButton}
|
||||
aria-label='Options'
|
||||
size='xs'
|
||||
icon={<FiPlus />}
|
||||
_expanded={{ bg: 'orange.300', color: 'white' }}
|
||||
_focus={{ boxShadow: 'none' }}
|
||||
backgroundColor={'orange.200'}
|
||||
color={'orange.500'}
|
||||
/>
|
||||
<Tooltip label='Add ...' delay={500}>
|
||||
<MenuButton
|
||||
as={IconButton}
|
||||
aria-label='Options'
|
||||
size='xs'
|
||||
icon={<FiPlus />}
|
||||
_expanded={{ bg: 'orange.300', color: 'white' }}
|
||||
_focus={{ boxShadow: 'none' }}
|
||||
backgroundColor={'orange.200'}
|
||||
color={'orange.500'}
|
||||
/>
|
||||
</Tooltip>
|
||||
<MenuList style={menuStyle}>
|
||||
<MenuItem
|
||||
icon={<FiPlus />}
|
||||
|
||||
@@ -20,8 +20,10 @@ export default function EventList(props) {
|
||||
// Handle keyboard shortcuts
|
||||
const handleKeyPress = useCallback(
|
||||
(e) => {
|
||||
// handle held key
|
||||
if (e.repeat) return;
|
||||
// Check if the alt key is pressed
|
||||
if (e.altKey) {
|
||||
if (e.altKey && (!e.ctrlKey || !e.shiftKey)) {
|
||||
// Arrow down
|
||||
if (e.keyCode === 40) {
|
||||
if (cursor == null) setCursor(0);
|
||||
@@ -145,6 +147,8 @@ export default function EventList(props) {
|
||||
|
||||
let cumulativeDelay = 0;
|
||||
let eventIndex = -1;
|
||||
let previousEnd = 0;
|
||||
let thisEnd = 0;
|
||||
|
||||
return (
|
||||
<div className={style.eventContainer}>
|
||||
@@ -167,6 +171,8 @@ export default function EventList(props) {
|
||||
cumulativeDelay = 0;
|
||||
} else if (e.type === 'event') {
|
||||
eventIndex++;
|
||||
previousEnd = thisEnd;
|
||||
thisEnd = e.timeEnd;
|
||||
}
|
||||
|
||||
return (
|
||||
@@ -184,6 +190,7 @@ export default function EventList(props) {
|
||||
next={nextId === e.id}
|
||||
eventsHandler={eventsHandler}
|
||||
delay={cumulativeDelay}
|
||||
previousEnd={previousEnd}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import DelayBlock from './DelayBlock';
|
||||
import BlockBlock from './BlockBlock';
|
||||
import EventBlock from './EventBlock';
|
||||
import DelayBlock from '../DelayBlock/DelayBlock';
|
||||
import BlockBlock from '../BlockBlock/BlockBlock';
|
||||
import EventBlock from '../EventBlock/EventBlock';
|
||||
import { memo, useContext } from 'react';
|
||||
import { LoggingContext } from '../../../app/context/LoggingContext';
|
||||
|
||||
@@ -10,7 +10,8 @@ const areEqual = (prevProps, nextProps) => {
|
||||
prevProps.selected === nextProps.selected &&
|
||||
prevProps.next === nextProps.next &&
|
||||
prevProps.index === nextProps.index &&
|
||||
prevProps.delay === nextProps.delay
|
||||
prevProps.delay === nextProps.delay &&
|
||||
prevProps.previousEnd === nextProps.previousEnd
|
||||
);
|
||||
};
|
||||
|
||||
@@ -24,6 +25,7 @@ const EventListItem = (props) => {
|
||||
next,
|
||||
eventsHandler,
|
||||
delay,
|
||||
previousEnd,
|
||||
...rest
|
||||
} = props;
|
||||
const { emitError } = useContext(LoggingContext);
|
||||
@@ -79,6 +81,7 @@ const EventListItem = (props) => {
|
||||
next={next}
|
||||
actionHandler={actionHandler}
|
||||
delay={delay}
|
||||
previousEnd={previousEnd}
|
||||
/>
|
||||
);
|
||||
case 'block':
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import { Menu, MenuButton, MenuItem, MenuList } from '@chakra-ui/menu';
|
||||
import { IconButton } from '@chakra-ui/button';
|
||||
import { FiTrash2, FiPlus, FiClock, FiMinusCircle } from 'react-icons/fi';
|
||||
import { FiClock, FiMinusCircle, FiPlus, FiTrash2 } from 'react-icons/fi';
|
||||
import { Divider } from '@chakra-ui/layout';
|
||||
import { Tooltip } from '@chakra-ui/tooltip';
|
||||
|
||||
export default function MenuActionButtons(props) {
|
||||
const { actionHandler } = props;
|
||||
@@ -12,16 +13,18 @@ export default function MenuActionButtons(props) {
|
||||
|
||||
return (
|
||||
<Menu isLazy lazyBehavior='unmount'>
|
||||
<MenuButton
|
||||
as={IconButton}
|
||||
aria-label='Create Menu'
|
||||
size={props.size || 'xs'}
|
||||
icon={<FiPlus />}
|
||||
_expanded={{ bg: 'orange.300', color: 'white' }}
|
||||
_focus={{ boxShadow: 'none' }}
|
||||
backgroundColor={'orange.200'}
|
||||
color={'orange.500'}
|
||||
/>
|
||||
<Tooltip label='Add / Delete ...'>
|
||||
<MenuButton
|
||||
as={IconButton}
|
||||
aria-label='Create Menu'
|
||||
size={props.size || 'xs'}
|
||||
icon={<FiPlus />}
|
||||
_expanded={{ bg: 'orange.300', color: 'white' }}
|
||||
_focus={{ boxShadow: 'none' }}
|
||||
backgroundColor={'orange.200'}
|
||||
color={'orange.500'}
|
||||
/>
|
||||
</Tooltip>
|
||||
<MenuList style={menuStyle}>
|
||||
<MenuItem icon={<FiPlus />} onClick={() => actionHandler('event')}>
|
||||
Add Event first
|
||||
|
||||
@@ -40,6 +40,7 @@ export default function MenuBar(props) {
|
||||
|
||||
const handleUpload = (event) => {
|
||||
const fileUploaded = event.target.files[0];
|
||||
console.log('1', fileUploaded)
|
||||
if (fileUploaded == null) return;
|
||||
|
||||
// Limit file size to 1MB
|
||||
@@ -48,17 +49,18 @@ export default function MenuBar(props) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Check file extension
|
||||
if (! fileUploaded.name.endsWith('.xlsx')
|
||||
|| !fileUploaded.name.endsWith('.json')) {
|
||||
emitError('Error: File type unknown')
|
||||
return;
|
||||
}
|
||||
console.log('2', ! fileUploaded.name.endsWith('.xlsx')
|
||||
|| !fileUploaded.name.endsWith('.json'))
|
||||
|
||||
try {
|
||||
uploaddb.mutate(fileUploaded);
|
||||
} catch (error) {
|
||||
emitError(`Failed uploading file: ${error}`)
|
||||
// Check file extension
|
||||
if (fileUploaded.name.endsWith('.xlsx') || fileUploaded.name.endsWith('.json')) {
|
||||
try {
|
||||
uploaddb.mutate(fileUploaded);
|
||||
} catch (error) {
|
||||
emitError(`Failed uploading file: ${error}`)
|
||||
}
|
||||
} else {
|
||||
emitError('Error: File type unknown')
|
||||
}
|
||||
|
||||
// reset input value
|
||||
|
||||
@@ -5,7 +5,7 @@ import { FiDownload } from 'react-icons/fi';
|
||||
export default function DownloadIconBtn(props) {
|
||||
const { clickhandler, ...rest } = props;
|
||||
return (
|
||||
<Tooltip label='Download File'>
|
||||
<Tooltip label='Export event list'>
|
||||
<IconButton
|
||||
size={props.size || 'xs'}
|
||||
icon={<FiDownload />}
|
||||
|
||||
@@ -1,19 +0,0 @@
|
||||
import { IconButton } from '@chakra-ui/button';
|
||||
import { Tooltip } from '@chakra-ui/tooltip';
|
||||
import { FiHome } from 'react-icons/fi';
|
||||
|
||||
export default function InfoIconBtn(props) {
|
||||
const { clickhandler, ...rest } = props;
|
||||
return (
|
||||
<Tooltip label='Event Main'>
|
||||
<IconButton
|
||||
size={props.size || 'xs'}
|
||||
icon={<FiHome />}
|
||||
colorScheme='white'
|
||||
onClick={clickhandler}
|
||||
_focus={{ boxShadow: 'none' }}
|
||||
{...rest}
|
||||
/>
|
||||
</Tooltip>
|
||||
);
|
||||
}
|
||||
@@ -5,7 +5,7 @@ import { FiUpload } from 'react-icons/fi';
|
||||
export default function UploadIconBtn(props) {
|
||||
const { clickhandler, ...rest } = props;
|
||||
return (
|
||||
<Tooltip label='Upload File'>
|
||||
<Tooltip label='Import event list'>
|
||||
<IconButton
|
||||
size={props.size || 'xs'}
|
||||
icon={<FiUpload />}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Button, IconButton } from '@chakra-ui/button';
|
||||
import { FiInfo, FiMinus, FiSun } from 'react-icons/fi';
|
||||
import { IoInformationCircleOutline, IoRemove, IoSunny } from 'react-icons/io5';
|
||||
import { ModalBody } from '@chakra-ui/modal';
|
||||
import { Input } from '@chakra-ui/react';
|
||||
import { getAliases, postAliases } from '../../app/api/ontimeApi';
|
||||
@@ -176,7 +176,7 @@ export default function AliasesModal() {
|
||||
<div className={style.hSeparator}>Custom Aliases</div>
|
||||
<div className={style.blockNotes}>
|
||||
<span className={style.inlineFlex}>
|
||||
<FiInfo color='#2b6cb0' fontSize={'2em'} />
|
||||
<IoInformationCircleOutline color='#2b6cb0' fontSize={'2em'} />
|
||||
URL aliases are useful in two main scenarios
|
||||
</span>
|
||||
<span className={style.labelNote}>Complicated URLs</span>
|
||||
@@ -267,7 +267,7 @@ export default function AliasesModal() {
|
||||
<Tooltip label='Enable alias' openDelay={500}>
|
||||
<IconButton
|
||||
size='xs'
|
||||
icon={<FiSun />}
|
||||
icon={<IoSunny />}
|
||||
colorScheme='blue'
|
||||
variant={alias.enabled ? null : 'outline'}
|
||||
onClick={() => setEnabled(alias.id, !alias.enabled)}
|
||||
@@ -276,7 +276,7 @@ export default function AliasesModal() {
|
||||
<Tooltip label='Delete alias' openDelay={500}>
|
||||
<IconButton
|
||||
size='xs'
|
||||
icon={<FiMinus />}
|
||||
icon={<IoRemove />}
|
||||
colorScheme='red'
|
||||
onClick={() => deleteAlias(alias.id)}
|
||||
/>
|
||||
|
||||
@@ -99,7 +99,7 @@ export default function AppSettingsModal() {
|
||||
<p className={style.notes}>
|
||||
Options related to the application
|
||||
<br />
|
||||
🔥 Changes take effect after app restart 🔥
|
||||
🔥 Changes take effect on save 🔥
|
||||
</p>
|
||||
<form onSubmit={submitHandler}>
|
||||
<div className={style.modalFields}>
|
||||
|
||||
@@ -39,6 +39,7 @@ export default function SettingsModal() {
|
||||
setSubmitting(true);
|
||||
|
||||
await postEvent(formData);
|
||||
await refetch();
|
||||
|
||||
setChanged(false);
|
||||
setSubmitting(false);
|
||||
|
||||
@@ -8,7 +8,70 @@ import style from './Modals.module.scss';
|
||||
import { LoggingContext } from '../../app/context/LoggingContext';
|
||||
import SubmitContainer from './SubmitContainer';
|
||||
import { inputProps, portInputProps } from './modalHelper';
|
||||
import { IoInformationCircleOutline } from 'react-icons/io5';
|
||||
import EnableBtn from '../../common/components/buttons/EnableBtn';
|
||||
|
||||
// currently defined endpoints
|
||||
// temporary
|
||||
const oscCycleEndpoints = [
|
||||
{
|
||||
title: 'On Event Start',
|
||||
message: '/ontime/eventNumber',
|
||||
value: '8 | int',
|
||||
},
|
||||
{
|
||||
title: 'On Update',
|
||||
message: '/ontime/time',
|
||||
value: '10:12:12 | string',
|
||||
},
|
||||
{
|
||||
title: 'On Update',
|
||||
message: '/ontime/overtime',
|
||||
value: '0-1 | int',
|
||||
},
|
||||
{
|
||||
title: 'On Update',
|
||||
message: '/ontime/title',
|
||||
value: 'Title of running event | string',
|
||||
},
|
||||
{
|
||||
title: 'On Finish',
|
||||
message: '/ontime/finished',
|
||||
value: '-',
|
||||
},
|
||||
];
|
||||
const oscTriggerEndpoints = [
|
||||
{
|
||||
title: 'On Start',
|
||||
message: '/ontime/play',
|
||||
value: '-',
|
||||
},
|
||||
{
|
||||
title: 'On Pause',
|
||||
message: '/ontime/pause',
|
||||
value: '-',
|
||||
},
|
||||
{
|
||||
title: 'On Previous',
|
||||
message: '/ontime/prev',
|
||||
value: '-',
|
||||
},
|
||||
{
|
||||
title: 'On Next',
|
||||
message: '/ontime/next',
|
||||
value: '-',
|
||||
},
|
||||
{
|
||||
title: 'On Reload',
|
||||
message: '/ontime/reload',
|
||||
value: '-',
|
||||
},
|
||||
{
|
||||
title: 'On Stop',
|
||||
message: '/ontime/stop',
|
||||
value: '-',
|
||||
},
|
||||
];
|
||||
|
||||
export default function OscSettingsModal() {
|
||||
const { data, status, refetch } = useFetch(OSC_SETTINGS, getOSC);
|
||||
@@ -57,6 +120,7 @@ export default function OscSettingsModal() {
|
||||
} else {
|
||||
// Post here
|
||||
await postOSC(formData);
|
||||
await refetch();
|
||||
setChanged(false);
|
||||
}
|
||||
setSubmitting(false);
|
||||
@@ -91,25 +155,44 @@ export default function OscSettingsModal() {
|
||||
</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 className={style.hSeparator}>
|
||||
OSC Input (Control ontime over OSC)
|
||||
</div>
|
||||
<div className={style.modalInline}>
|
||||
<FormControl id='oscInEnabled'>
|
||||
<FormLabel htmlFor='oscInEnabled'>
|
||||
OSC Enable
|
||||
<span className={style.labelNote}>
|
||||
<br />
|
||||
Enable / Disable control
|
||||
</span>
|
||||
</FormLabel>
|
||||
<EnableBtn
|
||||
active={formData.enabled}
|
||||
text={formData.enabled ? 'OSC IN Enabled' : 'OSC IN Disabled'}
|
||||
actionHandler={() => handleChange('enabled', !formData.enabled)}
|
||||
onClick={() => console.log('yay')}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormControl id='portIn'>
|
||||
<FormLabel htmlFor='portIn'>
|
||||
OSC In Port
|
||||
<span className={style.labelNote}>
|
||||
<br />
|
||||
Port - 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' }}
|
||||
/>
|
||||
</FormControl>
|
||||
</div>
|
||||
<div className={style.hSeparator}>OSC Output (feedback)</div>
|
||||
<div className={style.modalInline}>
|
||||
@@ -155,6 +238,48 @@ export default function OscSettingsModal() {
|
||||
/>
|
||||
</FormControl>
|
||||
</div>
|
||||
<div className={style.blockNotes}>
|
||||
<span className={style.inlineFlex}>
|
||||
<IoInformationCircleOutline color='#2b6cb0' fontSize={'2em'} />
|
||||
OSC Feedback messages
|
||||
</span>
|
||||
<span>
|
||||
In future OSC feedback will be user defined. <br />
|
||||
For now this is the list of OSC messages sent from ontime
|
||||
</span>
|
||||
<table>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td className={style.labelNote} style={{ width: '30%' }}>
|
||||
Cycle
|
||||
</td>
|
||||
<td className={style.labelNote}>Message</td>
|
||||
<td className={style.labelNote}>Value (example | type)</td>
|
||||
</tr>
|
||||
{oscCycleEndpoints.map((e) => (
|
||||
<tr key={e.message}>
|
||||
<td>{e.title}</td>
|
||||
<td>{e.message}</td>
|
||||
<td>{e.value}</td>
|
||||
</tr>
|
||||
))}
|
||||
<tr>
|
||||
<td className={style.labelNote} style={{ width: '30%' }}>
|
||||
Trigger
|
||||
</td>
|
||||
<td className={style.labelNote}>Message</td>
|
||||
<td className={style.labelNote}>Value (example | type)</td>
|
||||
</tr>
|
||||
{oscTriggerEndpoints.map((e) => (
|
||||
<tr key={e.message}>
|
||||
<td>{e.title}</td>
|
||||
<td>{e.message}</td>
|
||||
<td>{e.value}</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
<SubmitContainer
|
||||
revert={revert}
|
||||
|
||||
@@ -1,62 +0,0 @@
|
||||
import styles from './PreviewContainer.module.css';
|
||||
import IFrameLoader from './iframes/IFrameLoader';
|
||||
|
||||
// get origin from URL
|
||||
const serverURL = `${window.location.origin}`;
|
||||
|
||||
export default function PreviewContainer() {
|
||||
return (
|
||||
<div className={styles.previewContainer}>
|
||||
<div className={styles.previewItem}>
|
||||
<IFrameLoader title='Default Presenter' src={`${serverURL}/speaker`} />
|
||||
<a
|
||||
href={`${serverURL}/speaker`}
|
||||
target='_blank'
|
||||
rel='noreferrer'
|
||||
className={styles.label}
|
||||
>
|
||||
Default Presenter
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div className={styles.previewItem}>
|
||||
<IFrameLoader title='Audience' src={`${serverURL}/public`} />
|
||||
<a
|
||||
href={`${serverURL}/public`}
|
||||
target='_blank'
|
||||
rel='noreferrer'
|
||||
className={styles.label}
|
||||
>
|
||||
Audience
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div className={styles.previewItem}>
|
||||
<IFrameLoader title='Stage Manager' src={`${serverURL}/sm`} />
|
||||
<a
|
||||
href={`${serverURL}/sm`}
|
||||
target='_blank'
|
||||
rel='noreferrer'
|
||||
className={styles.label}
|
||||
>
|
||||
Stage Manager
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div className={styles.previewItem}>
|
||||
<IFrameLoader
|
||||
title='Lower third'
|
||||
src={`${serverURL}/lower?key=242424`}
|
||||
/>
|
||||
<a
|
||||
href={`${serverURL}/lower`}
|
||||
target='_blank'
|
||||
rel='noreferrer'
|
||||
className={styles.label}
|
||||
>
|
||||
Lower third
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,25 +0,0 @@
|
||||
.previewContainer {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
flex-wrap: wrap;
|
||||
justify-content: center;
|
||||
align-content: center;
|
||||
gap: 1em;
|
||||
}
|
||||
|
||||
.previewItem {
|
||||
width: 45%;
|
||||
}
|
||||
|
||||
.label {
|
||||
padding: 0.1em 4em;
|
||||
}
|
||||
|
||||
a::after {
|
||||
content: ' \2197';
|
||||
color: #ff7597;
|
||||
}
|
||||
|
||||
a:hover {
|
||||
color: #ff7597;
|
||||
}
|
||||
@@ -4,16 +4,12 @@ import { fetchEvent } from 'app/api/eventApi';
|
||||
import { useSocket } from 'app/context/socketContext';
|
||||
import { stringFromMillis } from 'ontime-utils/time';
|
||||
import { useFetch } from 'app/hooks/useFetch';
|
||||
import { EVENTS_TABLE, EVENT_TABLE } from 'app/api/apiConstants';
|
||||
import { EVENT_TABLE, EVENTS_TABLE } from 'app/api/apiConstants';
|
||||
|
||||
const withSocket = (Component) => {
|
||||
const WrappedComponent = (props) => {
|
||||
const {
|
||||
data: eventsData,
|
||||
} = useFetch(EVENTS_TABLE, fetchAllEvents);
|
||||
const {
|
||||
data: genData,
|
||||
} = useFetch(EVENT_TABLE, fetchEvent);
|
||||
return (props) => {
|
||||
const { data: eventsData } = useFetch(EVENTS_TABLE, fetchAllEvents);
|
||||
const { data: genData } = useFetch(EVENT_TABLE, fetchEvent);
|
||||
|
||||
const [publicEvents, setPublicEvents] = useState([]);
|
||||
const [backstageEvents, setBackstageEvents] = useState([]);
|
||||
@@ -34,6 +30,7 @@ const withSocket = (Component) => {
|
||||
const [timer, setTimer] = useState({
|
||||
clock: null,
|
||||
running: null,
|
||||
isNegative: null,
|
||||
startedAt: null,
|
||||
expectedFinish: null,
|
||||
});
|
||||
@@ -70,8 +67,8 @@ const withSocket = (Component) => {
|
||||
useEffect(() => {
|
||||
if (socket == null) return;
|
||||
|
||||
// Handle presenter messages
|
||||
socket.on('messages-presenter', (data) => {
|
||||
// Handle timer messages
|
||||
socket.on('messages-timer', (data) => {
|
||||
setPres({ ...data });
|
||||
});
|
||||
|
||||
@@ -121,14 +118,14 @@ const withSocket = (Component) => {
|
||||
socket.emit('get-messages');
|
||||
|
||||
// Ask for up to data
|
||||
socket.emit('get-presenter');
|
||||
socket.emit('get-timer');
|
||||
|
||||
// ask for timer
|
||||
socket.emit('get-timer');
|
||||
|
||||
// ask for playstate
|
||||
socket.emit('get-playstate');
|
||||
socket.emit('get-onAir')
|
||||
socket.emit('get-onAir');
|
||||
|
||||
// Ask for up titles
|
||||
socket.emit('get-titles');
|
||||
@@ -141,7 +138,7 @@ const withSocket = (Component) => {
|
||||
// Clear listeners
|
||||
return () => {
|
||||
socket.off('messages-public');
|
||||
socket.off('messages-presenter');
|
||||
socket.off('messages-timer');
|
||||
socket.off('messages-lower');
|
||||
socket.off('timer');
|
||||
socket.off('playstate');
|
||||
@@ -230,7 +227,7 @@ const withSocket = (Component) => {
|
||||
// get clock string
|
||||
const timeManager = {
|
||||
...timer,
|
||||
finished: playback === 'start' && timer.running <= 0 && timer.startedAt,
|
||||
finished: playback === 'start' && timer.isNegative && timer.startedAt,
|
||||
clock: stringFromMillis(timer.clock),
|
||||
clockNoSeconds: stringFromMillis(timer.clock, false),
|
||||
playstate: playback,
|
||||
@@ -255,8 +252,6 @@ const withSocket = (Component) => {
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
return WrappedComponent;
|
||||
};
|
||||
|
||||
export default withSocket;
|
||||
|
||||
@@ -27,7 +27,6 @@ export default function StageManager(props) {
|
||||
}, [backstageEvents]);
|
||||
|
||||
// Format messages
|
||||
|
||||
const showPubl = publ.text !== '' && publ.visible;
|
||||
|
||||
let stageTimer;
|
||||
@@ -35,7 +34,7 @@ export default function StageManager(props) {
|
||||
stageTimer = '- - : - -';
|
||||
} else {
|
||||
stageTimer = formatDisplay(Math.abs(time.running), true);
|
||||
if (time.running < 0) stageTimer = `-${stageTimer}`;
|
||||
if (time.isNegative) stageTimer = `-${stageTimer}`;
|
||||
}
|
||||
|
||||
// motion
|
||||
|
||||
@@ -1,28 +0,0 @@
|
||||
import style from './PresenterView.module.css';
|
||||
|
||||
export default function PresenterSimple() {
|
||||
return (
|
||||
<div className={style.container__graySimple}>
|
||||
{/* <div className={style.messageOverlayActive}>
|
||||
<div className={style.message}>Remember to smile</div>
|
||||
</div> */}
|
||||
|
||||
<div className={style.timerContainer}>
|
||||
<div className={style.countdownBig}>01:03</div>
|
||||
</div>
|
||||
|
||||
<div className={style.progress}>
|
||||
<div className={style.progressed}></div>
|
||||
</div>
|
||||
|
||||
{/* <div className={style.mainContainer}>
|
||||
<div className={style.finished}>TIME UP</div>
|
||||
</div> */}
|
||||
|
||||
<div className={style.clockContainer}>
|
||||
<div className={style.label}>Time Now</div>
|
||||
<div className={style.clock}>11:00:23</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -52,7 +52,7 @@ export default function Pip(props) {
|
||||
const showInfo =
|
||||
general.backstageInfo !== '' && general.backstageInfo != null;
|
||||
let stageTimer = formatDisplay(Math.abs(time.running), true);
|
||||
if (time.running < 0) stageTimer = `-${stageTimer}`;
|
||||
if (time.isNegative) stageTimer = `-${stageTimer}`;
|
||||
|
||||
return (
|
||||
<div className={style.container__gray}>
|
||||
|
||||
@@ -7,7 +7,7 @@ const isEqual = require('react-fast-compare');
|
||||
const areEqual = (prevProps, nextProps) => {
|
||||
return (
|
||||
isEqual(prevProps.title, nextProps.title) &&
|
||||
isEqual(prevProps.lower && nextProps.lower)
|
||||
isEqual(prevProps.lower, nextProps.lower)
|
||||
);
|
||||
};
|
||||
|
||||
@@ -56,6 +56,7 @@ const Lower = (props) => {
|
||||
clearTimeout(timeout);
|
||||
}
|
||||
};
|
||||
// eslint-disable-next-line
|
||||
}, [title.titleNow, title.subtitleNow, title.presenterNow]);
|
||||
|
||||
// TODO: sanitize data
|
||||
|
||||
@@ -49,7 +49,7 @@ export default function StudioClock(props) {
|
||||
>
|
||||
{title.titleNext}
|
||||
</div>
|
||||
<div className={time.running > 0 ? style.nextCountdown : style.nextCountdown__overtime}>
|
||||
<div className={time.isNegative ? style.nextCountdown : style.nextCountdown__overtime}>
|
||||
{selectedId != null && formatDisplay(time.running)}
|
||||
</div>
|
||||
<div className={style.indicators}>
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
import { useEffect } from 'react';
|
||||
import { formatDisplay } from '../../../common/utils/dateConfig';
|
||||
import NavLogo from '../../../common/components/nav/NavLogo';
|
||||
import style from './MinimalTimer.module.scss';
|
||||
|
||||
export default function MinimalTimer(props) {
|
||||
const { pres, time } = props;
|
||||
|
||||
// Set window title
|
||||
useEffect(() => {
|
||||
document.title = 'ontime - Minimal Timer';
|
||||
}, []);
|
||||
|
||||
const showOverlay = pres.text !== '' && pres.visible;
|
||||
const isPlaying = time.playstate !== 'pause';
|
||||
const timer = formatDisplay(time.running, true);
|
||||
const clean = timer.replaceAll(':', '');
|
||||
|
||||
return (
|
||||
<div className={time.finished ? style.containerFinished : style.container}>
|
||||
<div className={showOverlay ? style.messageOverlayActive : style.messageOverlay}>
|
||||
<div className={style.message}>{pres.text}</div>
|
||||
</div>
|
||||
<NavLogo />
|
||||
<div
|
||||
style={{ fontSize: `${89 / (clean.length - 1)}vw` }}
|
||||
className={isPlaying ? style.timer : style.timerPaused}
|
||||
>
|
||||
{time.isNegative ? `-${timer}` : timer}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
@use '../../../styles/main' as *;
|
||||
|
||||
.container,
|
||||
.containerFinished {
|
||||
background: $bg-black;
|
||||
height: 100vh;
|
||||
color: $title-white;
|
||||
display: grid;
|
||||
place-content: center;
|
||||
gap: 1vw;
|
||||
border: 1vw solid $bg-black;
|
||||
}
|
||||
|
||||
.containerFinished {
|
||||
border: 1vw solid $ontime-pink-variant;
|
||||
color: $ontime-pink-variant;
|
||||
transition: 0.3s;
|
||||
}
|
||||
|
||||
.timer,
|
||||
.timerPaused {
|
||||
font-family: "Arial Black", sans-serif;
|
||||
font-size: 20vw;
|
||||
color: inherit;
|
||||
opacity: 1;
|
||||
transition: 0.5s;
|
||||
transition-property: opacity;
|
||||
}
|
||||
|
||||
.timerPaused {
|
||||
opacity: 0.6;
|
||||
transition: 0.5s;
|
||||
}
|
||||
|
||||
/* =================== OVERLAY ===================*/
|
||||
|
||||
.messageOverlay,
|
||||
.messageOverlayActive {
|
||||
position: fixed;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background-color: rgba(0, 0, 0, 0.85);
|
||||
z-index: -1;
|
||||
opacity: 0;
|
||||
transition: 0.5s;
|
||||
}
|
||||
|
||||
.messageOverlayActive {
|
||||
opacity: 1;
|
||||
transition: 0.5s;
|
||||
transition-property: opacity;
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
.message {
|
||||
width: inherit;
|
||||
padding: 2vw;
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
color: white;
|
||||
transform: translate(-50%, -50%);
|
||||
-ms-transform: translate(-50%, -50%);
|
||||
font-size: 15vw;
|
||||
line-height: 30vh;
|
||||
text-align: center;
|
||||
font-weight: 600;
|
||||
}
|
||||
+30
-6
@@ -1,19 +1,35 @@
|
||||
import { AnimatePresence, motion } from 'framer-motion';
|
||||
import { useEffect } from 'react';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useSearchParams } from 'react-router-dom';
|
||||
import Countdown from 'common/components/countdown/Countdown';
|
||||
import MyProgressBar from 'common/components/myProgressBar/MyProgressBar';
|
||||
import NavLogo from 'common/components/nav/NavLogo';
|
||||
import TitleCard from 'common/components/views/TitleCard';
|
||||
import style from './PresenterView.module.css';
|
||||
import style from './Timer.module.scss';
|
||||
|
||||
export default function PresenterView(props) {
|
||||
export default function Timer(props) {
|
||||
const { general, pres, title, time } = props;
|
||||
const [elapsed, setElapsed] = useState(true);
|
||||
const [searchParams] = useSearchParams();
|
||||
|
||||
// Set window title
|
||||
useEffect(() => {
|
||||
document.title = 'ontime - Presenter Screen';
|
||||
document.title = 'ontime - Timer';
|
||||
}, []);
|
||||
|
||||
// eg. http://localhost:3000/timer?progress=up
|
||||
// Check for user options
|
||||
useEffect(() => {
|
||||
// progress: selector
|
||||
// Should be 'up' or 'down'
|
||||
const progress = searchParams.get('progress');
|
||||
if (progress === 'up') {
|
||||
setElapsed(true);
|
||||
} else if (progress === 'down') {
|
||||
setElapsed(false);
|
||||
}
|
||||
}, [searchParams]);
|
||||
|
||||
const showOverlay = pres.text !== '' && pres.visible;
|
||||
const isPlaying = time.playstate !== 'pause';
|
||||
const normalisedTime = Math.max(time.running, 0);
|
||||
@@ -21,7 +37,11 @@ export default function PresenterView(props) {
|
||||
// show timer if end message is empty
|
||||
const endMessage =
|
||||
general.endMessage == null || general.endMessage === '' ? (
|
||||
<Countdown time={time.running} hideZeroHours negative />
|
||||
<Countdown
|
||||
time={time.running}
|
||||
isNegative={time.isNegative}
|
||||
hideZeroHours
|
||||
/>
|
||||
) : (
|
||||
general.endMessage
|
||||
);
|
||||
@@ -79,7 +99,11 @@ export default function PresenterView(props) {
|
||||
isPlaying ? style.progressContainer : style.progressContainerPaused
|
||||
}
|
||||
>
|
||||
<MyProgressBar now={normalisedTime} complete={time.durationSeconds} />
|
||||
<MyProgressBar
|
||||
now={normalisedTime}
|
||||
complete={time.durationSeconds}
|
||||
showElapsed={elapsed}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
+9
-20
@@ -1,3 +1,5 @@
|
||||
@use '../../../styles/main' as *;
|
||||
|
||||
.container__gray,
|
||||
.container__grayFinished {
|
||||
margin: 0;
|
||||
@@ -5,9 +7,9 @@
|
||||
overflow: hidden;
|
||||
width: 100%; /* restrict the page width to viewport */
|
||||
|
||||
background: radial-gradient(circle, #202020 0%, #121212 80%);
|
||||
background: radial-gradient(circle, $bg-black-gradient 0%, $bg-black 80%);
|
||||
height: 100vh;
|
||||
color: #fffd;
|
||||
color: $title-white;
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr 5vw 1fr 1fr;
|
||||
grid-template-rows: auto 1fr auto minmax(25vh, auto);
|
||||
@@ -20,25 +22,11 @@
|
||||
padding: 1vw;
|
||||
}
|
||||
|
||||
.container__graySimple,
|
||||
.container__grayFinishedSimple {
|
||||
background: radial-gradient(circle, #202020 0%, #121212 80%);
|
||||
height: 100vh;
|
||||
color: #fffd;
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr 5vw 1fr 1fr;
|
||||
grid-template-rows: auto 1fr 30vh;
|
||||
grid-template-areas:
|
||||
' clck .... .... .... ....'
|
||||
' timr timr timr timr timr'
|
||||
' prog prog prog prog prog';
|
||||
gap: 1vw;
|
||||
}
|
||||
|
||||
.label {
|
||||
font-size: 1.3vw;
|
||||
color: #ff7597;
|
||||
color: $ontime-pink;
|
||||
}
|
||||
|
||||
/* =================== TITLES ===================*/
|
||||
|
||||
.nowContainer,
|
||||
@@ -52,6 +40,7 @@
|
||||
.nowContainer {
|
||||
grid-area: now;
|
||||
}
|
||||
|
||||
.nextContainer {
|
||||
grid-area: next;
|
||||
}
|
||||
@@ -71,7 +60,7 @@
|
||||
font-size: 12vw;
|
||||
line-height: 18vw;
|
||||
font-weight: 600;
|
||||
color: #ff6969;
|
||||
color: $ontime-pink-variant;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
@@ -95,7 +84,7 @@
|
||||
}
|
||||
|
||||
.container__grayFinished {
|
||||
border: 1vw solid #ff6969;
|
||||
border: 1vw solid $ontime-pink-variant;
|
||||
}
|
||||
|
||||
/* =================== OVERLAY ===================*/
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
$ontime-accent: #4bffabcc;
|
||||
$ontime-pink: #ff7597;
|
||||
$ontime-pink-variant: #ff6969;
|
||||
$ontime-roll: #2b6cb0;
|
||||
|
||||
$notes-color: #d69e2e;
|
||||
@@ -16,6 +17,11 @@ $light-text: #2b6cb022;
|
||||
|
||||
$error-red: #E53E3E;
|
||||
|
||||
$title-white: #fffd;
|
||||
$bg-black: #121212;
|
||||
$bg-black-gradient: #202020;
|
||||
|
||||
|
||||
//////////////////////////////////// general app element overriders
|
||||
|
||||
// no decoration on lists
|
||||
@@ -34,6 +40,7 @@ a {
|
||||
content: ' \2197';
|
||||
color: $ontime-pink;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
color: $ontime-pink;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,11 @@
|
||||
//////////////////////////////////// general app elements
|
||||
|
||||
@mixin main-container {
|
||||
background-color: rgba(0, 0, 0, 0.05);
|
||||
border: 1px solid rgba(0, 0, 0, 0.05);
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
@mixin container-bg {
|
||||
background-color: rgba(0, 0, 0, 0.13);
|
||||
border-radius: 2px;
|
||||
@@ -7,3 +13,9 @@
|
||||
margin: 0 0.5em;
|
||||
}
|
||||
|
||||
@mixin osc-label {
|
||||
color: #4bffabcc;
|
||||
font-size: 0.8em;
|
||||
-webkit-user-select: text;
|
||||
user-select: text;
|
||||
}
|
||||
|
||||
@@ -9002,6 +9002,15 @@ prop-types@^15.6.2, prop-types@^15.7.2:
|
||||
object-assign "^4.1.1"
|
||||
react-is "^16.8.1"
|
||||
|
||||
prop-types@^15.8.1:
|
||||
version "15.8.1"
|
||||
resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.8.1.tgz#67d87bf1a694f48435cf332c24af10214a3140b5"
|
||||
integrity sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==
|
||||
dependencies:
|
||||
loose-envify "^1.4.0"
|
||||
object-assign "^4.1.1"
|
||||
react-is "^16.13.1"
|
||||
|
||||
proxy-addr@~2.0.5:
|
||||
version "2.0.6"
|
||||
resolved "https://registry.yarnpkg.com/proxy-addr/-/proxy-addr-2.0.6.tgz#fdc2336505447d3f2f2c638ed272caf614bbb2bf"
|
||||
|
||||
+2
-1
@@ -13,7 +13,8 @@
|
||||
"rules": {
|
||||
"prettier/prettier": ["error", {
|
||||
"endOfLine": "auto",
|
||||
"singleQuote": true
|
||||
"singleQuote": true,
|
||||
"printWidth": 100
|
||||
}]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"viewportWidth": 1920,
|
||||
"viewportHeight": 1080,
|
||||
"video": false
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
// go trough routes and make sure things render as expected
|
||||
describe('validate routes', () => {
|
||||
it('viewer routes', () => {
|
||||
cy.visit('http://localhost:4001/');
|
||||
cy.contains('Time Now');
|
||||
|
||||
cy.visit('http://localhost:4001/timer');
|
||||
cy.contains('Time Now');
|
||||
|
||||
cy.visit('http://localhost:4001/presenter');
|
||||
cy.contains('Time Now');
|
||||
|
||||
cy.visit('http://localhost:4001/speaker');
|
||||
cy.contains('Time Now');
|
||||
|
||||
cy.visit('http://localhost:4001/stage');
|
||||
cy.contains('Time Now');
|
||||
|
||||
cy.visit('http://localhost:4001/backstage');
|
||||
cy.contains('Today');
|
||||
cy.contains('Time Now');
|
||||
cy.contains('Info');
|
||||
|
||||
cy.visit('http://localhost:4001/sm');
|
||||
cy.contains('Today');
|
||||
cy.contains('Time Now');
|
||||
cy.contains('Info');
|
||||
|
||||
cy.visit('http://localhost:4001/public');
|
||||
cy.contains('Today');
|
||||
cy.contains('Time Now');
|
||||
cy.contains('Info');
|
||||
|
||||
cy.visit('http://localhost:4001/pip');
|
||||
cy.contains('Today');
|
||||
cy.contains('Info');
|
||||
|
||||
cy.visit('http://localhost:4001/studio');
|
||||
cy.contains('ON AIR');
|
||||
});
|
||||
it('editor routes', () => {
|
||||
cy.visit('http://localhost:4001/editor');
|
||||
cy.contains('Event List');
|
||||
cy.contains('Timer Control');
|
||||
cy.contains('Display Messages');
|
||||
cy.contains('Info');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,22 @@
|
||||
/// <reference types="cypress" />
|
||||
// ***********************************************************
|
||||
// This example plugins/index.js can be used to load plugins
|
||||
//
|
||||
// You can change the location of this file or turn off loading
|
||||
// the plugins file with the 'pluginsFile' configuration option.
|
||||
//
|
||||
// You can read more here:
|
||||
// https://on.cypress.io/plugins-guide
|
||||
// ***********************************************************
|
||||
|
||||
// This function is called when a project is opened or re-opened (e.g. due to
|
||||
// the project's config changing)
|
||||
|
||||
/**
|
||||
* @type {Cypress.PluginConfig}
|
||||
*/
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
module.exports = (on, config) => {
|
||||
// `on` is used to hook into various events Cypress emits
|
||||
// `config` is the resolved Cypress config
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
// ***********************************************
|
||||
// This example commands.js shows you how to
|
||||
// create various custom commands and overwrite
|
||||
// existing commands.
|
||||
//
|
||||
// For more comprehensive examples of custom
|
||||
// commands please read more here:
|
||||
// https://on.cypress.io/custom-commands
|
||||
// ***********************************************
|
||||
//
|
||||
//
|
||||
// -- This is a parent command --
|
||||
// Cypress.Commands.add('login', (email, password) => { ... })
|
||||
//
|
||||
//
|
||||
// -- This is a child command --
|
||||
// Cypress.Commands.add('drag', { prevSubject: 'element'}, (subject, options) => { ... })
|
||||
//
|
||||
//
|
||||
// -- This is a dual command --
|
||||
// Cypress.Commands.add('dismiss', { prevSubject: 'optional'}, (subject, options) => { ... })
|
||||
//
|
||||
//
|
||||
// -- This will overwrite an existing command --
|
||||
// Cypress.Commands.overwrite('visit', (originalFn, url, options) => { ... })
|
||||
@@ -0,0 +1,20 @@
|
||||
// ***********************************************************
|
||||
// This example support/index.js is processed and
|
||||
// loaded automatically before your test files.
|
||||
//
|
||||
// This is a great place to put global configuration and
|
||||
// behavior that modifies Cypress.
|
||||
//
|
||||
// You can change the location of this file or turn off
|
||||
// automatically serving support files with the
|
||||
// 'supportFile' configuration option.
|
||||
//
|
||||
// You can read more here:
|
||||
// https://on.cypress.io/configuration
|
||||
// ***********************************************************
|
||||
|
||||
// Import commands.js using ES2015 syntax:
|
||||
import './commands'
|
||||
|
||||
// Alternatively you can use CommonJS syntax:
|
||||
// require('./commands')
|
||||
+10
-4
@@ -23,9 +23,7 @@ const nodePath =
|
||||
|
||||
(async () => {
|
||||
try {
|
||||
const { startServer, startOSCServer } = await import(
|
||||
nodePath
|
||||
);
|
||||
const { startServer, startOSCServer } = await import(nodePath);
|
||||
// Start express server
|
||||
loaded = await startServer();
|
||||
|
||||
@@ -61,7 +59,6 @@ if (!lock) {
|
||||
'An instance if the App is already running.'
|
||||
);
|
||||
app.quit();
|
||||
return;
|
||||
} else {
|
||||
app.on('second-instance', (event, commandLine, workingDirectory) => {
|
||||
// Someone tried to run a second instance, we should focus our window.
|
||||
@@ -191,6 +188,15 @@ app.whenReady().then(() => {
|
||||
|
||||
const trayContextMenu = Menu.buildFromTemplate(trayMenuTemplate);
|
||||
tray.setContextMenu(trayContextMenu);
|
||||
|
||||
// on tray click event, show main window
|
||||
tray.on('click', function (e) {
|
||||
if (!win.isVisible()) {
|
||||
win.show();
|
||||
}
|
||||
win.focus();
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
// unregister shortcuts before quitting
|
||||
|
||||
+23
-7
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "ontime",
|
||||
"version": "0.5.0",
|
||||
"version": "0.6.2",
|
||||
"author": "Carlos Valente",
|
||||
"description": "Time keeping for live events",
|
||||
"repository": "https://github.com/cpvalente/ontime",
|
||||
@@ -12,22 +12,30 @@
|
||||
"license": "AGPL-3.0-only",
|
||||
"main": "main.js",
|
||||
"devDependencies": {
|
||||
"cypress": "^9.2.1",
|
||||
"electron": "^13.6.1",
|
||||
"electron-builder": "^22.14.5",
|
||||
"eslint": "^8.5.0",
|
||||
"eslint-config-prettier": "^8.3.0",
|
||||
"eslint-plugin-prettier": "^4.0.0",
|
||||
"jest": "^27.4.5",
|
||||
"prettier": "^2.5.1"
|
||||
"nodemon": "^2.0.15",
|
||||
"prettier": "^2.5.1",
|
||||
"start-server-and-test": "^1.14.0",
|
||||
"supertest": "^6.1.6"
|
||||
},
|
||||
"scripts": {
|
||||
"nodestart": "NODE_ENV=development node src/app.js",
|
||||
"make": "mkdir -p src/data",
|
||||
"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 .",
|
||||
"start:server": "nodemon --experimental-modules --es-module-specifier-resolution=node src/app.js",
|
||||
"cy:run": "cypress run",
|
||||
"cypress": "start-server-and-test start http://localhost:4001 cy:run",
|
||||
"pack": "electron-builder --dir",
|
||||
"dist": "electron-builder",
|
||||
"dist-win": "electron-builder --publish=never --x64 --win",
|
||||
@@ -38,7 +46,8 @@
|
||||
"testEnvironment": "node",
|
||||
"testRunner": "jasmine2",
|
||||
"testPathIgnorePatterns": [
|
||||
"dist"
|
||||
"dist",
|
||||
"cypress"
|
||||
]
|
||||
},
|
||||
"build": {
|
||||
@@ -74,7 +83,9 @@
|
||||
"**/*",
|
||||
"assets/",
|
||||
"!**/{yarn.lock,yarn-error.log}",
|
||||
"!**/{test,tests,__test__,__tests__,mock,mocks,__mock__,__mocks__}"
|
||||
"!**/{test,tests,__test__,__tests__}",
|
||||
"!**/{mock,mocks,__mock__,__mocks__}",
|
||||
"!*{.spec.js,*.test.js}"
|
||||
],
|
||||
"directories": {
|
||||
"buildResources": "./assets/"
|
||||
@@ -86,7 +97,8 @@
|
||||
"filter": [
|
||||
"**/*",
|
||||
"!**/{yarn.lock,yarn-error.log}",
|
||||
"!**/{test,tests,__test__,__tests__,mock,mocks,__mock__,__mocks__}"
|
||||
"!**/{test,tests,__test__,__tests__}",
|
||||
"!**/{mock,mocks,__mock__,__mocks__}"
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -95,7 +107,8 @@
|
||||
"filter": [
|
||||
"**/*",
|
||||
"!**/{yarn.lock,yarn-error.log}",
|
||||
"!**/{test,tests,__test__,__tests__,mock,mocks,__mock__,__mocks__}"
|
||||
"!**/{test,tests,__test__,__tests__}",
|
||||
"!**/{mock,mocks,__mock__,__mocks__}"
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -103,8 +116,11 @@
|
||||
"to": "extraResources/utils",
|
||||
"filter": [
|
||||
"**/*",
|
||||
"!cypress/",
|
||||
"!**/{yarn.lock,yarn-error.log}",
|
||||
"!**/{test,tests,__test__,__tests__,mock,mocks,__mock__,__mocks__}"
|
||||
"!**/{test,tests,__test__,__tests__}",
|
||||
"!**/{mock,mocks,__mock__,__mocks__}",
|
||||
"!*{.spec.js,*.test.js}"
|
||||
]
|
||||
}
|
||||
]
|
||||
|
||||
+12
-4
@@ -123,6 +123,7 @@ const osc = data.osc;
|
||||
const oscIP = osc?.targetIP || config.osc.targetIP;
|
||||
const oscOutPort = osc?.portOut || config.osc.portOut;
|
||||
const oscInPort = osc?.port || config.osc.port;
|
||||
const oscInEnabled = osc?.enabled !== undefined ? osc.enabled : config.osc.inputEnabled;
|
||||
|
||||
const serverPort = data.settings.serverPort || config.server.port;
|
||||
|
||||
@@ -130,14 +131,19 @@ const serverPort = data.settings.serverPort || config.server.port;
|
||||
import { initiateOSC, shutdownOSCServer } from './controllers/OscController.js';
|
||||
|
||||
export const startOSCServer = async (overrideConfig = null) => {
|
||||
|
||||
if (!oscInEnabled) {
|
||||
global.timer.info('RX', 'OSC Input Disabled')
|
||||
return;
|
||||
}
|
||||
|
||||
// Setup default port
|
||||
const oscSettings = {
|
||||
port: overrideConfig?.port || oscInPort,
|
||||
ipOut: oscIP,
|
||||
portOut: oscOutPort,
|
||||
};
|
||||
|
||||
// Start OSC Server
|
||||
global.timer.info('RX', `Starting OSC Server on port: ${oscInPort}`)
|
||||
initiateOSC(oscSettings);
|
||||
};
|
||||
|
||||
@@ -156,7 +162,7 @@ export const startServer = async (overrideConfig = null) => {
|
||||
|
||||
// Start server
|
||||
const returnMessage = `Ontime is listening on port ${port}`;
|
||||
server.listen(port, '0.0.0.0', () => console.log(returnMessage));
|
||||
server.listen(port, '0.0.0.0');
|
||||
|
||||
// OSC Config
|
||||
const oscConfig = {
|
||||
@@ -167,7 +173,7 @@ export const startServer = async (overrideConfig = null) => {
|
||||
// init timer
|
||||
global.timer = new EventTimer(server, config.timer, oscConfig, data.http);
|
||||
global.timer.setupWithEventList(data.events);
|
||||
|
||||
global.timer.info('SERVER', returnMessage);
|
||||
return returnMessage;
|
||||
};
|
||||
|
||||
@@ -185,3 +191,5 @@ export const shutdown = async () => {
|
||||
// shutdown timer
|
||||
global.timer.shutdown();
|
||||
};
|
||||
|
||||
export { server, app };
|
||||
@@ -1,11 +1,6 @@
|
||||
import { Timer } from './Timer.js';
|
||||
import { Server } from 'socket.io';
|
||||
import {
|
||||
DAY_TO_MS,
|
||||
getSelectionByRoll,
|
||||
replacePlaceholder,
|
||||
updateRoll,
|
||||
} from './classUtils.js';
|
||||
import { DAY_TO_MS, getSelectionByRoll, replacePlaceholder, updateRoll } from './classUtils.js';
|
||||
import { OSCIntegration } from './integrations/Osc.js';
|
||||
import { HTTPIntegration } from './integrations/Http.js';
|
||||
import { cleanURL } from '../utils/url.js';
|
||||
@@ -90,10 +85,7 @@ export class EventTimer extends Timer {
|
||||
});
|
||||
|
||||
// set recurrent emits
|
||||
this._interval = setInterval(
|
||||
() => this.runCycle(),
|
||||
timerConfig?.refresh || 1000
|
||||
);
|
||||
this._interval = setInterval(() => this.runCycle(), timerConfig?.refresh || 1000);
|
||||
|
||||
// listen to new connections
|
||||
this._listenToConnections();
|
||||
@@ -306,8 +298,8 @@ export class EventTimer extends Timer {
|
||||
// _finish at is only set when an event is loaded
|
||||
if (this._finishAt > 0) {
|
||||
this.sendOsc(this.osc.implemented.play);
|
||||
this.sendOsc(this.osc.implemented.eventNumber, this.selectedEventIndex || 0);
|
||||
}
|
||||
|
||||
// check integrations - http
|
||||
if (h?.onLoad?.enabled) {
|
||||
if (h?.onLoad?.url != null || h?.onStart?.url !== '') {
|
||||
@@ -327,14 +319,9 @@ export class EventTimer extends Timer {
|
||||
if (this.state === 'start' || this.state === 'roll') {
|
||||
if (this.current != null && this.secondaryTimer == null) {
|
||||
this.sendOsc(this.osc.implemented.time, this.timeTag);
|
||||
this.sendOsc(
|
||||
this.osc.implemented.overtime,
|
||||
this.current > 0 ? 0 : 1
|
||||
);
|
||||
this.sendOsc(
|
||||
this.osc.implemented.title,
|
||||
this.titles?.titleNow || ''
|
||||
);
|
||||
this.sendOsc(this.osc.implemented.overtime, this.current > 0 ? 0 : 1);
|
||||
this.sendOsc(this.osc.implemented.title, this.titles?.titleNow || '');
|
||||
this.sendOsc(this.osc.implemented.presenter, this.titles?.presenterNow || '');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -467,17 +454,13 @@ export class EventTimer extends Timer {
|
||||
selectedEventId: this.selectedEventId,
|
||||
current: this.current,
|
||||
// safeguard on midnight rollover
|
||||
_finishAt:
|
||||
this._finishAt >= this._startedAt
|
||||
? this._finishAt
|
||||
: this._finishAt + DAY_TO_MS,
|
||||
_finishAt: this._finishAt >= this._startedAt ? this._finishAt : this._finishAt + DAY_TO_MS,
|
||||
clock: this.clock,
|
||||
secondaryTimer: this.secondaryTimer,
|
||||
_secondaryTarget: this._secondaryTarget,
|
||||
};
|
||||
|
||||
const { updatedTimer, updatedSecondaryTimer, doRollLoad, isFinished } =
|
||||
updateRoll(u);
|
||||
const { updatedTimer, updatedSecondaryTimer, doRollLoad, isFinished } = updateRoll(u);
|
||||
|
||||
this.current = updatedTimer;
|
||||
this.secondaryTimer = updatedSecondaryTimer;
|
||||
@@ -504,13 +487,13 @@ export class EventTimer extends Timer {
|
||||
switch (action) {
|
||||
/*******************************************/
|
||||
// Presenter message
|
||||
case 'set-presenter-text':
|
||||
case 'set-timer-text':
|
||||
this.presenter.text = payload;
|
||||
this.broadcastThis('messages-presenter', this.presenter);
|
||||
this.broadcastThis('messages-timer', this.presenter);
|
||||
break;
|
||||
case 'set-presenter-visible':
|
||||
case 'set-timer-visible':
|
||||
this.presenter.visible = payload;
|
||||
this.broadcastThis('messages-presenter', this.presenter);
|
||||
this.broadcastThis('messages-timer', this.presenter);
|
||||
break;
|
||||
|
||||
/*******************************************/
|
||||
@@ -553,9 +536,7 @@ export class EventTimer extends Timer {
|
||||
// keep track of connections
|
||||
this._numClients++;
|
||||
this._clientNames[socket.id] = getRandomName();
|
||||
const m = `${this._numClients} Clients with new connection: ${
|
||||
this._clientNames[socket.id]
|
||||
}`;
|
||||
const m = `${this._numClients} Clients with new connection: ${this._clientNames[socket.id]}`;
|
||||
this.info('CLIENT', m);
|
||||
|
||||
// send state
|
||||
@@ -572,9 +553,7 @@ export class EventTimer extends Timer {
|
||||
/********************************/
|
||||
socket.on('disconnect', () => {
|
||||
this._numClients--;
|
||||
const m = `${this._numClients} Clients with disconnection: ${
|
||||
this._clientNames[socket.id]
|
||||
}`;
|
||||
const m = `${this._numClients} Clients with disconnection: ${this._clientNames[socket.id]}`;
|
||||
delete this._clientNames[socket.id];
|
||||
this.info('CLIENT', m);
|
||||
});
|
||||
@@ -679,22 +658,22 @@ export class EventTimer extends Timer {
|
||||
|
||||
// Messages
|
||||
socket.on('get-messages', () => {
|
||||
this.broadcastThis('messages-presenter', this.presenter);
|
||||
this.broadcastThis('messages-timer', this.presenter);
|
||||
this.broadcastThis('messages-public', this.public);
|
||||
this.broadcastThis('messages-lower', this.lower);
|
||||
});
|
||||
|
||||
// Presenter message
|
||||
socket.on('set-presenter-text', (data) => {
|
||||
this._setTitles('set-presenter-text', data);
|
||||
socket.on('set-timer-text', (data) => {
|
||||
this._setTitles('set-timer-text', data);
|
||||
});
|
||||
|
||||
socket.on('set-presenter-visible', (data) => {
|
||||
this._setTitles('set-presenter-visible', data);
|
||||
socket.on('set-timer-visible', (data) => {
|
||||
this._setTitles('set-timer-visible', data);
|
||||
});
|
||||
|
||||
socket.on('get-presenter', () => {
|
||||
this.broadcastThis('messages-presenter', this.presenter);
|
||||
socket.on('get-timer', () => {
|
||||
this.broadcastThis('messages-timer', this.presenter);
|
||||
});
|
||||
/*******************************************/
|
||||
// Public message
|
||||
@@ -800,9 +779,7 @@ export class EventTimer extends Timer {
|
||||
} else if (this.selectedEventId != null) {
|
||||
// handle reload selected
|
||||
// Look for event (order might have changed)
|
||||
const eventIndex = this._eventlist.findIndex(
|
||||
(e) => e.id === this.selectedEventId
|
||||
);
|
||||
const eventIndex = this._eventlist.findIndex((e) => e.id === this.selectedEventId);
|
||||
|
||||
// Maybe is missing
|
||||
if (eventIndex === -1) {
|
||||
@@ -842,10 +819,7 @@ export class EventTimer extends Timer {
|
||||
if (e.id === this.selectedEventId) {
|
||||
// handle reload selected
|
||||
// Reload data if running
|
||||
let type =
|
||||
this.selectedEventId === id && this._startedAt != null
|
||||
? 'reload'
|
||||
: 'load';
|
||||
let type = this.selectedEventId === id && this._startedAt != null ? 'reload' : 'load';
|
||||
this.loadEvent(this.selectedEventIndex, type);
|
||||
} else if (e.id === this.nextEventId) {
|
||||
// roll needs to recalculate
|
||||
@@ -891,9 +865,7 @@ export class EventTimer extends Timer {
|
||||
}
|
||||
|
||||
// update selected event index
|
||||
this.selectedEventIndex = this._eventlist.findIndex(
|
||||
(e) => e.id === this.selectedEventId
|
||||
);
|
||||
this.selectedEventIndex = this._eventlist.findIndex((e) => e.id === this.selectedEventId);
|
||||
|
||||
// reload titles if necessary
|
||||
if (eventId === this.nextEventId || eventId === this.nextPublicEventId) {
|
||||
@@ -998,10 +970,7 @@ export class EventTimer extends Timer {
|
||||
|
||||
// iterate backwards to find it
|
||||
for (let i = this.selectedEventIndex; i >= 0; i--) {
|
||||
if (
|
||||
this._eventlist[i].type === 'event' &&
|
||||
this._eventlist[i].isPublic
|
||||
) {
|
||||
if (this._eventlist[i].type === 'event' && this._eventlist[i].isPublic) {
|
||||
this._loadThisTitles(this._eventlist[i], 'now-public');
|
||||
break;
|
||||
}
|
||||
@@ -1214,15 +1183,8 @@ export class EventTimer extends Timer {
|
||||
this._resetSelection();
|
||||
}
|
||||
|
||||
const {
|
||||
nowIndex,
|
||||
nowId,
|
||||
publicIndex,
|
||||
nextIndex,
|
||||
publicNextIndex,
|
||||
timers,
|
||||
timeToNext,
|
||||
} = getSelectionByRoll(this._eventlist, now);
|
||||
const { nowIndex, nowId, publicIndex, nextIndex, publicNextIndex, timers, timeToNext } =
|
||||
getSelectionByRoll(this._eventlist, now);
|
||||
|
||||
// nothing to play, unload
|
||||
if (nowIndex === null && nextIndex === null) {
|
||||
@@ -1327,8 +1289,7 @@ export class EventTimer extends Timer {
|
||||
// change playstate
|
||||
this.pause();
|
||||
|
||||
const gotoEvent =
|
||||
this.selectedEventIndex > 0 ? this.selectedEventIndex - 1 : 0;
|
||||
const gotoEvent = this.selectedEventIndex > 0 ? this.selectedEventIndex - 1 : 0;
|
||||
|
||||
if (gotoEvent === this.selectedEventIndex) return;
|
||||
this.loadEvent(gotoEvent);
|
||||
@@ -1390,6 +1351,7 @@ export class EventTimer extends Timer {
|
||||
}
|
||||
|
||||
/****************************************************************************/
|
||||
|
||||
/**
|
||||
* Logger logic
|
||||
* -------------
|
||||
@@ -1453,6 +1415,7 @@ export class EventTimer extends Timer {
|
||||
}
|
||||
|
||||
/****************************************************************************/
|
||||
|
||||
/**
|
||||
* Integrations
|
||||
* -------------
|
||||
@@ -1473,4 +1436,19 @@ export class EventTimer extends Timer {
|
||||
this.error('TX', reply.message);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds sync object
|
||||
* @returns {{running: number, timer: (null|string|*), presenter: null, playback: string, clock: null, title: null}}
|
||||
*/
|
||||
poll() {
|
||||
return {
|
||||
clock: this.clock,
|
||||
running: Timer.toSeconds(this.current),
|
||||
timer: this.timeTag,
|
||||
playback: this.state,
|
||||
title: this.titles.titleNow,
|
||||
presenter: this.titles.presenterNow,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -50,8 +50,7 @@ export class Timer {
|
||||
if (this._startedAt == null) this._startedAt = now;
|
||||
|
||||
// update current timer
|
||||
this.current =
|
||||
this._startedAt + this.duration + this._pausedTotal - now;
|
||||
this.current = this._startedAt + this.duration + this._pausedTotal - now;
|
||||
|
||||
// enable flag
|
||||
checkFinish = true;
|
||||
@@ -93,8 +92,8 @@ export class Timer {
|
||||
|
||||
// helpers
|
||||
static toSeconds(millis) {
|
||||
if (millis == null) return null;
|
||||
return Math.ceil(millis * 0.001);
|
||||
if (millis == null) return 0;
|
||||
return millis < 0 ? Math.ceil(millis * 0.001) : Math.floor(millis * 0.001);
|
||||
}
|
||||
|
||||
// get current time in epoc
|
||||
@@ -151,6 +150,7 @@ export class Timer {
|
||||
getTimeObject() {
|
||||
return {
|
||||
clock: this.clock,
|
||||
isNegative: this.current < 0,
|
||||
running: Timer.toSeconds(this.current),
|
||||
secondary: Timer.toSeconds(this.secondaryTimer),
|
||||
durationSeconds: Timer.toSeconds(this.duration),
|
||||
|
||||
@@ -4,26 +4,26 @@ import {
|
||||
replacePlaceholder,
|
||||
normaliseEndTime,
|
||||
sortArrayByProperty,
|
||||
updateRoll
|
||||
updateRoll,
|
||||
} from '../classUtils.js';
|
||||
|
||||
// test sortArrayByProperty()
|
||||
describe('sort simple arrays of objects', () => {
|
||||
it('sort array 1-5', () => {
|
||||
const arr1 = [
|
||||
{timeStart: 1},
|
||||
{timeStart: 5},
|
||||
{timeStart: 3},
|
||||
{timeStart: 2},
|
||||
{timeStart: 4},
|
||||
{ timeStart: 1 },
|
||||
{ timeStart: 5 },
|
||||
{ timeStart: 3 },
|
||||
{ timeStart: 2 },
|
||||
{ timeStart: 4 },
|
||||
];
|
||||
|
||||
const arr1Expected = [
|
||||
{timeStart: 1},
|
||||
{timeStart: 2},
|
||||
{timeStart: 3},
|
||||
{timeStart: 4},
|
||||
{timeStart: 5},
|
||||
{ timeStart: 1 },
|
||||
{ timeStart: 2 },
|
||||
{ timeStart: 3 },
|
||||
{ timeStart: 4 },
|
||||
{ timeStart: 5 },
|
||||
];
|
||||
|
||||
const sorted = sortArrayByProperty(arr1, 'timeStart');
|
||||
@@ -32,21 +32,21 @@ describe('sort simple arrays of objects', () => {
|
||||
|
||||
it('sort array 1-5 with null', () => {
|
||||
const arr1 = [
|
||||
{timeStart: 1},
|
||||
{timeStart: 5},
|
||||
{timeStart: 3},
|
||||
{timeStart: 2},
|
||||
{timeStart: 4},
|
||||
{timeStart: null},
|
||||
{ timeStart: 1 },
|
||||
{ timeStart: 5 },
|
||||
{ timeStart: 3 },
|
||||
{ timeStart: 2 },
|
||||
{ timeStart: 4 },
|
||||
{ timeStart: null },
|
||||
];
|
||||
|
||||
const arr1Expected = [
|
||||
{timeStart: null},
|
||||
{timeStart: 1},
|
||||
{timeStart: 2},
|
||||
{timeStart: 3},
|
||||
{timeStart: 4},
|
||||
{timeStart: 5},
|
||||
{ timeStart: null },
|
||||
{ timeStart: 1 },
|
||||
{ timeStart: 2 },
|
||||
{ timeStart: 3 },
|
||||
{ timeStart: 4 },
|
||||
{ timeStart: 5 },
|
||||
];
|
||||
|
||||
const sorted = sortArrayByProperty(arr1, 'timeStart');
|
||||
@@ -393,78 +393,76 @@ describe('test that roll behaviour with overlapping times', () => {
|
||||
// test replacePlaceholder()
|
||||
describe('test that it replaces data correctly', () => {
|
||||
const values = {
|
||||
$timer: "timer",
|
||||
$title: "title",
|
||||
$presenter: "presenter",
|
||||
$subtitle: "subtitle",
|
||||
"$next-title": "next title",
|
||||
"$next-presenter": "next presenter",
|
||||
"$next-subtitle": "next subtitle"
|
||||
$timer: 'timer',
|
||||
$title: 'title',
|
||||
$presenter: 'presenter',
|
||||
$subtitle: 'subtitle',
|
||||
'$next-title': 'next title',
|
||||
'$next-presenter': 'next presenter',
|
||||
'$next-subtitle': 'next subtitle',
|
||||
};
|
||||
|
||||
it('replaces timer', () => {
|
||||
const test = '___1232132 $timer';
|
||||
const expected = '___1232132 timer';
|
||||
const s = replacePlaceholder(test, values)
|
||||
const s = replacePlaceholder(test, values);
|
||||
expect(s).toBe(expected);
|
||||
});
|
||||
|
||||
it('replaces title', () => {
|
||||
const test = '___1232132 $title';
|
||||
const expected = '___1232132 title';
|
||||
const s = replacePlaceholder(test, values)
|
||||
const s = replacePlaceholder(test, values);
|
||||
expect(s).toBe(expected);
|
||||
});
|
||||
|
||||
it('replaces presenter', () => {
|
||||
const test = '___1232132 $presenter';
|
||||
const expected = '___1232132 presenter';
|
||||
const s = replacePlaceholder(test, values)
|
||||
const s = replacePlaceholder(test, values);
|
||||
expect(s).toBe(expected);
|
||||
});
|
||||
|
||||
it('replaces subtitle', () => {
|
||||
const test = '___1232132 $subtitle';
|
||||
const expected = '___1232132 subtitle';
|
||||
const s = replacePlaceholder(test, values)
|
||||
const s = replacePlaceholder(test, values);
|
||||
expect(s).toBe(expected);
|
||||
});
|
||||
|
||||
it('replaces next next title', () => {
|
||||
const test = '___1232132 $next-title';
|
||||
const expected = '___1232132 next title';
|
||||
const s = replacePlaceholder(test, values)
|
||||
const s = replacePlaceholder(test, values);
|
||||
expect(s).toBe(expected);
|
||||
});
|
||||
|
||||
it('replaces next presenter', () => {
|
||||
const test = '___1232132 $next-presenter';
|
||||
const expected = '___1232132 next presenter';
|
||||
const s = replacePlaceholder(test, values)
|
||||
const s = replacePlaceholder(test, values);
|
||||
expect(s).toBe(expected);
|
||||
});
|
||||
|
||||
it('replaces next subtitle', () => {
|
||||
const test = '___1232132 $next-subtitle';
|
||||
const expected = '___1232132 next subtitle';
|
||||
const s = replacePlaceholder(test, values)
|
||||
const s = replacePlaceholder(test, values);
|
||||
expect(s).toBe(expected);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
// test getSelectionByRoll() on issue #58
|
||||
describe('test that roll behaviour multi day event edge cases', () => {
|
||||
|
||||
it('if the start time is the day after end time, and start time is earlier than now', () => {
|
||||
const now = 66600000; // 19:30
|
||||
const now = 66600000; // 19:30
|
||||
const eventlist = [
|
||||
{
|
||||
id: 1,
|
||||
timeStart: 66000000, // 19:20
|
||||
timeEnd: 54600000, // 16:10
|
||||
timeStart: 66000000, // 19:20
|
||||
timeEnd: 54600000, // 16:10
|
||||
isPublic: false,
|
||||
}
|
||||
},
|
||||
];
|
||||
const expected = {
|
||||
nowIndex: 0,
|
||||
@@ -486,14 +484,14 @@ describe('test that roll behaviour multi day event edge cases', () => {
|
||||
});
|
||||
|
||||
it('if the start time is the day after end time, and both are later than now', () => {
|
||||
const now = 66840000; // 19:34
|
||||
const now = 66840000; // 19:34
|
||||
const eventlist = [
|
||||
{
|
||||
id: 1,
|
||||
timeStart: 67200000, // 19:40
|
||||
timeEnd: 66900000, // 19:35
|
||||
timeStart: 67200000, // 19:40
|
||||
timeEnd: 66900000, // 19:35
|
||||
isPublic: false,
|
||||
}
|
||||
},
|
||||
];
|
||||
const expected = {
|
||||
nowIndex: null,
|
||||
@@ -512,11 +510,10 @@ describe('test that roll behaviour multi day event edge cases', () => {
|
||||
|
||||
// test normaliseEndTime() on issue #58
|
||||
test('test typical scenarios', () => {
|
||||
|
||||
const t1 = {
|
||||
start: 10,
|
||||
end: 20,
|
||||
}
|
||||
};
|
||||
const t1_expected = 20;
|
||||
|
||||
expect(normaliseEndTime(t1.start, t1.end)).toBe(t1_expected);
|
||||
@@ -524,7 +521,7 @@ test('test typical scenarios', () => {
|
||||
const t2 = {
|
||||
start: 10 + DAY_TO_MS,
|
||||
end: 20,
|
||||
}
|
||||
};
|
||||
const t2_expected = 20 + DAY_TO_MS;
|
||||
|
||||
expect(normaliseEndTime(t2.start, t2.end)).toBe(t2_expected);
|
||||
@@ -532,16 +529,14 @@ test('test typical scenarios', () => {
|
||||
const t3 = {
|
||||
start: 10,
|
||||
end: 10,
|
||||
}
|
||||
};
|
||||
const t3_expected = 10;
|
||||
|
||||
expect(normaliseEndTime(t3.start, t3.end)).toBe(t3_expected);
|
||||
});
|
||||
|
||||
|
||||
// test updateRoll()
|
||||
describe('typical scenarios', () => {
|
||||
|
||||
it('it updates running events correctly', () => {
|
||||
const timers = {
|
||||
selectedEventId: 1,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import {Timer} from "../Timer";
|
||||
import { Timer } from '../Timer';
|
||||
|
||||
test('object instantiates correctly', () => {
|
||||
const t = new Timer();
|
||||
@@ -21,15 +21,15 @@ test('object instantiates correctly', () => {
|
||||
|
||||
test('convert between mills and seconds correctly', () => {
|
||||
expect(Timer.toSeconds(10000)).toBe(10);
|
||||
expect(Timer.toSeconds(9016)).toBe(10);
|
||||
expect(Timer.toSeconds(8016)).toBe(9);
|
||||
expect(Timer.toSeconds(7010)).toBe(8);
|
||||
expect(Timer.toSeconds(6006)).toBe(7);
|
||||
expect(Timer.toSeconds(4999)).toBe(5);
|
||||
expect(Timer.toSeconds(2995)).toBe(3);
|
||||
expect(Timer.toSeconds(1991)).toBe(2);
|
||||
expect(Timer.toSeconds(992)).toBe(1);
|
||||
expect(Timer.toSeconds(127)).toBe(1);
|
||||
expect(Timer.toSeconds(9016)).toBe(9);
|
||||
expect(Timer.toSeconds(8016)).toBe(8);
|
||||
expect(Timer.toSeconds(7010)).toBe(7);
|
||||
expect(Timer.toSeconds(6006)).toBe(6);
|
||||
expect(Timer.toSeconds(4999)).toBe(4);
|
||||
expect(Timer.toSeconds(2995)).toBe(2);
|
||||
expect(Timer.toSeconds(1991)).toBe(1);
|
||||
expect(Timer.toSeconds(992)).toBe(0);
|
||||
expect(Timer.toSeconds(127)).toBe(0);
|
||||
expect(Timer.toSeconds(0)).toBe(0);
|
||||
expect(Timer.toSeconds(-0)).toBe(-0);
|
||||
expect(Timer.toSeconds(-127)).toBe(-0);
|
||||
@@ -41,4 +41,14 @@ test('convert between mills and seconds correctly', () => {
|
||||
expect(Timer.toSeconds(-7010)).toBe(-7);
|
||||
expect(Timer.toSeconds(-8016)).toBe(-8);
|
||||
expect(Timer.toSeconds(-10000)).toBe(-10);
|
||||
});
|
||||
});
|
||||
|
||||
test('converting between millis to seconds handles partials correctly', () => {
|
||||
const finish = 82162001;
|
||||
const now = 80364519;
|
||||
const runningMs = finish - now;
|
||||
expect(Timer.toSeconds(runningMs)).toBe(1797);
|
||||
|
||||
expect(Timer.toSeconds(1800000)).toBe(1800);
|
||||
expect(Timer.toSeconds(1799761)).toBe(1799);
|
||||
});
|
||||
|
||||
@@ -24,6 +24,7 @@ export class OSCIntegration {
|
||||
time: 'time',
|
||||
overtime: 'overtime',
|
||||
title: 'title',
|
||||
eventNumber: 'eventNumber',
|
||||
presenter: 'presenter',
|
||||
};
|
||||
}
|
||||
@@ -36,6 +37,15 @@ export class OSCIntegration {
|
||||
*/
|
||||
init(oscConfig) {
|
||||
const { ip, port } = oscConfig;
|
||||
const validateType = typeof ip !== 'string' || typeof port !== 'number';
|
||||
const validateNull = ip == null || port == null;
|
||||
|
||||
if (validateType || validateNull) {
|
||||
return {
|
||||
success: false,
|
||||
message: `Config options incorrect`,
|
||||
};
|
||||
}
|
||||
try {
|
||||
this.oscClient = new Client(ip, port);
|
||||
return {
|
||||
@@ -45,7 +55,7 @@ export class OSCIntegration {
|
||||
} catch (error) {
|
||||
this.oscClient = null;
|
||||
return {
|
||||
success: true,
|
||||
success: false,
|
||||
message: `Failed initialising OSC Client: ${error}`,
|
||||
};
|
||||
}
|
||||
@@ -101,9 +111,24 @@ export class OSCIntegration {
|
||||
}
|
||||
break;
|
||||
|
||||
case 'eventNumber':
|
||||
if (payload != null && payload !== '') {
|
||||
// Send event number of current event
|
||||
this.oscClient.send(`${this.ADDRESS}/eventNumber`, payload, (err) => {
|
||||
if (err) {
|
||||
reply.success = false;
|
||||
reply.message = err;
|
||||
}
|
||||
});
|
||||
} else {
|
||||
reply.success = false;
|
||||
reply.message = 'Missing message data';
|
||||
}
|
||||
break;
|
||||
|
||||
case 'presenter':
|
||||
if (payload != null && payload !== '') {
|
||||
// Send presenter data on current event
|
||||
// Send timer data on current event
|
||||
this.oscClient.send(`${this.ADDRESS}/presenter`, payload, (err) => {
|
||||
if (err) {
|
||||
reply.success = false;
|
||||
|
||||
@@ -0,0 +1,162 @@
|
||||
import { OSCIntegration } from '../Osc';
|
||||
import { Server } from 'node-osc';
|
||||
|
||||
test('Class initialises correctly', () => {
|
||||
const osc = new OSCIntegration();
|
||||
expect(osc.ADDRESS).toBe('/ontime');
|
||||
expect(osc.oscClient).toBe(null);
|
||||
|
||||
// defined objects
|
||||
expect(osc.implemented.play).toBeDefined();
|
||||
expect(osc.implemented.pause).toBeDefined();
|
||||
expect(osc.implemented.stop).toBeDefined();
|
||||
expect(osc.implemented.previous).toBeDefined();
|
||||
expect(osc.implemented.next).toBeDefined();
|
||||
expect(osc.implemented.reload).toBeDefined();
|
||||
expect(osc.implemented.finished).toBeDefined();
|
||||
expect(osc.implemented.time).toBeDefined();
|
||||
expect(osc.implemented.overtime).toBeDefined();
|
||||
expect(osc.implemented.title).toBeDefined();
|
||||
expect(osc.implemented.eventNumber).toBeDefined();
|
||||
expect(osc.implemented.presenter).toBeDefined();
|
||||
|
||||
// initialise client succeeds
|
||||
const { ip, port } = { ip: '127.0.0.1', port: 12345 };
|
||||
const init = osc.init({ ip, port });
|
||||
expect(init.message).toBe(`Initialised OSC Client at ${ip}:${port}`);
|
||||
expect(init.success).toBe(true);
|
||||
expect(osc.oscClient).not.toBe(null);
|
||||
|
||||
// object shutdown as expected
|
||||
osc.shutdown();
|
||||
expect(osc.oscClient).toBe(null);
|
||||
});
|
||||
|
||||
describe('OSC fails to initialise when incorrect data is given', () => {
|
||||
test('IP of wrong type', () => {
|
||||
const osc = new OSCIntegration();
|
||||
const init = osc.init({ ip: 123, port: 8888 });
|
||||
expect(init.message).toBe('Config options incorrect');
|
||||
expect(init.success).toBe(false);
|
||||
expect(osc.oscClient).toBe(null);
|
||||
});
|
||||
|
||||
test('IP is null', () => {
|
||||
const osc = new OSCIntegration();
|
||||
const init = osc.init({ ip: null, port: 8888 });
|
||||
expect(init.message).toBe('Config options incorrect');
|
||||
expect(init.success).toBe(false);
|
||||
expect(osc.oscClient).toBe(null);
|
||||
});
|
||||
|
||||
test('Port of wrong type', () => {
|
||||
const osc = new OSCIntegration();
|
||||
const init = osc.init({ ip: 'localhost', port: 'test' });
|
||||
expect(init.message).toBe('Config options incorrect');
|
||||
expect(init.success).toBe(false);
|
||||
expect(osc.oscClient).toBe(null);
|
||||
});
|
||||
|
||||
test('Port is null', () => {
|
||||
const osc = new OSCIntegration();
|
||||
const init = osc.init({ ip: 'localhost', port: null });
|
||||
expect(init.message).toBe('Config options incorrect');
|
||||
expect(init.success).toBe(false);
|
||||
expect(osc.oscClient).toBe(null);
|
||||
});
|
||||
});
|
||||
|
||||
test('Test messages sending', async () => {
|
||||
const testPort = 9999;
|
||||
const testIP = 'localhost';
|
||||
const testPayload = 'test';
|
||||
const osc = new OSCIntegration();
|
||||
|
||||
const messages = [];
|
||||
|
||||
// prepare dummy server to receive messages
|
||||
const oscServer = new Server(testPort, testIP);
|
||||
|
||||
oscServer.on('message', (m) => {
|
||||
messages.push({ yay: m });
|
||||
});
|
||||
|
||||
// try and send a message before initialising
|
||||
const test = await osc.send('test');
|
||||
expect(test.success).toBe(false);
|
||||
expect(test.message).toBe('Client not initialised');
|
||||
|
||||
// initialise osc
|
||||
osc.init({ ip: testIP, port: testPort });
|
||||
|
||||
// try and send unrecognised message
|
||||
const test2 = await osc.send('test');
|
||||
expect(test2.success).toBe(true);
|
||||
|
||||
// send play message
|
||||
const playAddress = osc.implemented.play;
|
||||
const playSent = await osc.send(playAddress);
|
||||
expect(playSent.success).toBe(true);
|
||||
|
||||
// send pause message
|
||||
const pauseAddress = osc.implemented.pause;
|
||||
const pauseSent = await osc.send(pauseAddress);
|
||||
expect(pauseSent.success).toBe(true);
|
||||
|
||||
// send stop message
|
||||
const stopAddress = osc.implemented.stop;
|
||||
const stopSent = await osc.send(stopAddress);
|
||||
expect(stopSent.success).toBe(true);
|
||||
|
||||
// send previous message
|
||||
const previousAddress = osc.implemented.previous;
|
||||
const previousSent = await osc.send(previousAddress);
|
||||
expect(previousSent.success).toBe(true);
|
||||
|
||||
// send next message
|
||||
const nextAddress = osc.implemented.next;
|
||||
const nextSent = await osc.send(nextAddress);
|
||||
expect(nextSent.success).toBe(true);
|
||||
|
||||
// send reload message
|
||||
const reloadAddress = osc.implemented.reload;
|
||||
const reloadSent = await osc.send(reloadAddress);
|
||||
expect(reloadSent.success).toBe(true);
|
||||
|
||||
// send finished message
|
||||
const finishedAddress = osc.implemented.finished;
|
||||
const finishedSent = await osc.send(finishedAddress);
|
||||
expect(finishedSent.success).toBe(true);
|
||||
|
||||
// send time message
|
||||
const timeAddress = osc.implemented.time;
|
||||
const timeSent = await osc.send(timeAddress);
|
||||
expect(timeSent.success).toBe(true);
|
||||
|
||||
// send overtime message
|
||||
const overtimeAddress = osc.implemented.overtime;
|
||||
const overtimeSent = await osc.send(overtimeAddress, testPayload);
|
||||
expect(overtimeSent.success).toBe(true);
|
||||
|
||||
// send title message
|
||||
const titleAddress = osc.implemented.title;
|
||||
const titleSent = await osc.send(titleAddress, testPayload);
|
||||
expect(titleSent.success).toBe(true);
|
||||
|
||||
// send eventNumber message
|
||||
const eventNumberAddress = osc.implemented.eventNumber;
|
||||
const eventNumberSent = await osc.send(eventNumberAddress, testPayload);
|
||||
expect(eventNumberSent.success).toBe(true);
|
||||
|
||||
// send timer message
|
||||
const presenterAddress = osc.implemented.presenter;
|
||||
const presenterSent = await osc.send(presenterAddress, testPayload);
|
||||
expect(presenterSent.success).toBe(true);
|
||||
|
||||
// cleanup
|
||||
await osc.shutdown();
|
||||
await oscServer.close();
|
||||
|
||||
// see messagesObject
|
||||
// expect(messages.length).toBe(5);
|
||||
});
|
||||
@@ -13,7 +13,7 @@ export const config = {
|
||||
port: 8888,
|
||||
portOut: 9999,
|
||||
targetIP: '127.0.0.1',
|
||||
enabled: true,
|
||||
inputEnabled: true,
|
||||
},
|
||||
http: {
|
||||
user: '',
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
// get database
|
||||
import { db, data } from '../app.js';
|
||||
import { data, db } from '../app.js';
|
||||
|
||||
// utils
|
||||
import { generateId } from 'ontime-utils/generate_id.js';
|
||||
import {
|
||||
event as eventDef,
|
||||
delay as delayDef,
|
||||
block as blockDef,
|
||||
delay as delayDef,
|
||||
event as eventDef,
|
||||
} from '../models/eventsDefinition.js';
|
||||
|
||||
async function _insertAt(entry, index) {
|
||||
@@ -73,9 +73,14 @@ export const eventsGetAll = async (req, res) => {
|
||||
// Create controller for GET request to '/events/:eventId'
|
||||
// Returns -
|
||||
export const eventsGetById = async (req, res) => {
|
||||
const e = data.events.find({ id: req.params.eventId }).value();
|
||||
console.log('event by id', e);
|
||||
res.json(e);
|
||||
const id = req.params?.eventId;
|
||||
|
||||
if (id == null) {
|
||||
res.status(400).send(`No eventId found in request`);
|
||||
} else {
|
||||
const event = data.events.find((e) => e.id === id);
|
||||
res.json(event);
|
||||
}
|
||||
};
|
||||
|
||||
// Create controller for POST request to '/events/'
|
||||
@@ -89,23 +94,24 @@ export const eventsPost = async (req, res) => {
|
||||
|
||||
// ensure structure
|
||||
let newEvent = {};
|
||||
req.body.id = generateId();
|
||||
let id = req.body.id;
|
||||
if (data.events.find((e) => e.id === id)) {
|
||||
id = generateId();
|
||||
}
|
||||
|
||||
switch (req.body.type) {
|
||||
case 'event':
|
||||
newEvent = { ...eventDef, ...req.body };
|
||||
newEvent = { ...eventDef, ...req.body, id };
|
||||
break;
|
||||
case 'delay':
|
||||
newEvent = { ...delayDef, ...req.body };
|
||||
newEvent = { ...delayDef, ...req.body, id };
|
||||
break;
|
||||
case 'block':
|
||||
newEvent = { ...blockDef, ...req.body };
|
||||
newEvent = { ...blockDef, ...req.body, id };
|
||||
break;
|
||||
|
||||
default:
|
||||
res
|
||||
.status(400)
|
||||
.send(`Object type missing or unrecognised: ${req.body.type}`);
|
||||
res.status(400).send(`Object type missing or unrecognised: ${req.body.type}`);
|
||||
break;
|
||||
}
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@ import path from 'path';
|
||||
import { fileURLToPath } from 'url';
|
||||
|
||||
// get database
|
||||
import { db, data } from '../app.js';
|
||||
import { data, db } from '../app.js';
|
||||
import { networkInterfaces } from 'os';
|
||||
import { fileHandler } from '../utils/parser.js';
|
||||
import { generateId } from 'ontime-utils/generate_id.js';
|
||||
@@ -15,6 +15,19 @@ function getEventTitle() {
|
||||
return data.event.title;
|
||||
}
|
||||
|
||||
// Create controller for GET request to '/ontime/poll'
|
||||
// Returns data for current state
|
||||
export const poll = async (req, res) => {
|
||||
try {
|
||||
const s = global.timer.poll();
|
||||
res.status(200).send(s);
|
||||
} catch (error) {
|
||||
res.status(500).send({
|
||||
message: `Could not get sync data: ${error}`,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// Create controller for GET request to '/ontime/db'
|
||||
// Returns -
|
||||
export const dbDownload = async (req, res) => {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
// Create controller for GET request to '/playback'
|
||||
// Returns ACK message
|
||||
export const pbGet = async (req, res) => {
|
||||
res.send(global.timer.playState);
|
||||
res.send({ playback: global.timer.state });
|
||||
};
|
||||
|
||||
// Create controller for GET request to '/playback/onAir'
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
"ontime-utils": "link: ../server/utils/",
|
||||
"passport": "~0.4.1",
|
||||
"passport-local": "~1.0.0",
|
||||
"socket.io": "4.4.0",
|
||||
"socket.io": "^4.4.1",
|
||||
"universal-analytics": "^0.4.23"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
import { server, shutdown, startServer } from '../../app.js';
|
||||
import supertest from 'supertest';
|
||||
|
||||
beforeAll(() => startServer());
|
||||
afterAll(() => shutdown());
|
||||
|
||||
describe('When a GET request request is sent', () => {
|
||||
test('GET /event returns a valid object', async () => {
|
||||
await supertest(server)
|
||||
.get('/event')
|
||||
.expect(200)
|
||||
.then((response) => {
|
||||
expect(response.text.includes('<!doctype html>')).toBe(false);
|
||||
expect(response.body).toBeDefined();
|
||||
expect(typeof response.body.title).toBe('string');
|
||||
expect(typeof response.body.url).toBe('string');
|
||||
expect(typeof response.body.publicInfo).toBe('string');
|
||||
expect(typeof response.body.backstageInfo).toBe('string');
|
||||
expect(typeof response.body.endMessage).toBe('string');
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,57 @@
|
||||
import { server, shutdown, startServer } from '../../app.js';
|
||||
import supertest from 'supertest';
|
||||
|
||||
beforeAll(() => startServer());
|
||||
afterAll(() => shutdown());
|
||||
|
||||
const testEvent = {
|
||||
title: 'API test event',
|
||||
subtitle: 'test title',
|
||||
presenter: 'test presenter',
|
||||
note: 'test note',
|
||||
timeStart: 0,
|
||||
timeEnd: 42,
|
||||
isPublic: false,
|
||||
type: 'event',
|
||||
id: 'superSpecial12',
|
||||
};
|
||||
|
||||
describe('When a POST request is sent', () => {
|
||||
test('POST /event should return a 201', async () => {
|
||||
await supertest(server)
|
||||
.post('/events')
|
||||
.send(testEvent)
|
||||
.expect(201)
|
||||
});
|
||||
});
|
||||
|
||||
describe('When a GET request request is sent', () => {
|
||||
test('GET /events returns a valid object', async () => {
|
||||
await supertest(server)
|
||||
.get('/events')
|
||||
.expect(200)
|
||||
.then((response) => {
|
||||
expect(response.text.includes('<!doctype html>')).toBe(false);
|
||||
expect(response.body).toBeDefined();
|
||||
expect(typeof response.body).toBe('object');
|
||||
});
|
||||
});
|
||||
test('GET /events/:eventId returns a valid object', async () => {
|
||||
await supertest(server)
|
||||
.get(`/events/${testEvent.id}`)
|
||||
.expect(200)
|
||||
.then((response) => {
|
||||
expect(response.text.includes('<!doctype html>')).toBe(false);
|
||||
expect(response.body).toBeDefined();
|
||||
expect(response.body.title).toBe(testEvent.title);
|
||||
expect(response.body.subtitle).toBe(testEvent.subtitle);
|
||||
expect(response.body.presenter).toBe(testEvent.presenter);
|
||||
expect(response.body.note).toBe(testEvent.note);
|
||||
expect(response.body.timeStart).toBe(testEvent.timeStart);
|
||||
expect(response.body.timeEnd).toBe(testEvent.timeEnd);
|
||||
expect(response.body.isPublic).toBe(testEvent.isPublic);
|
||||
expect(response.body.type).toBe(testEvent.type);
|
||||
expect(response.body.id).toBe(testEvent.id);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,98 @@
|
||||
import { server, shutdown, startServer } from '../../app.js';
|
||||
import supertest from 'supertest';
|
||||
|
||||
beforeAll(() => startServer());
|
||||
afterAll(() => shutdown());
|
||||
|
||||
describe('When a GET request request is sent', () => {
|
||||
test('GET /ontime/poll returns a valid object', async () => {
|
||||
await supertest(server)
|
||||
.get('/ontime/poll')
|
||||
.expect(200)
|
||||
.then((response) => {
|
||||
expect(response.text.includes('<!doctype html>')).toBe(false);
|
||||
expect(response.body).toBeDefined();
|
||||
expect(typeof response.body.clock).toBe('number');
|
||||
expect(typeof response.body.running).toBe('number');
|
||||
expect(typeof response.body.timer).toBe('string');
|
||||
expect(typeof response.body.playback).toBe('string');
|
||||
expect(typeof response.body.title).toBe('string');
|
||||
expect(typeof response.body.presenter).toBe('string');
|
||||
});
|
||||
});
|
||||
|
||||
test('GET /ontime/db returns a JSON object', async () => {
|
||||
await supertest(server)
|
||||
.get('/ontime/db')
|
||||
.expect(200)
|
||||
.then((response) => {
|
||||
expect(response.text.includes('<!doctype html>')).toBe(false);
|
||||
expect(response.body).toBeDefined();
|
||||
expect(response.headers['content-type']).toContain('json');
|
||||
});
|
||||
});
|
||||
|
||||
test('GET /ontime/info returns a valid object', async () => {
|
||||
await supertest(server)
|
||||
.get('/ontime/info')
|
||||
.expect(200)
|
||||
.then((response) => {
|
||||
expect(response.text.includes('<!doctype html>')).toBe(false);
|
||||
expect(response.body).toBeDefined();
|
||||
expect(typeof response.body.networkInterfaces).toBe('object');
|
||||
expect(typeof response.body.version).toBe('number');
|
||||
expect(typeof response.body.serverPort).toBe('number');
|
||||
expect(typeof response.body.osc).toBe('object');
|
||||
});
|
||||
});
|
||||
|
||||
test('GET /ontime/aliases returns a valid object', async () => {
|
||||
await supertest(server)
|
||||
.get('/ontime/aliases')
|
||||
.expect(200)
|
||||
.then((response) => {
|
||||
expect(response.text.includes('<!doctype html>')).toBe(false);
|
||||
expect(response.body).toBeDefined();
|
||||
expect(typeof response.body).toBe('object');
|
||||
});
|
||||
});
|
||||
|
||||
test('GET /ontime/settings returns a valid object', async () => {
|
||||
await supertest(server)
|
||||
.get('/ontime/settings')
|
||||
.expect(200)
|
||||
.then((response) => {
|
||||
expect(response.text.includes('<!doctype html>')).toBe(false);
|
||||
expect(response.body).toBeDefined();
|
||||
expect(typeof response.body.version).toBe('number');
|
||||
expect(typeof response.body.serverPort).toBe('number');
|
||||
expect(typeof response.body.pinCode).toBeDefined();
|
||||
});
|
||||
});
|
||||
|
||||
test('GET /ontime/osc returns a valid object', async () => {
|
||||
await supertest(server)
|
||||
.get('/ontime/osc')
|
||||
.expect(200)
|
||||
.then((response) => {
|
||||
expect(response.text.includes('<!doctype html>')).toBe(false);
|
||||
expect(response.body).toBeDefined();
|
||||
expect(typeof response.body.port).toBe('number');
|
||||
expect(typeof response.body.portOut).toBe('number');
|
||||
expect(typeof response.body.targetIP).toBe('string');
|
||||
expect(typeof response.body.enabled).toBe('boolean');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('Any other returns the app', () => {
|
||||
test('GET / returns ontime app', async () => {
|
||||
await supertest(server)
|
||||
.get('/')
|
||||
.expect(200)
|
||||
.then((response) => {
|
||||
expect(response.body).toBeDefined();
|
||||
expect(response.text.includes('<!doctype html>')).toBe(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,129 @@
|
||||
import { server, shutdown, startServer } from '../../app.js';
|
||||
import supertest from 'supertest';
|
||||
|
||||
beforeAll(() => startServer());
|
||||
afterAll(() => shutdown());
|
||||
|
||||
describe('When a GET request request is sent', () => {
|
||||
test('GET /playback returns a valid object', async () => {
|
||||
await supertest(server)
|
||||
.get('/playback')
|
||||
.expect(200)
|
||||
.then((response) => {
|
||||
expect(response.text.includes('<!doctype html>')).toBe(false);
|
||||
expect(response.body).toBeDefined();
|
||||
expect(typeof response.body.playback).toBe('string');
|
||||
expect(response.body.playback).toBe('stop' || 'start' || 'pause' || 'roll');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('When a GET state change is sent', () => {
|
||||
test('GET /playback/onAir returns 200', async () => {
|
||||
await supertest(server)
|
||||
.get('/playback/onAir')
|
||||
.expect(200)
|
||||
.then((response) => {
|
||||
expect(response.text.includes('<!doctype html>')).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
test('GET /playback/offAir returns 200', async () => {
|
||||
await supertest(server)
|
||||
.get('/playback/offAir')
|
||||
.expect(200)
|
||||
.then((response) => {
|
||||
expect(response.text.includes('<!doctype html>')).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
test('GET /playback/start returns 200', async () => {
|
||||
await supertest(server)
|
||||
.get('/playback/start')
|
||||
.expect(200)
|
||||
.then((response) => {
|
||||
expect(response.text.includes('<!doctype html>')).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
test('GET /playback/play returns 200', async () => {
|
||||
await supertest(server)
|
||||
.get('/playback/play')
|
||||
.expect(200)
|
||||
.then((response) => {
|
||||
expect(response.text.includes('<!doctype html>')).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
test('GET /playback/pause returns 200', async () => {
|
||||
await supertest(server)
|
||||
.get('/playback/pause')
|
||||
.expect(200)
|
||||
.then((response) => {
|
||||
expect(response.text.includes('<!doctype html>')).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
test('GET /playback/stop returns 200', async () => {
|
||||
await supertest(server)
|
||||
.get('/playback/stop')
|
||||
.expect(200)
|
||||
.then((response) => {
|
||||
expect(response.text.includes('<!doctype html>')).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
test('GET /playback/roll returns 200', async () => {
|
||||
await supertest(server)
|
||||
.get('/playback/roll')
|
||||
.expect(200)
|
||||
.then((response) => {
|
||||
expect(response.text.includes('<!doctype html>')).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
test('GET /playback/previous returns 200', async () => {
|
||||
await supertest(server)
|
||||
.get('/playback/previous')
|
||||
.expect(200)
|
||||
.then((response) => {
|
||||
expect(response.text.includes('<!doctype html>')).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
test('GET /playback/next returns 200', async () => {
|
||||
await supertest(server)
|
||||
.get('/playback/next')
|
||||
.expect(200)
|
||||
.then((response) => {
|
||||
expect(response.text.includes('<!doctype html>')).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
test('GET /playback/unload returns 200', async () => {
|
||||
await supertest(server)
|
||||
.get('/playback/unload')
|
||||
.expect(200)
|
||||
.then((response) => {
|
||||
expect(response.text.includes('<!doctype html>')).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
test('GET /playback/reload returns 200', async () => {
|
||||
await supertest(server)
|
||||
.get('/playback/reload')
|
||||
.expect(200)
|
||||
.then((response) => {
|
||||
expect(response.text.includes('<!doctype html>')).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
test('GET of unknown request returns app', async () => {
|
||||
await supertest(server)
|
||||
.get('/playback/madeup')
|
||||
.expect(200)
|
||||
.then((response) => {
|
||||
expect(response.text.includes('<!doctype html>')).toBe(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -15,8 +15,12 @@ import {
|
||||
postSettings,
|
||||
getAliases,
|
||||
postAliases,
|
||||
poll,
|
||||
} from '../controllers/ontimeController.js';
|
||||
|
||||
// create route between controller and '/ontime/sync' endpoint
|
||||
router.get('/poll', poll);
|
||||
|
||||
// create route between controller and '/ontime/db' endpoint
|
||||
router.get('/db', dbDownload);
|
||||
|
||||
|
||||
@@ -13,7 +13,7 @@ import {
|
||||
pbPrevious,
|
||||
pbNext,
|
||||
pbUnload,
|
||||
pbReload
|
||||
pbReload,
|
||||
} from '../controllers/playbackController.js';
|
||||
|
||||
// create route between controller and '/playback/' endpoint
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import fs from 'fs';
|
||||
import xlsx from 'node-xlsx';
|
||||
import {
|
||||
event as eventDef,
|
||||
delay as delayDef,
|
||||
block as blockDef,
|
||||
delay as delayDef,
|
||||
event as eventDef,
|
||||
} from '../models/eventsDefinition.js';
|
||||
import { dbModelv1 } from '../models/dataModel.js';
|
||||
import { generateId } from 'ontime-utils/generate_id.js';
|
||||
@@ -183,7 +183,6 @@ export const parseExcel_v1 = async (excelData) => {
|
||||
events.push({ ...event, type: 'event' });
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
events,
|
||||
event: eventData,
|
||||
@@ -429,7 +428,7 @@ export const parseOsc_v1 = (data, enforce) => {
|
||||
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;
|
||||
if (s.enabled !== undefined) osc.enabled = s.enabled;
|
||||
|
||||
// write to db
|
||||
newOsc = {
|
||||
|
||||
@@ -1444,10 +1444,10 @@ socket.io-parser@~4.0.4:
|
||||
component-emitter "~1.3.0"
|
||||
debug "~4.3.1"
|
||||
|
||||
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==
|
||||
socket.io@^4.4.1:
|
||||
version "4.4.1"
|
||||
resolved "https://registry.yarnpkg.com/socket.io/-/socket.io-4.4.1.tgz#cd6de29e277a161d176832bb24f64ee045c56ab8"
|
||||
integrity sha512-s04vrBswdQBUmuWJuuNTmXUVJhP0cVky8bBDhdkf8y0Ptsu7fKU2LuLbts9g+pdmAdyMMn8F/9Mf1/wbtUN0fg==
|
||||
dependencies:
|
||||
accepts "~1.3.4"
|
||||
base64id "~2.0.0"
|
||||
|
||||
@@ -57,21 +57,18 @@ describe('test string to millis function', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('test excel date parser', () => {
|
||||
it('parses the given dates correctly', () => {
|
||||
const d0 = '1899-12-30T00:00:00.000Z';
|
||||
const d1 = '1899-12-30T08:00:00.000Z';
|
||||
const d2 = '1899-12-30T08:30:00.000Z';
|
||||
|
||||
const d0Millis = 0;
|
||||
const d1Millis = 28800000;
|
||||
const d2Millis = 30600000;
|
||||
|
||||
expect(excelDateStringToMillis(d0)).toBe(d0Millis);
|
||||
expect(excelDateStringToMillis(d1)).toBe(d1Millis);
|
||||
expect(excelDateStringToMillis(d2)).toBe(d2Millis);
|
||||
describe('test stringFromMillis handles partial secs', () => {
|
||||
it('test with 1795829', () => {
|
||||
const t = { val: 1795829, result: '00:29:55' };
|
||||
expect(stringFromMillis(t.val)).toBe(t.result);
|
||||
});
|
||||
it('test with 1797482', () => {
|
||||
const t = { val: 1797482, result: '00:29:57' };
|
||||
expect(stringFromMillis(t.val)).toBe(t.result);
|
||||
});
|
||||
});
|
||||
|
||||
describe('test excel date parser', () => {
|
||||
it('handles an invalid date string', () => {
|
||||
const s = 'hello';
|
||||
expect(excelDateStringToMillis(s)).toBe(0);
|
||||
|
||||
@@ -57,7 +57,7 @@ export const stringFromMillis = (
|
||||
export const excelDateStringToMillis = (excelDate) => {
|
||||
const date = new Date(excelDate);
|
||||
if (date instanceof Date && !isNaN(date)) {
|
||||
const h = date.getUTCHours();
|
||||
const h = date.getHours();
|
||||
const m = date.getMinutes();
|
||||
const s = date.getSeconds();
|
||||
|
||||
|
||||
+957
-29
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user