Feat/62 logger (#79)

* feat/62-logger request log data in app
* feat/62-logger refact osc integration
* feat/62-logger feedback on osc
* feat/62-logger refact broadcast on triggers
* feat/62-logger log triggers
* feat/62-logger add link to studio
* feat/62-logger replace toasts with logger context
* feat/62-logger style improvements
* feat/62-logger refactor code duplications
* feat/62-logger cleanup and version bump
This commit is contained in:
Carlos Valente
2021-12-25 19:39:40 +01:00
committed by GitHub
parent 160ccabebc
commit 9fc154955a
46 changed files with 3830 additions and 598 deletions
+3 -1
View File
@@ -5,6 +5,8 @@
], ],
"plugins": ["react", "testing-library", "jest"], "plugins": ["react", "testing-library", "jest"],
"rules": { "rules": {
"jest/no-mocks-import": "warn" "jest/no-mocks-import": "warn",
"no-useless-concat": "warn",
"prefer-template": "warn"
} }
} }
+15
View File
@@ -0,0 +1,15 @@
$ontime-accent: #4bffabcc;
$ontime-pink: #ff7597;
$ontime-roll: #2b6cb0;
$notes-color: #d69e2e;
$header-gray: #ccc;
$label-gray: #aaa;
@mixin container-bg {
background-color: rgba(0, 0, 0, 0.13);
border-radius: 2px;
padding: 0 0.5em;
margin: 0 0.5em;
}
+8 -12
View File
@@ -75,28 +75,26 @@ export const ontimeVars = [
]; ];
export const getInfo = async () => { export const getInfo = async () => {
const res = await axios.get(ontimeURL + '/info'); const res = await axios.get(`${ontimeURL}/info`);
return res.data; return res.data;
}; };
export const postInfo = async (data) => { export const postInfo = async (data) => {
const res = await axios.post(ontimeURL + '/info', data); return await axios.post(`${ontimeURL}/info`, data);
return res;
}; };
export const getOSC = async () => { export const getOSC = async () => {
const res = await axios.get(ontimeURL + '/osc'); const res = await axios.get(`${ontimeURL}/osc`);
return res.data; return res.data;
}; };
export const postOSC = async (data) => { export const postOSC = async (data) => {
const res = await axios.post(ontimeURL + '/osc', data); return await axios.post(`${ontimeURL}/osc`, data);
return res;
}; };
export const downloadEvents = async () => { export const downloadEvents = async () => {
await axios({ await axios({
url: ontimeURL + '/db', url: `${ontimeURL}/db`,
method: 'GET', method: 'GET',
responseType: 'blob', // important responseType: 'blob', // important
}).then((response) => { }).then((response) => {
@@ -123,15 +121,13 @@ export const uploadEvents = async (file) => {
const formData = new FormData(); const formData = new FormData();
formData.append('userFile', file); // appending file formData.append('userFile', file); // appending file
await axios await axios
.post(ontimeURL + '/db', formData, { .post(`${ontimeURL}/db`, formData, {
headers: { headers: {
'Content-Type': 'multipart/form-data', 'Content-Type': 'multipart/form-data',
}, },
}) });
.then((res) => console.log(res.data))
.catch((err) => console.error(err));
}; };
export const uploadEventsWithPath = async (filepath) => { export const uploadEventsWithPath = async (filepath) => {
await axios.post(ontimeURL + '/dbpath', { path: filepath }); await axios.post(`${ontimeURL}/dbpath`, { path: filepath });
}; };
+96
View File
@@ -0,0 +1,96 @@
import { useSocket } from './socketContext';
import { createContext, useCallback, useEffect, useState } from 'react';
import { generateId } from 'ontime-server/utils/generate_id';
import { nowInMillis, stringFromMillis } from 'ontime-server/utils/time';
export const LoggingContext = createContext({
logData: [],
emitInfo: () => undefined,
emitWarning: () => undefined,
emitError: () => undefined,
clearLog: () => undefined
});
export const LoggingProvider = (props) => {
const MAX_MESSAGES = 100;
const socket = useSocket();
const [logData, setLogData] = useState([]);
const origin = 'USER';
// handle incoming messages
useEffect(() => {
if (socket == null) return;
// Ask for log data
socket.emit('get-logger');
socket.on('logger', (data) => {
setLogData((l) => [data, ...l]);
});
// Clear listener
return () => {
socket.off('logger');
};
}, [socket]);
/**
* Utility function sends message over socket
* @param text
* @param level
* @private
*/
const _send = useCallback((text, level) => {
if (socket != null) {
const m = {
id: generateId(),
origin,
time: stringFromMillis(nowInMillis()),
level,
text
}
setLogData((l) => [m, ...l]);
socket.emit('logger', m);
}
if (logData.length > MAX_MESSAGES) {
setLogData((l) => l.pop());
}
},[logData, socket]);
/**
* Sends a message with level INFO
* @param text
*/
const emitInfo = useCallback((text) => {
_send(text, 'INFO');
}, [_send]);
/**
* Sends a message with level WARN
* @param text
*/
const emitWarning = useCallback((text) => {
_send(text, 'WARN');
}, [_send]);
/**
* Sends a message with level ERROR
* @param text
*/
const emitError = useCallback((text) => {
_send(text, 'ERROR');
}, [_send]);
/**
* Clears running log
*/
const clearLog = useCallback(() => {
setLogData([])
}, []);
return (
<LoggingContext.Provider value = {{ emitInfo, logData, emitWarning, emitError, clearLog }}>
{props.children}
</LoggingContext.Provider>
)
}
@@ -0,0 +1,24 @@
import PropTypes from "prop-types";
import style from "../../../features/info/Info.module.scss";
import {Icon} from "@chakra-ui/react";
import {FiChevronUp} from "react-icons/fi";
export default function CollapseBar(props) {
const {title = 'Collapse bar', isCollapsed = false, onClick}= props;
return(
<div className={style.header}>
{title}
<Icon
className={isCollapsed ? style.moreCollapsed : style.moreExpanded}
as={FiChevronUp}
onClick={onClick}
/>
</div>
)
}
CollapseBar.propTypes = {
title: PropTypes.string,
isCollapsed: PropTypes.bool,
onClick: PropTypes.func,
}
@@ -0,0 +1,17 @@
.header,
.header__roll {
padding: 0;
margin: 0;
font-size: 0.9em;
color: #ccc;
display: flex;
justify-content: space-between;
}
.header {
color: #ccc;
}
.header__roll {
color: #2b6cb0;
}
@@ -1,6 +1,9 @@
import React from 'react'; import React from 'react';
import { LoggingContext } from '../../../app/context/LoggingContext';
class ErrorBoundary extends React.Component { class ErrorBoundary extends React.Component {
static contextType = LoggingContext;
constructor(props) { constructor(props) {
super(props); super(props);
this.state = { error: null, errorInfo: null }; this.state = { error: null, errorInfo: null };
@@ -17,15 +20,11 @@ class ErrorBoundary extends React.Component {
errorInfo: info, errorInfo: info,
}); });
// TODO: Log the error to an error reporting service // TODO: Log the error to an error reporting service
this.logErrorToServices(error.toString(), info.componentStack); this.context.emitError(error.toString());
} }
// A fake logging service.
logErrorToServices = console.log;
render() { render() {
if (this.state.errorMessage) { if (this.state.errorMessage) {
// You can render any custom fallback UI
return <p>:/</p>; return <p>:/</p>;
} }
return this.props.children; return this.props.children;
@@ -1,23 +1,26 @@
import EditableTimer from 'common/input/EditableTimer'; import EditableTimer from 'common/input/EditableTimer';
import { showWarningToast } from 'common/helpers/toastManager'; import { useContext } from 'react';
import { LoggingContext } from '../../../app/context/LoggingContext';
export default function EventTimes(props) { export default function EventTimes(props) {
const { actionHandler, delay, timeStart, timeEnd } = props; const { actionHandler, delay, timeStart, timeEnd } = props;
const { emitWarning } = useContext(LoggingContext);
const handleValidate = (entry, v) => { const handleValidate = (entry, v) => {
// we dont inforce validation here // we dont enforce validation here
if (v == null || timeStart == null || timeEnd == null) return true; if (v == null || timeStart == null || timeEnd == null) return true;
if (timeStart === 0) return true; if (timeStart === 0) return true;
let validate = { value: true, catch: '' }; let validate = { value: true, catch: '' };
if (entry === 'timeStart' && v > timeEnd) if (entry === 'timeStart' && v > timeEnd) {
validate.catch = 'Start time later than end time'; validate.catch = 'Start time later than end time';
else if (entry === 'timeEnd' && v < timeStart) } else if (entry === 'timeEnd' && v < timeStart) {
validate.catch = 'End time earlier than start time'; validate.catch = 'End time earlier than start time';
}
if (validate.catch !== '') if (validate.catch !== '')
showWarningToast('Time Input Warning', validate.catch); emitWarning(`Time Input Warning: ${validate.catch}`);
return validate.value; return validate.value;
}; };
@@ -1,6 +1,7 @@
import EditableTimer from 'common/input/EditableTimer'; import EditableTimer from 'common/input/EditableTimer';
import { showWarningToast } from 'common/helpers/toastManager'; import { stringFromMillis } from 'ontime-server/utils/time';
import { stringFromMillis } from 'common/utils/dateConfig'; import { useContext } from 'react';
import { LoggingContext } from '../../../app/context/LoggingContext';
const label = { const label = {
fontSize: '0.75em', fontSize: '0.75em',
@@ -83,6 +84,8 @@ const Times = (props) => {
export default function EventTimesVertical(props) { export default function EventTimesVertical(props) {
const { delay, timeStart, timeEnd, duration } = props; const { delay, timeStart, timeEnd, duration } = props;
const { emitWarning } = useContext(LoggingContext);
const handleValidate = (entry, v) => { const handleValidate = (entry, v) => {
// we dont enforce validation here // we dont enforce validation here
@@ -90,17 +93,20 @@ export default function EventTimesVertical(props) {
if (timeStart === 0) return true; if (timeStart === 0) return true;
let validate = { value: true, catch: '' }; let validate = { value: true, catch: '' };
if (entry === 'timeStart' && v > timeEnd) if (entry === 'timeStart' && v > timeEnd) {
validate.catch = 'Start time later than end time'; validate.catch = 'Start time later than end time';
else if (entry === 'timeEnd' && v < timeStart) } else if (entry === 'timeEnd' && v < timeStart) {
validate.catch = 'End time earlier than start time'; validate.catch = 'End time earlier than start time';
}
if (validate.catch !== '') if (validate.catch !== '') {
showWarningToast('Time Input Warning', validate.catch); emitWarning(`Time Input Warning: ${validate.catch}`);
}
return validate.value; return validate.value;
}; };
return (delay != null) & (delay > 0) ? ( return (
(delay != null) && (delay > 0) ? (
<TimesDelayed <TimesDelayed
handleValidate={handleValidate} handleValidate={handleValidate}
actionHandler={props.actionHandler} actionHandler={props.actionHandler}
@@ -117,5 +123,6 @@ export default function EventTimesVertical(props) {
timeEnd={timeEnd} timeEnd={timeEnd}
duration={duration} duration={duration}
/> />
); )
)
} }
@@ -1,4 +1,4 @@
import { stringFromMillis } from 'common/utils/dateConfig'; import { stringFromMillis } from 'ontime-server/utils/time';
import style from './Paginator.module.css'; import style from './Paginator.module.css';
export default function TodayItem(props) { export default function TodayItem(props) {
const { selected, timeStart, timeEnd, title, backstageEvent } = props; const { selected, timeStart, timeEnd, title, backstageEvent } = props;
@@ -19,7 +19,7 @@ export default function TodayItem(props) {
}`} }`}
>{`${start} · ${end}`}</div> >{`${start} · ${end}`}</div>
<div className={style.entryTitle}>{title}</div> <div className={style.entryTitle}>{title}</div>
{backstageEvent && <div className={style.backstageInd}></div>} {backstageEvent && <div className={style.backstageInd}/>}
</div> </div>
); );
} }
@@ -1,28 +0,0 @@
import { createStandaloneToast } from '@chakra-ui/react';
const toast = createStandaloneToast();
// const customToast = createStandaloneToast({ theme: yourCustomTheme })
// error toast
export const showErrorToast = (title, description) => {
toast({
title: title,
description: description,
position: 'top-left',
variant: 'subtle',
status: 'error',
isClosable: true,
});
};
// warning toast
export const showWarningToast = (title, description) => {
toast({
title: title,
description: description,
position: 'top-left',
variant: 'subtle',
status: 'warning',
isClosable: true,
});
};
+6 -5
View File
@@ -1,15 +1,16 @@
import { Editable, EditableInput, EditablePreview } from '@chakra-ui/editable'; import { Editable, EditableInput, EditablePreview } from '@chakra-ui/editable';
import { useEffect, useState } from 'react'; import { useContext, useEffect, useState } from 'react';
import { import {
isTimeString, isTimeString,
stringFromMillis,
timeStringToMillis, timeStringToMillis,
} from '../utils/dateConfig'; } from '../utils/dateConfig';
import { showErrorToast } from '../helpers/toastManager'; import { stringFromMillis } from 'ontime-server/utils/time';
import style from './EditableTimer.module.css'; import style from './EditableTimer.module.css';
import { LoggingContext } from '../../app/context/LoggingContext';
export default function EditableTimer(props) { export default function EditableTimer(props) {
const { name, actionHandler, time, delay, validate } = props; const { name, actionHandler, time, delay, validate } = props;
const { emitError } = useContext(LoggingContext);
const [value, setValue] = useState(''); const [value, setValue] = useState('');
// prepare time fields // prepare time fields
@@ -18,9 +19,9 @@ export default function EditableTimer(props) {
try { try {
setValue(stringFromMillis(time + delay)); setValue(stringFromMillis(time + delay));
} catch (error) { } catch (error) {
showErrorToast('Error parsing date', error.text); emitError(`Unable to parse date: ${error.text}`);
} }
}, [time, delay]); }, [time, delay, emitError]);
const validateValue = (value) => { const validateValue = (value) => {
const success = handleSubmit(value); const success = handleSubmit(value);
-30
View File
@@ -4,37 +4,7 @@ export const timeFormatSeconds = 'HH:mm:ss';
const mts = 1000; // millis to seconds const mts = 1000; // millis to seconds
const mtm = 1000 * 60; // millis to minutes const mtm = 1000 * 60; // millis to minutes
const mth = 1000 * 60 * 60; // millis to hours const mth = 1000 * 60 * 60; // millis to hours
const mtd = 1000 * 60 * 60 * 24; // millis to days
/**
* @description Converts milliseconds to string representing time
* @param {number} ms - time in milliseconds
* @param {boolean} showSeconds - wether to show the seconds
* @param {string} delim - character between HH MM SS
* @param {string} ifNull - what to return if value is null
* @returns {string} String representing time 00:12:02
*/
// This is shared and tested in backend in time.js
export const stringFromMillis = (
ms,
showSeconds = true,
delim = ':',
ifNull = '...'
) => {
if (ms === null || isNaN(ms)) return ifNull;
const isNegative = ms < 0 ? '-' : '';
const showWith0 = (value) => (value < 10 ? `0${value}` : value);
const hours = showWith0(Math.floor(((ms / mth) % 60) % 24));
const minutes = showWith0(Math.floor((ms / mtm) % 60));
const seconds = showWith0(Math.floor((ms / mts) % 60));
return showSeconds
? `${isNegative}${
parseInt(hours) ? `${hours}${delim}` : `00${delim}`
}${minutes}${delim}${seconds}`
: `${isNegative}${parseInt(hours) ? `${hours}` : '00'}${delim}${minutes}`;
};
/** /**
* another go at simpler string formatting (counters) * another go at simpler string formatting (counters)
+2 -2
View File
@@ -1,10 +1,10 @@
import { stringFromMillis } from 'ontime-server/utils/time';
/** /**
* @description From a list of events, returns only events of type event with calculated delays * @description From a list of events, returns only events of type event with calculated delays
* @param {Object[]} events - given events * @param {Object[]} events - given events
* @returns {Object[]} Filtered events with calculated delays * @returns {Object[]} Filtered events with calculated delays
*/ */
import {stringFromMillis} from "./dateConfig";
export const getEventsWithDelay = (events) => { export const getEventsWithDelay = (events) => {
if (events == null) return []; if (events == null) return [];
@@ -1,6 +1,6 @@
import style from './PlaybackControl.module.scss'; import style from './PlaybackControl.module.scss';
import Countdown from 'common/components/countdown/Countdown'; import Countdown from 'common/components/countdown/Countdown';
import {stringFromMillis} from 'common/utils/dateConfig'; import { stringFromMillis } from 'ontime-server/utils/time';
import {Tooltip} from '@chakra-ui/react'; import {Tooltip} from '@chakra-ui/react';
import {Button} from '@chakra-ui/button'; import {Button} from '@chakra-ui/button';
import {memo} from 'react'; import {memo} from 'react';
+4 -3
View File
@@ -1,10 +1,11 @@
import { lazy, useEffect } from 'react'; import { lazy, useEffect } from 'react';
import { Box } from '@chakra-ui/layout'; import { Box } from '@chakra-ui/layout';
import { useDisclosure } from '@chakra-ui/hooks'; import { useDisclosure } from '@chakra-ui/hooks';
import styles from './Editor.module.css'; import styles from './Editor.module.scss';
import MenuBar from 'features/menu/MenuBar'; import MenuBar from 'features/menu/MenuBar';
import ModalManager from 'features/modals/ModalManager'; import ModalManager from 'features/modals/ModalManager';
import ErrorBoundary from 'common/components/errorBoundary/ErrorBoundary'; import ErrorBoundary from 'common/components/errorBoundary/ErrorBoundary';
import { LoggingProvider } from '../../app/context/LoggingContext';
const EventListWrapper = lazy(() => const EventListWrapper = lazy(() =>
import('features/editors/list/EventListWrapper') import('features/editors/list/EventListWrapper')
@@ -22,7 +23,7 @@ export default function Editor() {
}, []); }, []);
return ( return (
<> <LoggingProvider>
<ModalManager isOpen={isOpen} onClose={onClose} /> <ModalManager isOpen={isOpen} onClose={onClose} />
<div className={styles.mainContainer}> <div className={styles.mainContainer}>
@@ -68,6 +69,6 @@ export default function Editor() {
</div> </div>
</Box> </Box>
</div> </div>
</> </LoggingProvider>
); );
} }
@@ -8,7 +8,7 @@
display: grid; display: grid;
grid-template-rows: auto 1fr; grid-template-rows: auto 1fr;
grid-template-columns: 40px 48em auto auto; grid-template-columns: 40px 48em 31em auto;
grid-template-areas: grid-template-areas:
'sett even play info' 'sett even play info'
'sett even mess info'; 'sett even mess info';
@@ -110,12 +110,23 @@ h1 {
.editor { .editor {
grid-area: even; grid-area: even;
.content {
height: calc(100% - 3em);
overflow: hidden;
}
} }
.info { .info {
grid-area: info; grid-area: info;
min-width: 17em; min-width: 17em;
max-width: 32em;
.content {
display: flex;
flex-direction: column;
height: calc(100% - 3em);
overflow: hidden;
}
} }
.messages { .messages {
@@ -1,4 +1,4 @@
import style from './List.module.css'; import style from './List.module.scss';
import { createRef, useCallback, useEffect, useMemo, useState } from 'react'; import { createRef, useCallback, useEffect, useMemo, useState } from 'react';
import { useSocket } from 'app/context/socketContext'; import { useSocket } from 'app/context/socketContext';
import Empty from 'common/state/Empty'; import Empty from 'common/state/Empty';
@@ -1,8 +1,8 @@
import DelayBlock from './DelayBlock'; import DelayBlock from './DelayBlock';
import BlockBlock from './BlockBlock'; import BlockBlock from './BlockBlock';
import EventBlock from './EventBlock'; import EventBlock from './EventBlock';
import { showErrorToast } from 'common/helpers/toastManager'; import { memo, useContext } from 'react';
import { memo } from 'react'; import { LoggingContext } from '../../../app/context/LoggingContext';
const areEqual = (prevProps, nextProps) => { const areEqual = (prevProps, nextProps) => {
return ( return (
@@ -26,6 +26,7 @@ const EventListItem = (props) => {
delay, delay,
...rest ...rest
} = props; } = props;
const { emitError } = useContext(LoggingContext);
// Create / delete new events // Create / delete new events
const actionHandler = (action, payload) => { const actionHandler = (action, payload) => {
@@ -59,7 +60,7 @@ const EventListItem = (props) => {
// request update in parent // request update in parent
eventsHandler('patch', newData); eventsHandler('patch', newData);
} else { } else {
showErrorToast('Field Error: ' + field); emitError(`Unknown field: ${field}`);
} }
break; break;
default: default:
@@ -1,5 +1,5 @@
import { useMutation, useQueryClient } from 'react-query'; import { useMutation, useQueryClient } from 'react-query';
import { useCallback, useEffect, useState } from 'react'; import { useCallback, useContext, useEffect, useState } from 'react';
import { import {
fetchAllEvents, fetchAllEvents,
requestPatch, requestPatch,
@@ -12,16 +12,17 @@ import {
} from 'app/api/eventsApi.js'; } from 'app/api/eventsApi.js';
import EventList from './EventList'; import EventList from './EventList';
import EventListMenu from 'features/menu/EventListMenu.jsx'; import EventListMenu from 'features/menu/EventListMenu.jsx';
import { showErrorToast } from 'common/helpers/toastManager';
import { useFetch } from 'app/hooks/useFetch.js'; import { useFetch } from 'app/hooks/useFetch.js';
import Empty from 'common/state/Empty'; import Empty from 'common/state/Empty';
import { EVENTS_TABLE } from 'app/api/apiConstants'; import { EVENTS_TABLE } from 'app/api/apiConstants';
import { BatchOperation } from 'app/context/collapseAtom'; import { BatchOperation } from 'app/context/collapseAtom';
import { useAtom } from 'jotai'; import { useAtom } from 'jotai';
import { LoggingContext } from '../../../app/context/LoggingContext';
export default function EventListWrapper() { export default function EventListWrapper() {
const [, setCollapsed] = useAtom(BatchOperation); const [, setCollapsed] = useAtom(BatchOperation);
const queryClient = useQueryClient(); const queryClient = useQueryClient();
const { emitError } = useContext(LoggingContext);
const { data, status, isError, refetch } = useFetch( const { data, status, isError, refetch } = useFetch(
EVENTS_TABLE, EVENTS_TABLE,
fetchAllEvents fetchAllEvents
@@ -230,9 +231,9 @@ export default function EventListWrapper() {
// Show toasts on errors // Show toasts on errors
useEffect(() => { useEffect(() => {
if (isError) { if (isError) {
showErrorToast('Error fetching data'); emitError('Error fetching data');
} }
}, [isError]); }, [emitError, isError]);
// Events API // Events API
const eventsHandler = useCallback( const eventsHandler = useCallback(
@@ -242,35 +243,35 @@ export default function EventListWrapper() {
try { try {
await addEvent.mutateAsync(payload); await addEvent.mutateAsync(payload);
} catch (error) { } catch (error) {
showErrorToast('Error creating event', error.message); emitError(`Error fetching data: ${error.message}`);
} }
break; break;
case 'update': case 'update':
try { try {
await updateEvent.mutateAsync(payload); await updateEvent.mutateAsync(payload);
} catch (error) { } catch (error) {
showErrorToast('Error updating event', error.message); emitError(`Error updating event: ${error.message}`);
} }
break; break;
case 'patch': case 'patch':
try { try {
await patchEvent.mutateAsync(payload); await patchEvent.mutateAsync(payload);
} catch (error) { } catch (error) {
showErrorToast('Error updating event', error.message); emitError(`Error updating event: ${error.message}`);
} }
break; break;
case 'delete': case 'delete':
try { try {
await deleteEvent.mutateAsync(payload); await deleteEvent.mutateAsync(payload);
} catch (error) { } catch (error) {
showErrorToast('Error deleting event', error.message); emitError(`Error deleting event: ${error.message}`);
} }
break; break;
case 'reorder': case 'reorder':
try { try {
await reorderEvent.mutateAsync(payload); await reorderEvent.mutateAsync(payload);
} catch (error) { } catch (error) {
showErrorToast('Error reordering event', error.message); emitError(`Error re-ordering event: ${error.message}`);
} }
break; break;
case 'applyDelay': case 'applyDelay':
@@ -293,13 +294,13 @@ export default function EventListWrapper() {
// delete block after, if any // delete block after, if any
if (blockAfter) await deleteEvent.mutateAsync(blockAfter); if (blockAfter) await deleteEvent.mutateAsync(blockAfter);
} catch (error) { } catch (error) {
showErrorToast('Error applying delay', error.message); emitError(`Error applying delay: ${error.message}`);
} }
} else { } else {
try { try {
await applyDelay.mutateAsync(payload.id); await applyDelay.mutateAsync(payload.id);
} catch (error) { } catch (error) {
showErrorToast('Error applying delay', error.message); emitError(`Error applying delay: ${error.message}`);
} }
} }
break; break;
@@ -317,11 +318,11 @@ export default function EventListWrapper() {
try { try {
await deleteAllEvents.mutateAsync(); await deleteAllEvents.mutateAsync();
} catch (error) { } catch (error) {
showErrorToast('Error deleting events', error.message); emitError(`Error deleting events: ${error.message}`);
} }
break; break;
default: default:
showErrorToast('Unrecognised request', action); emitError(`Unhandled request: ${action}`);
break; break;
} }
}, },
@@ -7,7 +7,7 @@
border-radius: 4px; border-radius: 4px;
padding: 8px; padding: 8px;
overflow-y: scroll; overflow-y: scroll;
height: 73vh; height: 100%;
} }
.list { .list {
+4 -6
View File
@@ -15,11 +15,10 @@ export default function Info() {
titleNext: '', titleNext: '',
subtitleNext: '', subtitleNext: '',
presenterNext: '', presenterNext: '',
noteNext: '', noteNext: ''
}); });
const [selected, setSelected] = useState('No events'); const [selected, setSelected] = useState('No events');
const [playback, setPlayback] = useState(null); const [playback, setPlayback] = useState(null);
const logData = [];
// handle incoming messages // handle incoming messages
useEffect(() => { useEffect(() => {
@@ -61,20 +60,19 @@ export default function Info() {
}; };
}, [socket]); }, [socket]);
// TODO: Put this in use effect
// prepare data // prepare data
const titlesNow = { const titlesNow = {
title: titles.titleNow, title: titles.titleNow,
subtitle: titles.subtitleNow, subtitle: titles.subtitleNow,
presenter: titles.presenterNow, presenter: titles.presenterNow,
note: titles.noteNow, note: titles.noteNow
}; };
const titlesNext = { const titlesNext = {
title: titles.titleNext, title: titles.titleNext,
subtitle: titles.subtitleNext, subtitle: titles.subtitleNext,
presenter: titles.presenterNext, presenter: titles.presenterNext,
note: titles.noteNext, note: titles.noteNext
}; };
return ( return (
@@ -83,10 +81,10 @@ export default function Info() {
<span>{`Running on port 4001`}</span> <span>{`Running on port 4001`}</span>
<span>{selected}</span> <span>{selected}</span>
</div> </div>
{/* <InfoLogger logData={logData} /> */}
<InfoNif /> <InfoNif />
<InfoTitle title={'Now'} data={titlesNow} roll={playback === 'roll'} /> <InfoTitle title={'Now'} data={titlesNow} roll={playback === 'roll'} />
<InfoTitle title={'Next'} data={titlesNext} roll={playback === 'roll'} /> <InfoTitle title={'Next'} data={titlesNext} roll={playback === 'roll'} />
<InfoLogger />
</> </>
); );
} }
+15 -30
View File
@@ -1,4 +1,6 @@
.container { @use '../../main' as *;
@mixin container {
margin-top: 1em; margin-top: 1em;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
@@ -8,9 +10,13 @@
padding: 8px; padding: 8px;
} }
.container {
@include container;
}
.main { .main {
font-size: 0.9em; font-size: 0.9em;
color: #ff7597; color: $ontime-pink;
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
} }
@@ -20,17 +26,17 @@
padding: 0; padding: 0;
margin: 0; margin: 0;
font-size: 0.9em; font-size: 0.9em;
color: #ccc; color: $header-gray;
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
} }
.header { .header {
color: #ccc; color: $header-gray;
} }
.headerRoll { .headerRoll {
color: #2b6cb0; color: $ontime-roll;
} }
.collapsedTitle { .collapsedTitle {
@@ -57,7 +63,7 @@
.label { .label {
font-size: 0.9em; font-size: 0.9em;
color: #aaa; color: $label-gray;
} }
.label::after { .label::after {
@@ -70,43 +76,22 @@
} }
.notes { .notes {
color: #d69e2e; color: $notes-color;
overflow: hidden; overflow: hidden;
text-overflow: ellipsis; text-overflow: ellipsis;
} }
.if { .if {
font-size: 0.8em; font-size: 0.8em;
color: #4bffabcc; color: $ontime-accent;
background-color: rgba(0, 0, 0, 0.13); @include container-bg;
border-radius: 2px;
padding: 0 0.5em;
margin: 0 0.5em;
} }
.log {
overflow-y: scroll;
height: 30vh;
ul > li { ul > li {
font-size: 0.9em; font-size: 0.9em;
color: #fff; color: #fff;
} }
}
.info {
color: #fff;
}
.error {
color: red;
}
.client {
color: lightblue;
}
.moreExpanded, .moreExpanded,
.moreCollapsed { .moreCollapsed {
cursor: pointer; cursor: pointer;
+105 -22
View File
@@ -1,34 +1,117 @@
import { Icon } from '@chakra-ui/react'; import { useContext, useEffect, useState } from 'react';
import { useState } from 'react'; import style from './InfoLogger.module.scss';
import { FiChevronUp } from 'react-icons/fi'; import CollapseBar from "../../common/components/collapseBar/CollapseBar";
import style from './Info.module.scss'; import { LoggingContext } from '../../app/context/LoggingContext';
export default function InfoLogger(props) { export default function InfoLogger() {
const { logData, clearLog } = useContext(LoggingContext);
const [data, setData] = useState([]);
const [collapsed, setCollapsed] = useState(false); const [collapsed, setCollapsed] = useState(false);
// Todo: save in local storage
const [showClient, setShowClient] = useState(true);
const [showServer, setShowServer] = useState(true);
const [showRx, setShowRx] = useState(true);
const [showTx, setShowTx] = useState(true);
const [showPlayback, setShowPlayback] = useState(true);
const [showUser, setShowUser] = useState(true);
const { logData } = props; useEffect(() => {
const matchers = [];
if (showUser) {
matchers.push('USER');
}
if (showClient) {
matchers.push('CLIENT');
}
if (showServer) {
matchers.push('SERVER');
}
if (showRx) {
matchers.push('RX');
}
if (showTx) {
matchers.push('TX');
}
if (showPlayback) {
matchers.push('PLAYBACK');
}
const d = logData.filter((d) => (
matchers.some((m) => d.origin === m)
))
setData(d);
},[logData, showUser, showClient, showServer, showPlayback, showRx, showTx])
const disableOthers = (toEnable) => {
toEnable === 'USER' ? setShowUser(true) : setShowUser(false);
toEnable === 'CLIENT' ? setShowClient(true) : setShowClient(false);
toEnable === 'SERVER' ? setShowServer(true) : setShowServer(false);
toEnable === 'RX' ? setShowRx(true) : setShowRx(false);
toEnable === 'TX' ? setShowTx(true) : setShowTx(false);
toEnable === 'PLAYBACK' ? setShowPlayback(true) : setShowPlayback(false);
}
return ( return (
<div className={style.container}> <div className={collapsed ? style.container : style.container__expanded}>
<div className={style.header}> <CollapseBar title={'Log'} isCollapsed={collapsed} onClick={() => setCollapsed((c) => !c)}/>
Log
<Icon
className={collapsed ? style.moreCollapsed : style.moreExpanded}
as={FiChevronUp}
onClick={() => setCollapsed((c) => !c)}
/>
</div>
{!collapsed && ( {!collapsed && (
<>
<div className={style.toggleBar}>
<div
onClick={() => setShowUser((s) => !s)}
onAuxClick={() => disableOthers('USER')}
className={(showUser) ? style.active : null}>
USER
</div>
<div
onClick={() => setShowClient((s) => !s)}
onAuxClick={() => disableOthers('CLIENT')}
className={(showClient) ? style.active : null}>
CLIENT
</div>
<div
onClick={() => setShowServer((s) => !s)}
onAuxClick={() => disableOthers('SERVER')}
className={(showServer) ? style.active : null}>
SERVER
</div>
<div
onClick={() => setShowPlayback((s) => !s)}
onAuxClick={() => disableOthers('PLAYBACK')}
className={(showPlayback) ? style.active : null}>
Playback
</div>
<div
onClick={() => setShowRx((s) => !s)}
onAuxClick={() => disableOthers('RX')}
className={(showRx) ? style.active : null}>
RX
</div>
<div
onClick={() => setShowTx((s) => !s)}
onAuxClick={() => disableOthers('TX')}
className={(showTx) ? style.active : null}>
TX
</div>
<div
onClick={clearLog}
className={style.clear}>
Clear
</div>
</div>
<ul className={style.log}> <ul className={style.log}>
<li className={style.info}>10:35:23 [PLAYBACK] Next</li> {data.map((d) => (
<li className={style.client}> <li key={d.id} className={d.level === 'INFO' ? style.info : d.level === 'WARN' ? style.warn : d.level === 'ERROR' ? style.error : ''}>
10:32:10 [CLIENT] New socket client (total: 3) <div
className={style.time}
>{d.time}</div>
<div className={style.origin}>{d.origin}</div>
<div className={style.msg}>{d.text}</div>
</li> </li>
<li className={style.info}>10:28:23 [PLAYBACK] Next</li> ))}
<li className={style.info}>10:25:23 [PLAYBACK] Play</li>
<li className={style.info}>10:23:13 [SERVER] Server Reconnected</li>
<li className={style.error}>10:23:10 [SERVER] Server Disconnected</li>
</ul> </ul>
</>
)} )}
</div> </div>
); );
@@ -0,0 +1,89 @@
@use 'Info.module' as *;
@use '../../main' as *;
.container,
.container__expanded{
@include container;
max-height: 80%;
}
.container__expanded {
min-height: 50%;
height: 100%
}
.log {
height: 100%;
overflow-y: scroll;
font-size: 0.8em;
user-select:text;
@include container-bg;
li {
display: flex;
margin-bottom: 2px;
.time {
width: 13%;
}
.origin {
width: 18%;
}
.msg {
width: 70%;
}
}
li.info {
color: #aaa;
}
li.warn {
color: #dd6b20;
}
li.error {
color: #f00;
}
.entry:hover {
color: #ddd;
}
}
.info {
color: #fff;
}
.error {
color: red;
}
.client {
color: lightblue;
}
.toggleBar {
display: flex;
font-size: 0.7em;
justify-content: flex-start;
gap: 1em;
padding: 0.5em 0;
font-weight: 600;
div {
padding: 2px 8px;
background: #0002;
border: 1px solid #fff1;
border-radius: 2px;
cursor: pointer;
}
div.active {
background: $ontime-accent;
color: darken($ontime-accent, 70%);
}
.clear {
border: 1px solid rgba($ontime-pink, 0.5);
}
}
+2 -11
View File
@@ -1,10 +1,9 @@
import { Icon } from '@chakra-ui/react';
import { useState } from 'react'; import { useState } from 'react';
import { FiChevronUp } from 'react-icons/fi';
import { APP_TABLE } from 'app/api/apiConstants'; import { APP_TABLE } from 'app/api/apiConstants';
import { getInfo, ontimePlaceholderInfo } from 'app/api/ontimeApi'; import { getInfo, ontimePlaceholderInfo } from 'app/api/ontimeApi';
import { useFetch } from 'app/hooks/useFetch'; import { useFetch } from 'app/hooks/useFetch';
import style from './Info.module.scss'; import style from './Info.module.scss';
import CollapseBar from '../../common/components/collapseBar/CollapseBar';
export default function InfoNif() { export default function InfoNif() {
const { data, status } = useFetch(APP_TABLE, getInfo, { const { data, status } = useFetch(APP_TABLE, getInfo, {
@@ -23,15 +22,7 @@ export default function InfoNif() {
return ( return (
<div className={style.container}> <div className={style.container}>
<div className={style.header}> <CollapseBar title={'Network Info'} isCollapsed={collapsed} onClick={() => setCollapsed((c) => !c)}/>
Network Info
<Icon
className={collapsed ? style.moreCollapsed : style.moreExpanded}
as={FiChevronUp}
onClick={() => setCollapsed((c) => !c)}
/>
</div>
{!collapsed && ( {!collapsed && (
<div> <div>
{status === 'success' && ( {status === 'success' && (
+8 -10
View File
@@ -9,10 +9,12 @@ import QuitIconBtn from './buttons/QuitIconBtn';
import style from './MenuBar.module.css'; import style from './MenuBar.module.css';
import HelpIconBtn from './buttons/HelpIconBtn'; import HelpIconBtn from './buttons/HelpIconBtn';
import UploadIconBtn from './buttons/UploadIconBtn'; import UploadIconBtn from './buttons/UploadIconBtn';
import { useRef } from 'react'; import { useContext, useRef } from 'react';
import { LoggingContext } from '../../app/context/LoggingContext';
export default function MenuBar(props) { export default function MenuBar(props) {
const { onOpen } = props; const { onOpen } = props;
const { emitError } = useContext(LoggingContext);
const hiddenFileInput = useRef(null); const hiddenFileInput = useRef(null);
const queryClient = useQueryClient(); const queryClient = useQueryClient();
const uploaddb = useMutation(uploadEvents, { const uploaddb = useMutation(uploadEvents, {
@@ -34,28 +36,24 @@ export default function MenuBar(props) {
const handleUpload = (event) => { const handleUpload = (event) => {
const fileUploaded = event.target.files[0]; const fileUploaded = event.target.files[0];
if (fileUploaded == null) return; if (fileUploaded == null) return;
console.log(fileUploaded);
// Limit file size to 1MB // Limit file size to 1MB
if (fileUploaded.size > 1000000) { if (fileUploaded.size > 1000000) {
console.log('Error: File size limit (1MB) exceeded'); emitError('Error: File size limit (1MB) exceeded')
return; return;
} }
// Check file extension // Check file extension
if (fileUploaded.name.endsWith('.xlsx')) { if (! fileUploaded.name.endsWith('.xlsx')
console.log('excel file'); || !fileUploaded.name.endsWith('.json')) {
} else if (fileUploaded.name.endsWith('.json')) { emitError('Error: File type unknown')
console.log('json file');
} else {
console.log('Error: File type unknown');
return; return;
} }
try { try {
uploaddb.mutate(fileUploaded); uploaddb.mutate(fileUploaded);
} catch (error) { } catch (error) {
console.log(error); emitError(`Failed uploading file: ${error}`)
} }
// reset input value // reset input value
@@ -23,6 +23,7 @@ export default function AliasesModal() {
const smLink = 'http://localhost:4001/sm'; const smLink = 'http://localhost:4001/sm';
const publicLink = 'http://localhost:4001/public'; const publicLink = 'http://localhost:4001/public';
const pipLink = 'http://localhost:4001/pip'; const pipLink = 'http://localhost:4001/pip';
const studioLink = 'http://localhost:4001/studio';
return ( return (
<> <>
@@ -81,6 +82,17 @@ export default function AliasesModal() {
{pipLink} {pipLink}
</a> </a>
</p> </p>
<p className={style.flexNote}>
Studio Clock<br />
<a
href={studioLink}
target='_blank'
rel='noreferrer'
className={style.label}
>
{studioLink}
</a>
</p>
</div> </div>
<span>Manage custom aliases</span> <span>Manage custom aliases</span>
@@ -1,14 +1,15 @@
import { ModalBody } from '@chakra-ui/modal'; import { ModalBody } from '@chakra-ui/modal';
import { FormLabel, FormControl, Input, Button } from '@chakra-ui/react'; import { FormLabel, FormControl, Input, Button } from '@chakra-ui/react';
import { getOSC, oscPlaceholderSettings, postOSC } from 'app/api/ontimeApi'; import { getOSC, oscPlaceholderSettings, postOSC } from 'app/api/ontimeApi';
import { useEffect, useState } from 'react'; import { useContext, useEffect, useState } from 'react';
import { useFetch } from 'app/hooks/useFetch'; import { useFetch } from 'app/hooks/useFetch';
import { OSC_SETTINGS } from 'app/api/apiConstants'; import { OSC_SETTINGS } from 'app/api/apiConstants';
import { showErrorToast } from 'common/helpers/toastManager';
import style from './Modals.module.scss'; import style from './Modals.module.scss';
import { LoggingContext } from '../../app/context/LoggingContext';
export default function AppSettingsModal() { export default function AppSettingsModal() {
const { data, status } = useFetch(OSC_SETTINGS, getOSC); const { data, status } = useFetch(OSC_SETTINGS, getOSC);
const { emitError } = useContext(LoggingContext);
const [formData, setFormData] = useState(oscPlaceholderSettings); const [formData, setFormData] = useState(oscPlaceholderSettings);
const [changed, setChanged] = useState(false); const [changed, setChanged] = useState(false);
const [submitting, setSubmitting] = useState(false); const [submitting, setSubmitting] = useState(false);
@@ -41,7 +42,7 @@ export default function AppSettingsModal() {
// set fields with error // set fields with error
if (e.status) { if (e.status) {
showErrorToast('Invalid Input', e.message); emitError(`Invalid Input: ${e.message}`);
return; return;
} }
@@ -12,14 +12,15 @@ import {
ontimeVars, ontimeVars,
postInfo, postInfo,
} from 'app/api/ontimeApi'; } from 'app/api/ontimeApi';
import { useEffect, useState } from 'react'; import { useContext, useEffect, useState } from 'react';
import { useFetch } from 'app/hooks/useFetch'; import { useFetch } from 'app/hooks/useFetch';
import { APP_TABLE } from 'app/api/apiConstants'; import { APP_TABLE } from 'app/api/apiConstants';
import { showErrorToast } from 'common/helpers/toastManager';
import style from './Modals.module.scss'; import style from './Modals.module.scss';
import { LoggingContext } from '../../app/context/LoggingContext';
export default function IntegrationSettingsModal() { export default function IntegrationSettingsModal() {
const { data, status } = useFetch(APP_TABLE, getInfo); const { data, status } = useFetch(APP_TABLE, getInfo);
const { emitError } = useContext(LoggingContext);
const [formData, setFormData] = useState(httpPlaceholder); const [formData, setFormData] = useState(httpPlaceholder);
const [changed, setChanged] = useState(false); const [changed, setChanged] = useState(false);
const [submitting, setSubmitting] = useState(false); const [submitting, setSubmitting] = useState(false);
@@ -51,7 +52,7 @@ export default function IntegrationSettingsModal() {
// set fields with error // set fields with error
if (e.status) { if (e.status) {
showErrorToast('Invalid Input', e.message); emitError(`Invalid Input: ${e.message}`);
return; return;
} }
+1 -1
View File
@@ -2,7 +2,7 @@ import { useEffect, useState } from 'react';
import { fetchAllEvents } from 'app/api/eventsApi'; import { fetchAllEvents } from 'app/api/eventsApi';
import { fetchEvent } from 'app/api/eventApi'; import { fetchEvent } from 'app/api/eventApi';
import { useSocket } from 'app/context/socketContext'; import { useSocket } from 'app/context/socketContext';
import { stringFromMillis } from 'common/utils/dateConfig'; import { stringFromMillis } from 'ontime-server/utils/time';
import { useFetch } from 'app/hooks/useFetch'; import { useFetch } from 'app/hooks/useFetch';
import { EVENTS_TABLE, EVENT_TABLE } from 'app/api/apiConstants'; import { EVENTS_TABLE, EVENT_TABLE } from 'app/api/apiConstants';
+1 -1
View File
@@ -119,7 +119,7 @@ app.whenReady().then(() => {
createWindow(); createWindow();
// register global shortcuts // register global shortcuts
// (available regardless of wheter app is in focus) // (available regardless of whether app is in focus)
// bring focus to window // bring focus to window
globalShortcut.register('Alt+1', () => { globalShortcut.register('Alt+1', () => {
win.show(); win.show();
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "ontime", "name": "ontime",
"version": "0.4.7", "version": "0.4.8",
"author": "Carlos Valente", "author": "Carlos Valente",
"description": "Time keeping for live events", "description": "Time keeping for live events",
"repository": "https://github.com/cpvalente/ontime", "repository": "https://github.com/cpvalente/ontime",
+1 -1
View File
@@ -153,7 +153,7 @@ export const startServer = async (overrideConfig = null) => {
const port = 4001; const port = 4001;
// Start server // Start server
const returnMessage = `HTTP Server is listening on port ${port}`; 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', () => console.log(returnMessage));
// OSC Config // OSC Config
+302 -240
View File
@@ -1,110 +1,85 @@
import { Timer } from './Timer.js'; import { Timer } from './Timer.js';
import { Server } from 'socket.io'; 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 { OSCIntegration } from './integrations/Osc.js';
import {HTTPIntegration} from "./integrations/Http.js"; import { HTTPIntegration } from './integrations/Http.js';
import {cleanURL} from "../utils/url.js"; import { cleanURL } from '../utils/url.js';
import getRandomName from '../utils/getRandomName.js';
import { stringFromMillis } from '../utils/time.js';
import { generateId } from '../utils/generate_id.js';
/* /*
* EventTimer adds functions specific to APP * Class EventTimer adds functions specific to APP
* namely: * @extends Timer
* - Presenter message, text and status
* - Public message, text and status
*
*/ */
export class EventTimer extends Timer { export class EventTimer extends Timer {
/**
* Instantiates an event timer object
* @param {object} httpServer
* @param {object} timerConfig
* @param {object} [oscConfig]
* @param {object} [httpConfig]
*/
constructor(httpServer, timerConfig, oscConfig, httpConfig) {
// call super constructor
super();
// Keep track of Timer lifecycle this.cycleState = {
// idle: before it is initialised /* idle: before it is initialised */
// load: when a new event is loaded
// update: every update call cycle (1 x second)
// stop: when the timer is stopped
// finish: when a timer finishes
cycleState = {
idle: 'idle', idle: 'idle',
/* onLoad: when a new event is loaded */
onLoad: 'onLoad', onLoad: 'onLoad',
/* armed: when a new event is loaded but hasn't started */
armed: 'armed', armed: 'armed',
onStart: 'onStart', onStart: 'onStart',
/* update: every update call cycle (1 x second) */
onUpdate: 'onUpdate', onUpdate: 'onUpdate',
onPause: 'onPause', onPause: 'onPause',
onStop: 'onStop', onStop: 'onStop',
onFinish: 'onFinish', onFinish: 'onFinish',
}; };
ontimeCycle = 'idle'; this.ontimeCycle = 'idle';
prevCycle = null; this.prevCycle = null;
lastUpdate = null;
// Socket IO Object
io = null;
// OSC Object // OSC Object
osc = null; this.osc = null;
// HTTP Client Object // HTTP Client Object
http = null; this.http = null;
_numClients = 0; this._numClients = 0;
_interval = null; this._interval = null;
presenter = { this.presenter = {
text: '', text: '',
visible: false, visible: false,
}; };
public = { this.public = {
text: '', text: '',
visible: false, visible: false,
}; };
lower = { this.lower = {
text: '', text: '',
visible: false, visible: false,
}; };
titlesPublic = { // call general title reset
titleNow: null, this._resetSelection();
subtitleNow: null,
presenterNow: null,
titleNext: null,
subtitleNext: null,
presenterNext: null,
};
titles = {
titleNow: null,
subtitleNow: null,
presenterNow: null,
noteNow: null,
titleNext: null,
subtitleNext: null,
presenterNext: null,
noteNext: null,
};
selectedEventIndex = null;
selectedEventId = null;
nextEventId = null;
selectedPublicEventId = null;
nextPublicEventId = null;
numEvents = null;
_eventlist = null;
onAir = false;
/**
* Instantiates an event timer object
* @param httpServer
* @param timerConfig
* @param [oscConfig]
* @param [httpConfig]
*/
constructor(httpServer, timerConfig, oscConfig, httpConfig) {
// call super constructor
super();
// initialise class variables
this.numEvents = 0; this.numEvents = 0;
this._eventlist = null;
this.onAir = false;
// initialise socketIO server // initialise socketIO server
this.messageStack = [];
this.MAX_MESSAGES = 100;
this._clientNames = {};
this.io = new Server(httpServer, { this.io = new Server(httpServer, {
cors: { cors: {
origin: '*', origin: '*',
@@ -114,22 +89,6 @@ export class EventTimer extends Timer {
}, },
}); });
// Todo: extract
// initialise osc object
if (oscConfig != null) {
console.log('initialise OSC Client on port: ', oscConfig?.port);
this.osc = new OSCIntegration();
this.osc.init(oscConfig);
}
// Todo: extract
// initialise http object
if (httpConfig != null) {
this.http = new HTTPIntegration();
this.http.init(httpConfig);
this.httpMessages = httpConfig.messages;
}
// set recurrent emits // set recurrent emits
this._interval = setInterval( this._interval = setInterval(
() => this.runCycle(), () => this.runCycle(),
@@ -138,28 +97,65 @@ export class EventTimer extends Timer {
// listen to new connections // listen to new connections
this._listenToConnections(); this._listenToConnections();
if (oscConfig != null) {
this._initOscClient(oscConfig);
}
if (httpConfig != null) {
this._initHTTPClient(httpConfig);
}
} }
/** /**
* @description Shutdown process * @description Shutdown process
*/ */
shutdown() { shutdown() {
console.log('Shutting down integrations') this.info('SERVER', 'Shutting down ontime');
console.log('... Closing socket server'); this.info('TX', '... Closing socket server');
this.io.close(); this.io.close();
console.log('... Closing osc server'); this.info('TX', '... Closing OSC Client');
this.osc.shutdown(); this.osc.shutdown();
this.info('TX', '... Closing HTTP Client');
this.http.shutdown();
} }
// send current timer /**
* Initialises OSC Integration object
* @param {object} oscConfig
* @private
*/
_initOscClient(oscConfig) {
this.osc = new OSCIntegration();
const r = this.osc.init(oscConfig);
r.success ? this.info('TX', r.message) : this.error('TX', r.message);
}
/**
* Initialises HTTP Integration object
* @param {object} httpConfig
* @private
*/
_initHTTPClient(httpConfig) {
this.info('TX', `Initialise HTTP Client on port`);
this.http = new HTTPIntegration();
this.http.init(httpConfig);
this.httpMessages = httpConfig.messages;
}
/**
* Sends time object over websockets
*/
broadcastTimer() { broadcastTimer() {
// through websockets // through websockets
this.io.emit('timer', this.getTimes()); this.io.emit('timer', this.getTimeObject());
} }
// broadcast state /**
broadcastState(update = true) { * Broadcasts complete object state
this.io.emit('timer', this.getTimes(update)); */
broadcastState() {
this.broadcastTimer();
this.io.emit('playstate', this.state); this.io.emit('playstate', this.state);
this.io.emit('selected', { this.io.emit('selected', {
id: this.selectedEventId, id: this.selectedEventId,
@@ -168,6 +164,7 @@ export class EventTimer extends Timer {
}); });
this.io.emit('selected-id', this.selectedEventId); this.io.emit('selected-id', this.selectedEventId);
this.io.emit('next-id', this.nextEventId); this.io.emit('next-id', this.nextEventId);
this.io.emit('numevents', this.numEvents);
this.io.emit('publicselected-id', this.selectedPublicEventId); this.io.emit('publicselected-id', this.selectedPublicEventId);
this.io.emit('publicnext-id', this.nextPublicEventId); this.io.emit('publicnext-id', this.nextPublicEventId);
this.io.emit('titles', this.titles); this.io.emit('titles', this.titles);
@@ -175,7 +172,11 @@ export class EventTimer extends Timer {
this.io.emit('onAir', this.onAir); this.io.emit('onAir', this.onAir);
} }
// broadcast message /**
* Broadcast given message
* @param {string} address - socket io address
* @param {any} payload - message body
*/
broadcastThis(address, payload) { broadcastThis(address, payload) {
this.io.emit(address, payload); this.io.emit(address, payload);
} }
@@ -192,69 +193,73 @@ export class EventTimer extends Timer {
case 'start': case 'start':
if (this.numEvents === 0 || this.numEvents == null) return false; if (this.numEvents === 0 || this.numEvents == null) return false;
// Call action and force update // Call action and force update
this.info('PLAYBACK', 'Play Mode Start');
this.start(); this.start();
this.runCycle();
break; break;
case 'pause': case 'pause':
if (this.numEvents === 0 || this.numEvents == null) return false; if (this.numEvents === 0 || this.numEvents == null) return false;
// Call action and force update // Call action and force update
this.info('PLAYBACK', 'Play Mode Pause');
this.pause(); this.pause();
this.runCycle();
break; break;
case 'stop': case 'stop':
if (this.numEvents === 0 || this.numEvents == null) return false; if (this.numEvents === 0 || this.numEvents == null) return false;
// Call action and force update // Call action and force update
this.info('PLAYBACK', 'Play Mode Stop');
this.stop(); this.stop();
this.runCycle();
break; break;
case 'roll': case 'roll':
if (this.numEvents === 0 || this.numEvents == null) return false; if (this.numEvents === 0 || this.numEvents == null) return false;
// Call action and force update // Call action and force update
this.info('PLAYBACK', 'Play Mode Roll');
this.roll(); this.roll();
this.runCycle();
break; break;
case 'previous': case 'previous':
if (this.numEvents === 0 || this.numEvents == null) return false; if (this.numEvents === 0 || this.numEvents == null) return false;
// Call action and force update // Call action and force update
this.info('PLAYBACK', 'Play Mode Previous');
this.previous(); this.previous();
this.runCycle();
break; break;
case 'next': case 'next':
if (this.numEvents === 0 || this.numEvents == null) return false; if (this.numEvents === 0 || this.numEvents == null) return false;
// Call action and force update // Call action and force update
this.info('PLAYBACK', 'Play Mode Next');
this.next(); this.next();
this.runCycle();
break; break;
case 'unload': case 'unload':
if (this.numEvents === 0 || this.numEvents == null) return false; if (this.numEvents === 0 || this.numEvents == null) return false;
// Call action and force update // Call action and force update
this.info('PLAYBACK', 'Events unloaded');
this.unload(); this.unload();
this.runCycle();
break; break;
case 'reload': case 'reload':
if (this.numEvents === 0 || this.numEvents == null) return false; if (this.numEvents === 0 || this.numEvents == null) return false;
// Call action and force update // Call action and force update
this.info('PLAYBACK', 'Reloaded event');
this.reload(); this.reload();
this.runCycle();
break; break;
case 'onAir': case 'onAir':
// Call action // Call action
this.info('PLAYBACK', 'Going On Air');
this.setonAir(true); this.setonAir(true);
break; break;
case 'offAir': case 'offAir':
// Call action and force update // Call action and force update
this.info('PLAYBACK', 'Going Off Air');
this.setonAir(false); this.setonAir(false);
break; break;
default: default:
// Error, disable flag // Error, disable flag
console.log('ERROR: Unhandled action triggered') this.error('RX', `Unhandled action triggered ${action}`);
reply = false; reply = false;
break; break;
} }
// update state
this.runCycle();
return reply; return reply;
} }
/** /**
* @description State machine checks what actions need to * @description State machine checks what actions need to
* happen at every app cycle * happen at every app cycle
@@ -264,18 +269,19 @@ export class EventTimer extends Timer {
let httpMessage = null; let httpMessage = null;
switch (this.ontimeCycle) { switch (this.ontimeCycle) {
case "idle": case 'idle':
break; break;
case "armed": case 'armed':
// if we come from roll, see if we can start // if we come from roll, see if we can start
if (this.state === 'roll') { if (this.state === 'roll') {
this.update(); this.update();
} }
break; break;
case "onLoad": case 'onLoad':
// broadcast change // broadcast change
this.broadcastState(); this.broadcastState();
// Todo: wrap in reusable function
// check integrations - http // check integrations - http
if (h?.onLoad?.enabled) { if (h?.onLoad?.enabled) {
if (h?.onLoad?.url != null || h?.onLoad?.url !== '') { if (h?.onLoad?.url != null || h?.onLoad?.url !== '') {
@@ -286,13 +292,13 @@ export class EventTimer extends Timer {
// update lifecycle: armed // update lifecycle: armed
this.ontimeCycle = this.cycleState.armed; this.ontimeCycle = this.cycleState.armed;
break; break;
case "onStart": case 'onStart':
// broadcast current state // broadcast current state
this.broadcastState(); this.broadcastState();
// send OSC if there is something running // send OSC if there is something running
// _finish at is only set when an event is loaded // _finish at is only set when an event is loaded
if (this._finishAt > 0) { if (this._finishAt > 0) {
this.osc.send(this.osc.implemented.play); this.sendOsc(this.osc.implemented.play);
} }
// check integrations - http // check integrations - http
@@ -305,7 +311,7 @@ export class EventTimer extends Timer {
// update lifecycle: onUpdate // update lifecycle: onUpdate
this.ontimeCycle = this.cycleState.onUpdate; this.ontimeCycle = this.cycleState.onUpdate;
break; break;
case "onUpdate": case 'onUpdate':
// call update // call update
this.update(); this.update();
// broadcast current state // broadcast current state
@@ -313,9 +319,15 @@ export class EventTimer extends Timer {
// through OSC, only if running // through OSC, only if running
if (this.state === 'start' || this.state === 'roll') { if (this.state === 'start' || this.state === 'roll') {
if (this.current != null && this.secondaryTimer == null) { if (this.current != null && this.secondaryTimer == null) {
this.osc.send(this.osc.implemented.time, this.timeTag); this.sendOsc(this.osc.implemented.time, this.timeTag);
this.osc.send(this.osc.implemented.overtime, this.current > 0 ? 0 : 1); this.sendOsc(
this.osc.send(this.osc.implemented.title, this.titles?.titleNow || ''); this.osc.implemented.overtime,
this.current > 0 ? 0 : 1
);
this.sendOsc(
this.osc.implemented.title,
this.titles?.titleNow || ''
);
} }
} }
@@ -327,11 +339,11 @@ export class EventTimer extends Timer {
} }
break; break;
case "onPause": case 'onPause':
// broadcast current state // broadcast current state
this.broadcastState(); this.broadcastState();
// send OSC // send OSC
this.osc.send(this.osc.implemented.pause); this.sendOsc(this.osc.implemented.pause);
// check integrations - http // check integrations - http
if (h?.onLoad?.enabled) { if (h?.onLoad?.enabled) {
@@ -344,13 +356,13 @@ export class EventTimer extends Timer {
this.ontimeCycle = this.cycleState.armed; this.ontimeCycle = this.cycleState.armed;
break; break;
case "onStop": case 'onStop':
// broadcast change // broadcast change
this.broadcastState(); this.broadcastState();
// send OSC if something was actually stopped // send OSC if something was actually stopped
if (this.prevCycle === this.cycleState.onUpdate) { if (this.prevCycle === this.cycleState.onUpdate) {
this.osc.send(this.osc.implemented.stop); this.sendOsc(this.osc.implemented.stop);
} }
// check integrations - http // check integrations - http
@@ -363,12 +375,11 @@ export class EventTimer extends Timer {
// update lifecycle: idle // update lifecycle: idle
this.ontimeCycle = this.cycleState.idle; this.ontimeCycle = this.cycleState.idle;
break; break;
case "onFinish": case 'onFinish':
console.log('onFinish')
// broadcast change // broadcast change
this.broadcastState(false); this.broadcastState();
// finished an event // finished an event
this.osc.send(this.osc.implemented.finished); this.sendOsc(this.osc.implemented.finished);
// check integrations - http // check integrations - http
if (h?.onLoad?.enabled) { if (h?.onLoad?.enabled) {
@@ -381,20 +392,20 @@ export class EventTimer extends Timer {
this.ontimeCycle = this.cycleState.onUpdate; this.ontimeCycle = this.cycleState.onUpdate;
break; break;
default: default:
console.log(`ERROR: Unhandled cycle: ${this.ontimeCycle}`) this.error('SERVER', `Unhandled cycle: ${this.ontimeCycle}`);
} }
// send http message if any // send http message if any
if (httpMessage != null) { if (httpMessage != null) {
const v = { const v = {
'$timer': this.timeTag, $timer: this.timeTag,
'$title': this.titles.titleNow, $title: this.titles.titleNow,
'$presenter': this.titles.presenterNow, $presenter: this.titles.presenterNow,
'$subtitle': this.titles.subtitleNow, $subtitle: this.titles.subtitleNow,
'$next-title': this.titles.titleNext, '$next-title': this.titles.titleNext,
'$next-presenter': this.titles.presenterNext, '$next-presenter': this.titles.presenterNext,
'$next-subtitle': this.titles.subtitleNext, '$next-subtitle': this.titles.subtitleNext,
} };
const m = cleanURL(replacePlaceholder(httpMessage, v)); const m = cleanURL(replacePlaceholder(httpMessage, v));
this.http.send(m); this.http.send(m);
} }
@@ -407,7 +418,6 @@ export class EventTimer extends Timer {
} }
update() { update() {
// if there is nothing selected, update clock // if there is nothing selected, update clock
const now = this._getCurrentTime(); const now = this._getCurrentTime();
@@ -450,13 +460,17 @@ export class EventTimer extends Timer {
selectedEventId: this.selectedEventId, selectedEventId: this.selectedEventId,
current: this.current, current: this.current,
// safeguard on midnight rollover // 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, clock: this.clock,
secondaryTimer: this.secondaryTimer, secondaryTimer: this.secondaryTimer,
_secondaryTarget: this._secondaryTarget, _secondaryTarget: this._secondaryTarget,
} };
const {updatedTimer, updatedSecondaryTimer, doRollLoad, isFinished} = updateRoll(u); const { updatedTimer, updatedSecondaryTimer, doRollLoad, isFinished } =
updateRoll(u);
this.current = updatedTimer; this.current = updatedTimer;
this.secondaryTimer = updatedSecondaryTimer; this.secondaryTimer = updatedSecondaryTimer;
@@ -473,7 +487,13 @@ export class EventTimer extends Timer {
} }
} }
_setterManager(action, payload) { /**
* Set titles and broadcast change
* @param {string} action
* @param {any} payload
* @private
*/
_setTitles(action, payload) {
switch (action) { switch (action) {
/*******************************************/ /*******************************************/
// Presenter message // Presenter message
@@ -513,6 +533,10 @@ export class EventTimer extends Timer {
} }
} }
/**
* Handle socket io connections
* @private
*/
_listenToConnections() { _listenToConnections() {
this.io.on('connection', (socket) => { this.io.on('connection', (socket) => {
/*******************************/ /*******************************/
@@ -521,12 +545,14 @@ export class EventTimer extends Timer {
/*******************************/ /*******************************/
// keep track of connections // keep track of connections
this._numClients++; this._numClients++;
console.log( this._clientNames[socket.id] = getRandomName();
`EventTimer: ${this._numClients} Clients with new connection: ${socket.id}` const m = `${this._numClients} Clients with new connection: ${
); this._clientNames[socket.id]
}`;
this.info('CLIENT', m);
// send state // send state
socket.emit('timer', this.getTimes()); socket.emit('timer', this.getTimeObject());
socket.emit('playstate', this.state); socket.emit('playstate', this.state);
socket.emit('selected-id', this.selectedEventId); socket.emit('selected-id', this.selectedEventId);
socket.emit('next-id', this.nextEventId); socket.emit('next-id', this.nextEventId);
@@ -539,9 +565,11 @@ export class EventTimer extends Timer {
/********************************/ /********************************/
socket.on('disconnect', () => { socket.on('disconnect', () => {
this._numClients--; this._numClients--;
console.log( const m = `${this._numClients} Clients with disconnection: ${
`EventTimer: Client disconnected, total now: ${this._numClients}` this._clientNames[socket.id]
); }`;
delete this._clientNames[socket.id];
this.info('CLIENT', m);
}); });
/***************************************/ /***************************************/
@@ -552,7 +580,7 @@ export class EventTimer extends Timer {
/*******************************************/ /*******************************************/
// general playback state // general playback state
socket.on('get-state', () => { socket.on('get-state', () => {
socket.emit('timer', this.getTimes()); socket.emit('timer', this.getTimeObject());
socket.emit('playstate', this.state); socket.emit('playstate', this.state);
socket.emit('selected-id', this.selectedEventId); socket.emit('selected-id', this.selectedEventId);
socket.emit('next-id', this.nextEventId); socket.emit('next-id', this.nextEventId);
@@ -567,7 +595,7 @@ export class EventTimer extends Timer {
}); });
socket.on('get-timer', () => { socket.on('get-timer', () => {
socket.emit('timer', this.getTimes()); socket.emit('timer', this.getTimeObject());
}); });
socket.on('increment-timer', (data) => { socket.on('increment-timer', (data) => {
@@ -651,11 +679,11 @@ export class EventTimer extends Timer {
// Presenter message // Presenter message
socket.on('set-presenter-text', (data) => { socket.on('set-presenter-text', (data) => {
this._setterManager('set-presenter-text', data); this._setTitles('set-presenter-text', data);
}); });
socket.on('set-presenter-visible', (data) => { socket.on('set-presenter-visible', (data) => {
this._setterManager('set-presenter-visible', data); this._setTitles('set-presenter-visible', data);
}); });
socket.on('get-presenter', () => { socket.on('get-presenter', () => {
@@ -664,11 +692,11 @@ export class EventTimer extends Timer {
/*******************************************/ /*******************************************/
// Public message // Public message
socket.on('set-public-text', (data) => { socket.on('set-public-text', (data) => {
this._setterManager('set-public-text', data); this._setTitles('set-public-text', data);
}); });
socket.on('set-public-visible', (data) => { socket.on('set-public-visible', (data) => {
this._setterManager('set-public-visible', data); this._setTitles('set-public-visible', data);
}); });
socket.on('get-public', () => { socket.on('get-public', () => {
@@ -678,11 +706,11 @@ export class EventTimer extends Timer {
/*******************************************/ /*******************************************/
// Lower third message // Lower third message
socket.on('set-lower-text', (data) => { socket.on('set-lower-text', (data) => {
this._setterManager('set-lower-text', data); this._setTitles('set-lower-text', data);
}); });
socket.on('set-lower-visible', (data) => { socket.on('set-lower-visible', (data) => {
this._setterManager('set-lower-visible', data); this._setTitles('set-lower-visible', data);
}); });
socket.on('get-lower', () => { socket.on('get-lower', () => {
@@ -691,6 +719,9 @@ export class EventTimer extends Timer {
}); });
} }
/**
* Deletes running event list from object
*/
clearEventList() { clearEventList() {
// unload events // unload events
this.unload(); this.unload();
@@ -706,6 +737,10 @@ export class EventTimer extends Timer {
this.broadcastThis('numevents', this.numEvents); this.broadcastThis('numevents', this.numEvents);
} }
/**
* Adds an event list to object
* @param {array} eventlist
*/
setupWithEventList(eventlist) { setupWithEventList(eventlist) {
if (!Array.isArray(eventlist) || eventlist.length < 1) return; if (!Array.isArray(eventlist) || eventlist.length < 1) return;
@@ -717,19 +752,23 @@ export class EventTimer extends Timer {
this._eventlist = events; this._eventlist = events;
this.numEvents = numEvents; this.numEvents = numEvents;
// list may be empty // list may contain no events
if (numEvents < 1) return; if (numEvents < 1) return;
// load first event // load first event
this.loadEvent(0); this.loadEvent(0);
// update clients // update clients
this.broadcastThis('numevents', this.numEvents); this.broadcastState();
// run cycle // run cycle
this.runCycle(); this.runCycle();
} }
/**
* Updates event list in object
* @param {array} eventlist
*/
updateEventList(eventlist) { updateEventList(eventlist) {
// filter only events // filter only events
const events = eventlist.filter((e) => e.type === 'event'); const events = eventlist.filter((e) => e.type === 'event');
@@ -771,12 +810,17 @@ export class EventTimer extends Timer {
} }
// update clients // update clients
this.broadcastThis('numevents', this.numEvents); this.broadcastState();
// run cycle // run cycle
this.runCycle(); this.runCycle();
} }
/**
* Updates a single id in the object list
* @param {string} id
* @param {object} entry - new event object
*/
updateSingleEvent(id, entry) { updateSingleEvent(id, entry) {
// find object in events // find object in events
const eventIndex = this._eventlist.findIndex((e) => e.id === id); const eventIndex = this._eventlist.findIndex((e) => e.id === id);
@@ -810,16 +854,20 @@ export class EventTimer extends Timer {
this._loadTitlesNow(); this._loadTitlesNow();
} }
} catch (error) { } catch (error) {
console.log(error); this.error('SERVER', error);
} }
// update clients // update clients
this.broadcastThis('numevents', this.numEvents); this.broadcastState();
// run cycle // run cycle
this.runCycle(); this.runCycle();
} }
/**
* Deleted an event from the list by its id
* @param {string} eventId
*/
deleteId(eventId) { deleteId(eventId) {
// find object in events // find object in events
const eventIndex = this._eventlist.findIndex((e) => e.id === eventId); const eventIndex = this._eventlist.findIndex((e) => e.id === eventId);
@@ -848,7 +896,7 @@ export class EventTimer extends Timer {
} }
// update clients // update clients
this.broadcastThis('numevents', this.numEvents); this.broadcastState();
// run cycle // run cycle
this.runCycle(); this.runCycle();
@@ -856,34 +904,35 @@ export class EventTimer extends Timer {
/** /**
* @description loads an event with a given Id * @description loads an event with a given Id
* @param eventId - ID of event in eventlist * @param {string} eventId - ID of event in eventlist
*/ */
loadEventById(eventId) { loadEventById(eventId) {
const eventIndex = this._eventlist.findIndex((e) => e.id === eventId); const eventIndex = this._eventlist.findIndex((e) => e.id === eventId);
if (eventIndex === -1) return; if (eventIndex === -1) return;
this.pause(); this.pause();
this.loadEvent(eventIndex, 'load', true); this.loadEvent(eventIndex, 'load');
// run cycle // run cycle
this.runCycle(); this.runCycle();
} }
/** /**
* @description loads an event with a given index * @description loads an event with a given index
* @param eventIndex - Index of event in eventlist * @param {number} eventIndex - Index of event in eventlist
*/ */
loadEventByIndex(eventIndex) { loadEventByIndex(eventIndex) {
if (eventIndex === -1 || eventIndex > this.numEvents) return; if (eventIndex === -1 || eventIndex > this.numEvents) return;
this.pause(); this.pause();
this.loadEvent(eventIndex, 'load', true); this.loadEvent(eventIndex, 'load');
// run cycle // run cycle
this.runCycle(); this.runCycle();
} }
// Loads a given event /**
// load timers * Loads a given event by index
// load selectedEventIndex * @param {object} eventIndex
// load titles * @param {string} [type='load'] - 'load' or 'reload', whether we are keeping running time
*/
loadEvent(eventIndex, type = 'load') { loadEvent(eventIndex, type = 'load') {
const e = this._eventlist[eventIndex]; const e = this._eventlist[eventIndex];
if (e == null) return; if (e == null) return;
@@ -894,7 +943,6 @@ export class EventTimer extends Timer {
if (end < start) end += DAY_TO_MS; if (end < start) end += DAY_TO_MS;
// time stuff changes on whether we keep the running clock // time stuff changes on whether we keep the running clock
if (type === 'load') { if (type === 'load') {
this._resetTimers(); this._resetTimers();
@@ -1092,75 +1140,6 @@ export class EventTimer extends Timer {
this.nextPublicEventId = null; this.nextPublicEventId = null;
} }
print() {
return `
Timer
=========
Playback
------------------------------
state = ${this.state}
current = ${this.current}
duration = ${this.duration}
secondaryTimer = ${this.secondaryTimer}
Events
------------------------------
numEvents = ${this.numEvents}
selectedEventIndex = ${this.selectedEventIndex}
selectedEventId = ${this.selectedEventId}
nextEventId = ${this.nextEventId}
selectedPublicEventId = ${this.selectedPublicEventId}
nextPublicEventId = ${this.nextPublicEventId}
Private Titles
------------------------------
NowID = ${this.selectedEventId}
NextID = ${this.nextEventId}
Title Now = ${this.titles.titleNow}
Subtitle Now = ${this.titles.subtitleNow}
Presenter Now = ${this.titles.presenterNow}
Note Now = ${this.titles.noteNow}
Title Next = ${this.titles.titleNext}
Subtitle Next = ${this.titles.subtitleNext}
Presenter Next = ${this.titles.presenterNext}
Note Next = ${this.titles.noteNext}
Public Titles
------------------------------
NowID = ${this.selectedPublicEventId}
NextID = ${this.nextPublicEventId}
Title Now = ${this.titlesPublic.titleNow}
Subtitle Now = ${this.titlesPublic.subtitleNow}
Presenter Now = ${this.titlesPublic.presenterNow}
Title Next = ${this.titlesPublic.titleNext}
Subtitle Next = ${this.titlesPublic.subtitleNext}
Presenter Next = ${this.titlesPublic.presenterNext}
Messages
------------------------------
presenter text = ${this.presenter.text}
presenter vis = ${this.presenter.visible}
public text = ${this.public.text}
public vis = ${this.public.visible}
lower text = ${this.lower.text}
lower vis = ${this.lower.visible}
Private
------------------------------
finishAt = ${this._finishAt}
finished = ${this._finishedAt}
startedAt = ${this._startedAt}
pausedAt = ${this._pausedAt}
pausedInterval = ${this._pausedInterval}
pausedTotal = ${this._pausedTotal}
Socket
------------------------------
numClients = ${this._numClients}
`;
}
/** /**
* @description Set onAir property of timer * @description Set onAir property of timer
* @param {boolean} onAir - whether flag is active * @param {boolean} onAir - whether flag is active
@@ -1241,7 +1220,7 @@ export class EventTimer extends Timer {
// nothing to play, unload // nothing to play, unload
if (nowIndex === null && nextIndex === null) { if (nowIndex === null && nextIndex === null) {
this.unload(); this.unload();
console.log('Roll: no events found'); this.warning('SERVER', 'Roll: no events found');
return; return;
} }
@@ -1267,8 +1246,9 @@ export class EventTimer extends Timer {
// Set running timers // Set running timers
if (nowIndex === null) { if (nowIndex === null) {
// only warn the first time // only warn the first time
if (this.secondaryTimer === null) if (this.secondaryTimer === null) {
console.log('Roll: waiting for event start'); this.info('SERVER', 'Roll: waiting for event start');
}
// reset running timer // reset running timer
// ??? should this not have been reset? // ??? should this not have been reset?
@@ -1319,9 +1299,6 @@ export class EventTimer extends Timer {
// load into event // load into event
this.rollLoad(); this.rollLoad();
// broadcast change
this.broadcastState();
} }
previous() { previous() {
@@ -1338,7 +1315,7 @@ export class EventTimer extends Timer {
} }
// send OSC // send OSC
this.osc.send(this.osc.implemented.previous); this.sendOsc(this.osc.implemented.previous);
// change playstate // change playstate
this.pause(); this.pause();
@@ -1364,7 +1341,7 @@ export class EventTimer extends Timer {
} }
// send OSC // send OSC
this.osc.send(this.osc.implemented.next); this.sendOsc(this.osc.implemented.next);
// change playstate // change playstate
this.pause(); this.pause();
@@ -1399,9 +1376,94 @@ export class EventTimer extends Timer {
this.pause(); this.pause();
// send OSC // send OSC
this.osc.send(this.osc.implemented.reload); this.sendOsc(this.osc.implemented.reload);
// reload data // reload data
this.loadEvent(this.selectedEventIndex); this.loadEvent(this.selectedEventIndex);
} }
/****************************************************************************/
/**
* Logger logic
* -------------
*
* This should be separate of event timer, left here for convenience
*
*/
/**
* Utility method, sends message and pushes into stack
* @param {string} level
* @param {string} origin
* @param {string} text
*/
_push(level, origin, text) {
const m = {
id: generateId(),
level,
origin,
text,
time: stringFromMillis(this._getCurrentTime()),
};
this.messageStack.unshift(m);
this.io.emit('logger', m);
if (process.env.NODE_ENV !== 'prod') {
console.log(`[${m.level}] \t ${m.origin} \t ${m.text}`);
}
if (this.messageStack.length > this.MAX_MESSAGES) {
this.messageStack.pop();
}
}
/**
* Sends a message with level LOG
* @param {string} origin
* @param {string} text
*/
info(origin, text) {
this._push('INFO', origin, text);
}
/**
* Sends a message with level WARN
* @param {string} origin
* @param {string} text
*/
warning(origin, text) {
this._push('WARN', origin, text);
}
/**
* Sends a message with level ERROR
* @param {string} origin
* @param {string} text
*/
error(origin, text) {
this._push('ERROR', origin, text);
}
/****************************************************************************/
/**
* Integrations
* -------------
*
* Code related to integrations
*
*/
/**
* Calls OSC send message and resolves reply to logger
* @param {string} message
* @param {any} [payload]
*/
async sendOsc(message, payload = undefined) {
// Todo: add disabled osc check
const reply = await this.osc.send(message, payload);
if (!reply.success) {
this.error('TX', reply.message);
}
}
} }
+18 -30
View File
@@ -7,22 +7,11 @@
import { stringFromMillis } from '../utils/time.js'; import { stringFromMillis } from '../utils/time.js';
export class Timer { export class Timer {
clock = null; constructor() {
duration = null; this.clock = null;
current = null; this._resetTimers(true);
timeTag = null; this.state = 'stop';
secondaryTimer = null; }
_secondaryTarget = null;
_finishAt = null;
_finishedAt = null;
_finishedFlag = false;
_startedAt = null;
_pausedAt = null;
_pausedInterval = null;
_pausedTotal = null;
state = 'stop';
constructor() {}
// call setup separately // call setup separately
setupWithSeconds(seconds, autoStart = false) { setupWithSeconds(seconds, autoStart = false) {
@@ -74,11 +63,11 @@ export class Timer {
if (this._startedAt != null) { if (this._startedAt != null) {
// update current timer // update current timer
this.current = this.current =
this._startedAt this._startedAt +
+ this.duration this.duration +
+ this._pausedTotal this._pausedTotal +
+ this._pausedInterval this._pausedInterval -
- now; now;
} }
// enable flag // enable flag
@@ -92,13 +81,14 @@ export class Timer {
if (checkFinish) { if (checkFinish) {
// is event finished? // is event finished?
const isTimeOver = this.current <= 0; const isTimeOver = this.current <= 0;
const isUpdating = (this.state !== 'pause'); const isUpdating = this.state !== 'pause';
if (isTimeOver && isUpdating && this._finishedAt == null) { if (isTimeOver && isUpdating && this._finishedAt == null) {
if (this._finishedAt === null) this._finishedAt = now; if (this._finishedAt === null) this._finishedAt = now;
this._finishedFlag = true; this._finishedFlag = true;
} }
} }
this.timeTag = stringFromMillis(this.current);
} }
// helpers // helpers
@@ -136,6 +126,7 @@ export class Timer {
_resetTimers(total = false) { _resetTimers(total = false) {
if (total) this.duration = null; if (total) this.duration = null;
this.current = this.duration; this.current = this.duration;
this.timeTag = null;
this.running = null; this.running = null;
this.secondaryTimer = null; this.secondaryTimer = null;
this._secondaryTarget = null; this._secondaryTarget = null;
@@ -153,14 +144,11 @@ export class Timer {
return this.duration - this.current; return this.duration - this.current;
} }
// get time object /**
getTimes(update = true) { * Builds time object
// update timer * @returns {{running: number, secondary: number, expectedFinish: number, durationSeconds: number, startedAt: null, clock: null}}
if (update) this.update(); */
getTimeObject() {
// update timetag
this.timeTag = stringFromMillis(this.current);
return { return {
clock: this.clock, clock: this.clock,
running: Timer.toSeconds(this.current), running: Timer.toSeconds(this.current),
+16 -11
View File
@@ -1,4 +1,3 @@
/** /**
* Utility variable: 24 hour in milliseconds . * Utility variable: 24 hour in milliseconds .
* @type {number} * @type {number}
@@ -11,7 +10,8 @@ export const DAY_TO_MS = 86400000;
* @param {number} end - When does the event end * @param {number} end - When does the event end
* @returns {number} normalised time * @returns {number} normalised time
*/ */
export const normaliseEndTime = (start, end) => (end < start ? end + DAY_TO_MS : end); export const normaliseEndTime = (start, end) =>
end < start ? end + DAY_TO_MS : end;
/** /**
* @description Sorts an array of objects by given property * @description Sorts an array of objects by given property
@@ -36,7 +36,6 @@ export const sortArrayByProperty = (arr, property) => {
export const replacePlaceholder = (str, values) => { export const replacePlaceholder = (str, values) => {
for (let [k, v] of Object.entries(values)) { for (let [k, v] of Object.entries(values)) {
str = str.replace(k, v); str = str.replace(k, v);
console.log(k, v);
} }
return str; return str;
}; };
@@ -72,7 +71,10 @@ export const getSelectionByRoll = (arr, now) => {
// exit early if we are past the events // exit early if we are past the events
const lastEvent = orderedEvents[orderedEvents.length - 1]; const lastEvent = orderedEvents[orderedEvents.length - 1];
const lastNormalEnd = normaliseEndTime(lastEvent.timeStart, lastEvent.timeEnd); const lastNormalEnd = normaliseEndTime(
lastEvent.timeStart,
lastEvent.timeEnd
);
if (now > lastNormalEnd) { if (now > lastNormalEnd) {
return { return {
nowIndex, nowIndex,
@@ -128,7 +130,7 @@ export const getSelectionByRoll = (arr, now) => {
// check how far the start is from now // check how far the start is from now
const wait = e.timeStart - now; const wait = e.timeStart - now;
if (nextIndex === null || wait < timeToNext) { if (nextIndex == null || wait < timeToNext) {
timeToNext = wait; timeToNext = wait;
nextIndex = arr.findIndex((a) => a.id === e.id); nextIndex = arr.findIndex((a) => a.id === e.id);
} }
@@ -161,8 +163,14 @@ export const getSelectionByRoll = (arr, now) => {
* @returns {object} object with selection variables * @returns {object} object with selection variables
*/ */
export const updateRoll = (currentTimers) => { export const updateRoll = (currentTimers) => {
const {
const {selectedEventId,current,_finishAt,clock,secondaryTimer,_secondaryTarget} = currentTimers; selectedEventId,
current,
_finishAt,
clock,
secondaryTimer,
_secondaryTarget,
} = currentTimers;
// timers // timers
let updatedTimer = current; let updatedTimer = current;
@@ -181,8 +189,6 @@ export const updateRoll = (currentTimers) => {
if (updatedTimer < 0) { if (updatedTimer < 0) {
isFinished = true; isFinished = true;
} }
console.log(updatedTimer, isFinished, _finishAt)
} else if (secondaryTimer >= 0) { } else if (secondaryTimer >= 0) {
// if secondaryTimer is running we are in waiting to roll // if secondaryTimer is running we are in waiting to roll
@@ -203,5 +209,4 @@ export const updateRoll = (currentTimers) => {
} }
return { updatedTimer, updatedSecondaryTimer, doRollLoad, isFinished }; return { updatedTimer, updatedSecondaryTimer, doRollLoad, isFinished };
} };
+52 -18
View File
@@ -2,11 +2,9 @@
import { Client, Message } from 'node-osc'; import { Client, Message } from 'node-osc';
export class OSCIntegration { export class OSCIntegration {
ADDRESS = '/ontime';
constructor() { constructor() {
// OSC Client // OSC Client
this.ADDRESS = '/ontime';
this.oscClient = null; this.oscClient = null;
} }
@@ -27,7 +25,7 @@ export class OSCIntegration {
overtime: 'overtime', overtime: 'overtime',
title: 'title', title: 'title',
presenter: 'presenter', presenter: 'presenter',
} };
} }
/** /**
@@ -40,10 +38,16 @@ export class OSCIntegration {
const { ip, port } = oscConfig; const { ip, port } = oscConfig;
try { try {
this.oscClient = new Client(ip, port); this.oscClient = new Client(ip, port);
console.log(`Initialised OSC Client at ${ip}:${port}`); return {
success: true,
message: `Initialised OSC Client at ${ip}:${port}`,
};
} catch (error) { } catch (error) {
this.oscClient = null; this.oscClient = null;
console.log(`Failed initialising OSC Client: ${error}`); return {
success: true,
message: `Failed initialising OSC Client: ${error}`,
};
} }
} }
@@ -53,14 +57,21 @@ export class OSCIntegration {
* @param {string} [payload] - optional payload required in some message types * @param {string} [payload] - optional payload required in some message types
*/ */
async send(messageType, payload) { async send(messageType, payload) {
const reply = {
success: true,
message: 'OSC Message sent',
};
if (this.oscClient == null) { if (this.oscClient == null) {
console.log('OSC ERROR: Client not initialised'); reply.success = false;
return; reply.message = 'Client not initialised';
return reply;
} }
if (messageType == null) { if (messageType == null) {
console.log('OSC ERROR: Message undefined'); reply.success = false;
return; reply.message = 'Message undefined';
return reply;
} }
// only specify special cases // only specify special cases
@@ -68,35 +79,58 @@ export class OSCIntegration {
case 'overtime': case 'overtime':
// Whether timer is negative // Whether timer is negative
this.oscClient.send(`${this.ADDRESS}/overtime`, payload, (err) => { this.oscClient.send(`${this.ADDRESS}/overtime`, payload, (err) => {
if (err) console.error(err); if (err) {
reply.success = false;
reply.message = err;
}
}); });
break; break;
case 'title': case 'title':
if (payload != null && payload !== "") { if (payload != null && payload !== '') {
// Send Title of current event // Send Title of current event
this.oscClient.send(`${this.ADDRESS}/title`, payload, (err) => { this.oscClient.send(`${this.ADDRESS}/title`, payload, (err) => {
if (err) console.error(err); if (err) {
reply.success = false;
reply.message = err;
}
}); });
} else {
reply.success = false;
reply.message = 'Missing message data';
} }
break; break;
case 'presenter': case 'presenter':
if (payload != null && payload !== "") { if (payload != null && payload !== '') {
// Send presenter data on current event // Send presenter data on current event
this.oscClient.send(`${this.ADDRESS}/presenter`, payload, (err) => { this.oscClient.send(`${this.ADDRESS}/presenter`, payload, (err) => {
if (err) console.error(err); if (err) {
reply.success = false;
reply.message = err;
}
}); });
} else {
reply.success = false;
reply.message = 'Missing message data';
} }
break; break;
default: default:
// catch all for messages, allows to add new messages // catch all for messages, allows to add new messages
// but should be used with the integrations definition // but should be used with the integrations definition
const message = new Message(`${this.ADDRESS}/${messageType}`) // eslint-disable-next-line no-case-declarations
if (payload != null) message.append(payload) const message = new Message(`${this.ADDRESS}/${messageType}`);
if (payload != null) message.append(payload);
this.oscClient.send(message, (err) => { this.oscClient.send(message, (err) => {
if (err) console.error(err); if (err) {
reply.success = false;
reply.message = err;
}
}); });
break; break;
} }
return reply;
} }
shutdown() { shutdown() {
+13 -20
View File
@@ -7,9 +7,7 @@ export const shutdownOSCServer = () => {
}; };
export const initiateOSC = (config) => { export const initiateOSC = (config) => {
oscServer = new Server(config.port, '0.0.0.0', () => { oscServer = new Server(config.port, '0.0.0.0');
console.log(`OSC Server is listening on port ${config.port}`);
});
// error // error
oscServer.on('error', console.error); oscServer.on('error', console.error);
@@ -19,7 +17,6 @@ export const initiateOSC = (config) => {
// ontime: fixed message for app // ontime: fixed message for app
// path: command to be called // path: command to be called
// args: extra data, only used on some of the API entries (delay, goto) // args: extra data, only used on some of the API entries (delay, goto)
console.log('OSC received', msg);
// split message // split message
const [, address, path] = msg[0].split('/'); const [, address, path] = msg[0].split('/');
@@ -46,40 +43,35 @@ export const initiateOSC = (config) => {
break; break;
case 'start': case 'start':
case 'play': case 'play':
console.log('calling play');
global.timer.trigger('start'); global.timer.trigger('start');
break; break;
case 'pause': case 'pause':
console.log('calling pause');
global.timer.trigger('pause'); global.timer.trigger('pause');
break; break;
case 'prev': case 'prev':
console.log('calling prev');
global.timer.trigger('previous'); global.timer.trigger('previous');
break; break;
case 'next': case 'next':
console.log('calling next');
global.timer.trigger('next'); global.timer.trigger('next');
break; break;
case 'unload': case 'unload':
case 'stop': case 'stop':
console.log('calling unload');
global.timer.trigger('unload'); global.timer.trigger('unload');
break; break;
case 'reload': case 'reload':
console.log('calling reload');
global.timer.trigger('reload'); global.timer.trigger('reload');
break; break;
case 'roll': case 'roll':
console.log('calling roll');
global.timer.trigger('roll'); global.timer.trigger('roll');
break; break;
case 'delay': case 'delay':
console.log('calling delay with', args);
try { try {
const t = parseInt(args); const t = parseInt(args);
if (isNaN(t)) { if (isNaN(t)) {
console.error(`OSC IN: delay time not recognised ${args}`); global.timer.error(
'RX',
`OSC IN: delay time not recognised ${args}`
);
return; return;
} }
global.timer.increment(t * 1000 * 60); global.timer.increment(t * 1000 * 60);
@@ -88,36 +80,37 @@ export const initiateOSC = (config) => {
} }
break; break;
case 'goto': case 'goto':
console.log('calling goto with', args);
try { try {
const eventIndex = parseInt(args); const eventIndex = parseInt(args);
if (isNaN(eventIndex) || eventIndex <= 0 || eventIndex == null) { if (isNaN(eventIndex) || eventIndex <= 0 || eventIndex == null) {
console.error( global.timer.error(
'RX',
`OSC IN: event index not recognised or out of range ${eventIndex}` `OSC IN: event index not recognised or out of range ${eventIndex}`
); );
} }
global.timer.loadEventByIndex(eventIndex - 1); global.timer.loadEventByIndex(eventIndex - 1);
} catch (error) { } catch (error) {
console.log('error calling goto: ', error); global.timer.error('RX', `OSC IN: error calling goto ${error}`);
} }
break; break;
case 'gotoid': case 'gotoid':
console.log('calling gotoid with', args); console.log('calling gotoid with', args);
if (args == null) { if (args == null) {
console.error( global.timer.error(
`OSC IN: event id not recognised or out of range ${args}` 'RX',
`OSC IN: event id not recognised or out of range ${args}}`
); );
return; return;
} }
try { try {
global.timer.loadEventById(args.toString().toLowerCase()); global.timer.loadEventById(args.toString().toLowerCase());
} catch (error) { } catch (error) {
console.log('error calling goto: ', error); global.timer.error('RX', `OSC IN: error calling goto ${error}`);
} }
break; break;
default: default:
console.log(`Error: unhandled message ${path}`); global.timer.warning('RX', `OSC IN: unhandled message ${path}`);
break; break;
} }
}); });
@@ -21,6 +21,5 @@ export const postEvent = async (req, res) => {
res.sendStatus(200); res.sendStatus(200);
} catch (error) { } catch (error) {
res.status(400).send(error); res.status(400).send(error);
console.log(error);
} }
}; };
@@ -130,7 +130,6 @@ export const postInfo = async (req, res) => {
res.sendStatus(200); res.sendStatus(200);
} catch (error) { } catch (error) {
res.status(400).send(error); res.status(400).send(error);
console.log(error);
} }
}; };
@@ -7,69 +7,59 @@ export const pbGet = async (req, res) => {
// Create controller for GET request to '/playback/onAir' // Create controller for GET request to '/playback/onAir'
// Turns onAir flag to true // Turns onAir flag to true
export const onAir = async (req, res) => { export const onAir = async (req, res) => {
console.log('Setting onAir to true');
global.timer.trigger('onAir') ? res.sendStatus(200) : res.sendStatus(400); global.timer.trigger('onAir') ? res.sendStatus(200) : res.sendStatus(400);
}; };
// Create controller for GET request to '/playback/onAir' // Create controller for GET request to '/playback/onAir'
// Turns onAir flag to true // Turns onAir flag to true
export const offAir = async (req, res) => { export const offAir = async (req, res) => {
console.log('Setting onAir to false');
global.timer.trigger('offAir') ? res.sendStatus(200) : res.sendStatus(400); global.timer.trigger('offAir') ? res.sendStatus(200) : res.sendStatus(400);
}; };
// Create controller for GET request to '/playback/start' // Create controller for GET request to '/playback/start'
// Starts timer object // Starts timer object
export const pbStart = async (req, res) => { export const pbStart = async (req, res) => {
console.log('Calling start');
global.timer.trigger('start') ? res.sendStatus(200) : res.sendStatus(400); global.timer.trigger('start') ? res.sendStatus(200) : res.sendStatus(400);
}; };
// Create controller for GET request to '/playback/pause' // Create controller for GET request to '/playback/pause'
// Pauses timer object // Pauses timer object
export const pbPause = async (req, res) => { export const pbPause = async (req, res) => {
console.log('Calling pause');
global.timer.trigger('pause') ? res.sendStatus(200) : res.sendStatus(400); global.timer.trigger('pause') ? res.sendStatus(200) : res.sendStatus(400);
}; };
// Create controller for GET request to '/playback/stop' // Create controller for GET request to '/playback/stop'
// Stops timer object // Stops timer object
export const pbStop = async (req, res) => { export const pbStop = async (req, res) => {
console.log('Calling stop');
global.timer.trigger('stop') ? res.sendStatus(200) : res.sendStatus(400); global.timer.trigger('stop') ? res.sendStatus(200) : res.sendStatus(400);
}; };
// Create controller for GET request to '/playback/roll' // Create controller for GET request to '/playback/roll'
// Sets timer object to roll mode // Sets timer object to roll mode
export const pbRoll = async (req, res) => { export const pbRoll = async (req, res) => {
console.log('Calling roll');
global.timer.trigger('roll') ? res.sendStatus(200) : res.sendStatus(400); global.timer.trigger('roll') ? res.sendStatus(200) : res.sendStatus(400);
}; };
// Create controller for GET request to '/playback/previous' // Create controller for GET request to '/playback/previous'
// Sets timer object to roll mode // Sets timer object to roll mode
export const pbPrevious = async (req, res) => { export const pbPrevious = async (req, res) => {
console.log('Calling previous');
global.timer.trigger('previous') ? res.sendStatus(200) : res.sendStatus(400); global.timer.trigger('previous') ? res.sendStatus(200) : res.sendStatus(400);
}; };
// Create controller for GET request to '/playback/next' // Create controller for GET request to '/playback/next'
// Sets timer object to roll mode // Sets timer object to roll mode
export const pbNext = async (req, res) => { export const pbNext = async (req, res) => {
console.log('Calling next');
global.timer.trigger('next') ? res.sendStatus(200) : res.sendStatus(400); global.timer.trigger('next') ? res.sendStatus(200) : res.sendStatus(400);
}; };
// Create controller for GET request to '/playback/unload' // Create controller for GET request to '/playback/unload'
// Unloads any events // Unloads any events
export const pbUnload = async (req, res) => { export const pbUnload = async (req, res) => {
console.log('Calling unload');
global.timer.trigger('unload') ? res.sendStatus(200) : res.sendStatus(400); global.timer.trigger('unload') ? res.sendStatus(200) : res.sendStatus(400);
}; };
// Create controller for GET request to '/playback/reload' // Create controller for GET request to '/playback/reload'
// Reloads current event // Reloads current event
export const pbReload = async (req, res) => { export const pbReload = async (req, res) => {
console.log('Calling reload');
global.timer.trigger('reload') ? res.sendStatus(200) : res.sendStatus(400); global.timer.trigger('reload') ? res.sendStatus(200) : res.sendStatus(400);
}; };
+1
View File
@@ -1,4 +1,5 @@
{ {
"name": "ontime-server",
"type": "module", "type": "module",
"dependencies": { "dependencies": {
"body-parser": "~1.19.0", "body-parser": "~1.19.0",
@@ -0,0 +1,11 @@
import getRandomName from '../getRandomName.js';
test('generates 500 unique names', () => {
let names = [];
for (let i = 0; i < 500; i++) {
names.push(getRandomName());
}
const unique = [...new Set(names)];
expect(names.length).toBe(unique.length);
});
File diff suppressed because it is too large Load Diff
+20 -4
View File
@@ -2,10 +2,26 @@ const mts = 1000; // millis to seconds
const mtm = 1000 * 60; // millis to minutes const mtm = 1000 * 60; // millis to minutes
const mth = 1000 * 60 * 60; // millis to hours const mth = 1000 * 60 * 60; // millis to hours
/**
* Returns current time in milliseconds
* @returns {number}
*/
export const nowInMillis = () => {
const now = new Date();
// extract milliseconds since midnight
let elapsed = now.getHours() * 3600000;
elapsed += now.getMinutes() * 60000;
elapsed += now.getSeconds() * 1000;
elapsed += now.getMilliseconds();
return elapsed;
};
/** /**
* @description Converts milliseconds to string representing time * @description Converts milliseconds to string representing time
* @param {number} ms - time in milliseconds * @param {number} ms - time in milliseconds
* @param {boolean} showSeconds - wether to show the seconds * @param {boolean} showSeconds - weather to show the seconds
* @param {string} delim - character between HH MM SS * @param {string} delim - character between HH MM SS
* @param {string} ifNull - what to return if value is null * @param {string} ifNull - what to return if value is null
* @returns {string} String representing time 00:12:02 * @returns {string} String representing time 00:12:02
@@ -17,7 +33,7 @@ export const stringFromMillis = (
delim = ':', delim = ':',
ifNull = '...' ifNull = '...'
) => { ) => {
if (ms === null || isNaN(ms)) return ifNull; if (ms == null || isNaN(ms)) return ifNull;
const isNegative = ms < 0 ? '-' : ''; const isNegative = ms < 0 ? '-' : '';
const millis = Math.abs(ms); const millis = Math.abs(ms);
@@ -36,7 +52,7 @@ export const stringFromMillis = (
/** /**
* @description Converts an excel date to milliseconds * @description Converts an excel date to milliseconds
* @argument {string} excelDate - excel string date * @argument {string} excelDate - excel string date
* @returns {number} - time in millisenconds * @returns {number} - time in milliseconds
*/ */
export const excelDateStringToMillis = (excelDate) => { export const excelDateStringToMillis = (excelDate) => {
const date = new Date(excelDate); const date = new Date(excelDate);
@@ -47,5 +63,5 @@ export const excelDateStringToMillis = (excelDate) => {
return h * mth + m * mtm + s * mts; return h * mth + m * mtm + s * mts;
} }
return null; return 0;
}; };