diff --git a/client/.eslintrc b/client/.eslintrc index 93abcf088..5a459fde9 100644 --- a/client/.eslintrc +++ b/client/.eslintrc @@ -5,6 +5,8 @@ ], "plugins": ["react", "testing-library", "jest"], "rules": { - "jest/no-mocks-import": "warn" + "jest/no-mocks-import": "warn", + "no-useless-concat": "warn", + "prefer-template": "warn" } } diff --git a/client/src/_main.scss b/client/src/_main.scss new file mode 100644 index 000000000..83da3237e --- /dev/null +++ b/client/src/_main.scss @@ -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; +} \ No newline at end of file diff --git a/client/src/app/api/ontimeApi.js b/client/src/app/api/ontimeApi.js index 1d736dfb8..91fdce1f5 100644 --- a/client/src/app/api/ontimeApi.js +++ b/client/src/app/api/ontimeApi.js @@ -75,28 +75,26 @@ export const ontimeVars = [ ]; export const getInfo = async () => { - const res = await axios.get(ontimeURL + '/info'); + const res = await axios.get(`${ontimeURL}/info`); return res.data; }; export const postInfo = async (data) => { - const res = await axios.post(ontimeURL + '/info', data); - return res; + return await axios.post(`${ontimeURL}/info`, data); }; export const getOSC = async () => { - const res = await axios.get(ontimeURL + '/osc'); + const res = await axios.get(`${ontimeURL}/osc`); return res.data; }; export const postOSC = async (data) => { - const res = await axios.post(ontimeURL + '/osc', data); - return res; + return await axios.post(`${ontimeURL}/osc`, data); }; export const downloadEvents = async () => { await axios({ - url: ontimeURL + '/db', + url: `${ontimeURL}/db`, method: 'GET', responseType: 'blob', // important }).then((response) => { @@ -123,15 +121,13 @@ export const uploadEvents = async (file) => { const formData = new FormData(); formData.append('userFile', file); // appending file await axios - .post(ontimeURL + '/db', formData, { + .post(`${ontimeURL}/db`, formData, { headers: { 'Content-Type': 'multipart/form-data', }, - }) - .then((res) => console.log(res.data)) - .catch((err) => console.error(err)); + }); }; export const uploadEventsWithPath = async (filepath) => { - await axios.post(ontimeURL + '/dbpath', { path: filepath }); + await axios.post(`${ontimeURL}/dbpath`, { path: filepath }); }; diff --git a/client/src/app/context/LoggingContext.js b/client/src/app/context/LoggingContext.js new file mode 100644 index 000000000..6f85ee39c --- /dev/null +++ b/client/src/app/context/LoggingContext.js @@ -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 ( + + {props.children} + + ) +} \ No newline at end of file diff --git a/client/src/common/components/collapseBar/CollapseBar.jsx b/client/src/common/components/collapseBar/CollapseBar.jsx new file mode 100644 index 000000000..ed0126d2c --- /dev/null +++ b/client/src/common/components/collapseBar/CollapseBar.jsx @@ -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( +
+ {title} + +
+ ) +} +CollapseBar.propTypes = { + title: PropTypes.string, + isCollapsed: PropTypes.bool, + onClick: PropTypes.func, +} \ No newline at end of file diff --git a/client/src/common/components/collapseBar/CollapseBar.module.scss b/client/src/common/components/collapseBar/CollapseBar.module.scss new file mode 100644 index 000000000..43fbb7a87 --- /dev/null +++ b/client/src/common/components/collapseBar/CollapseBar.module.scss @@ -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; +} \ No newline at end of file diff --git a/client/src/common/components/errorBoundary/ErrorBoundary.jsx b/client/src/common/components/errorBoundary/ErrorBoundary.jsx index c97221ee1..d8662eac9 100644 --- a/client/src/common/components/errorBoundary/ErrorBoundary.jsx +++ b/client/src/common/components/errorBoundary/ErrorBoundary.jsx @@ -1,6 +1,9 @@ import React from 'react'; +import { LoggingContext } from '../../../app/context/LoggingContext'; class ErrorBoundary extends React.Component { + static contextType = LoggingContext; + constructor(props) { super(props); this.state = { error: null, errorInfo: null }; @@ -17,15 +20,11 @@ class ErrorBoundary extends React.Component { errorInfo: info, }); // 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() { if (this.state.errorMessage) { - // You can render any custom fallback UI return

:/

; } return this.props.children; diff --git a/client/src/common/components/eventTimes/EventTimes.jsx b/client/src/common/components/eventTimes/EventTimes.jsx index 0b7b66fd1..cfc0d9300 100644 --- a/client/src/common/components/eventTimes/EventTimes.jsx +++ b/client/src/common/components/eventTimes/EventTimes.jsx @@ -1,23 +1,26 @@ 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) { const { actionHandler, delay, timeStart, timeEnd } = props; + const { emitWarning } = useContext(LoggingContext); const handleValidate = (entry, v) => { - // we dont inforce validation here + // we dont enforce validation here if (v == null || timeStart == null || timeEnd == null) return true; if (timeStart === 0) return true; let validate = { value: true, catch: '' }; - if (entry === 'timeStart' && v > timeEnd) + if (entry === 'timeStart' && v > timeEnd) { 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'; + } if (validate.catch !== '') - showWarningToast('Time Input Warning', validate.catch); + emitWarning(`Time Input Warning: ${validate.catch}`); return validate.value; }; diff --git a/client/src/common/components/eventTimes/EventTimesVertical.jsx b/client/src/common/components/eventTimes/EventTimesVertical.jsx index 3e0ea8357..7a796c294 100644 --- a/client/src/common/components/eventTimes/EventTimesVertical.jsx +++ b/client/src/common/components/eventTimes/EventTimesVertical.jsx @@ -1,6 +1,7 @@ import EditableTimer from 'common/input/EditableTimer'; -import { showWarningToast } from 'common/helpers/toastManager'; -import { stringFromMillis } from 'common/utils/dateConfig'; +import { stringFromMillis } from 'ontime-server/utils/time'; +import { useContext } from 'react'; +import { LoggingContext } from '../../../app/context/LoggingContext'; const label = { fontSize: '0.75em', @@ -83,6 +84,8 @@ const Times = (props) => { export default function EventTimesVertical(props) { const { delay, timeStart, timeEnd, duration } = props; + const { emitWarning } = useContext(LoggingContext); + const handleValidate = (entry, v) => { // we dont enforce validation here @@ -90,32 +93,36 @@ export default function EventTimesVertical(props) { if (timeStart === 0) return true; let validate = { value: true, catch: '' }; - if (entry === 'timeStart' && v > timeEnd) + if (entry === 'timeStart' && v > timeEnd) { 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'; + } - if (validate.catch !== '') - showWarningToast('Time Input Warning', validate.catch); + if (validate.catch !== '') { + emitWarning(`Time Input Warning: ${validate.catch}`); + } return validate.value; }; - return (delay != null) & (delay > 0) ? ( - - ) : ( - - ); + return ( + (delay != null) && (delay > 0) ? ( + + ) : ( + + ) + ) } diff --git a/client/src/common/components/views/TodayItem.jsx b/client/src/common/components/views/TodayItem.jsx index 73da1d3f1..5a7ca28df 100644 --- a/client/src/common/components/views/TodayItem.jsx +++ b/client/src/common/components/views/TodayItem.jsx @@ -1,4 +1,4 @@ -import { stringFromMillis } from 'common/utils/dateConfig'; +import { stringFromMillis } from 'ontime-server/utils/time'; import style from './Paginator.module.css'; export default function TodayItem(props) { const { selected, timeStart, timeEnd, title, backstageEvent } = props; @@ -19,7 +19,7 @@ export default function TodayItem(props) { }`} >{`${start} ยท ${end}`}
{title}
- {backstageEvent &&
} + {backstageEvent &&
}
); } diff --git a/client/src/common/helpers/toastManager.jsx b/client/src/common/helpers/toastManager.jsx deleted file mode 100644 index 1f686a8ac..000000000 --- a/client/src/common/helpers/toastManager.jsx +++ /dev/null @@ -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, - }); -}; diff --git a/client/src/common/input/EditableTimer.jsx b/client/src/common/input/EditableTimer.jsx index d3962c59c..c7f6c5c14 100644 --- a/client/src/common/input/EditableTimer.jsx +++ b/client/src/common/input/EditableTimer.jsx @@ -1,15 +1,16 @@ import { Editable, EditableInput, EditablePreview } from '@chakra-ui/editable'; -import { useEffect, useState } from 'react'; +import { useContext, useEffect, useState } from 'react'; import { isTimeString, - stringFromMillis, timeStringToMillis, } from '../utils/dateConfig'; -import { showErrorToast } from '../helpers/toastManager'; +import { stringFromMillis } from 'ontime-server/utils/time'; import style from './EditableTimer.module.css'; +import { LoggingContext } from '../../app/context/LoggingContext'; export default function EditableTimer(props) { const { name, actionHandler, time, delay, validate } = props; + const { emitError } = useContext(LoggingContext); const [value, setValue] = useState(''); // prepare time fields @@ -18,9 +19,9 @@ export default function EditableTimer(props) { try { setValue(stringFromMillis(time + delay)); } catch (error) { - showErrorToast('Error parsing date', error.text); + emitError(`Unable to parse date: ${error.text}`); } - }, [time, delay]); + }, [time, delay, emitError]); const validateValue = (value) => { const success = handleSubmit(value); diff --git a/client/src/common/utils/dateConfig.js b/client/src/common/utils/dateConfig.js index 3bb3af543..171729cd8 100644 --- a/client/src/common/utils/dateConfig.js +++ b/client/src/common/utils/dateConfig.js @@ -4,37 +4,7 @@ export const timeFormatSeconds = 'HH:mm:ss'; const mts = 1000; // millis to seconds const mtm = 1000 * 60; // millis to minutes 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) @@ -79,7 +49,7 @@ export const millisToMinutes = (millis) => { }; /** - * @description Converts timestring to milliseconds + * @description Converts timestring to milliseconds * @param {string} string - time string "23:00:12" * @returns {number} Amount in milliseconds */ diff --git a/client/src/common/utils/eventsManager.js b/client/src/common/utils/eventsManager.js index b00db9df4..5e861e9ff 100644 --- a/client/src/common/utils/eventsManager.js +++ b/client/src/common/utils/eventsManager.js @@ -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 * @param {Object[]} events - given events * @returns {Object[]} Filtered events with calculated delays */ -import {stringFromMillis} from "./dateConfig"; - export const getEventsWithDelay = (events) => { if (events == null) return []; diff --git a/client/src/features/control/PlaybackTimer.jsx b/client/src/features/control/PlaybackTimer.jsx index 5aad78e3d..635646ecb 100644 --- a/client/src/features/control/PlaybackTimer.jsx +++ b/client/src/features/control/PlaybackTimer.jsx @@ -1,6 +1,6 @@ import style from './PlaybackControl.module.scss'; 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 {Button} from '@chakra-ui/button'; import {memo} from 'react'; diff --git a/client/src/features/editors/Editor.jsx b/client/src/features/editors/Editor.jsx index 27047811a..8ce7224f4 100644 --- a/client/src/features/editors/Editor.jsx +++ b/client/src/features/editors/Editor.jsx @@ -1,10 +1,11 @@ import { lazy, useEffect } from 'react'; import { Box } from '@chakra-ui/layout'; 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 ModalManager from 'features/modals/ModalManager'; import ErrorBoundary from 'common/components/errorBoundary/ErrorBoundary'; +import { LoggingProvider } from '../../app/context/LoggingContext'; const EventListWrapper = lazy(() => import('features/editors/list/EventListWrapper') @@ -22,7 +23,7 @@ export default function Editor() { }, []); return ( - <> +
@@ -68,6 +69,6 @@ export default function Editor() {
- +
); } diff --git a/client/src/features/editors/Editor.module.css b/client/src/features/editors/Editor.module.scss similarity index 91% rename from client/src/features/editors/Editor.module.css rename to client/src/features/editors/Editor.module.scss index ec991e8ed..51c39b1ed 100644 --- a/client/src/features/editors/Editor.module.css +++ b/client/src/features/editors/Editor.module.scss @@ -8,7 +8,7 @@ display: grid; grid-template-rows: auto 1fr; - grid-template-columns: 40px 48em auto auto; + grid-template-columns: 40px 48em 31em auto; grid-template-areas: 'sett even play info' 'sett even mess info'; @@ -110,12 +110,23 @@ h1 { .editor { grid-area: even; + + .content { + height: calc(100% - 3em); + overflow: hidden; + } } .info { grid-area: info; min-width: 17em; - max-width: 32em; + + .content { + display: flex; + flex-direction: column; + height: calc(100% - 3em); + overflow: hidden; + } } .messages { diff --git a/client/src/features/editors/list/EventList.jsx b/client/src/features/editors/list/EventList.jsx index 9e901f729..8c535ddb8 100644 --- a/client/src/features/editors/list/EventList.jsx +++ b/client/src/features/editors/list/EventList.jsx @@ -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 { useSocket } from 'app/context/socketContext'; import Empty from 'common/state/Empty'; diff --git a/client/src/features/editors/list/EventListItem.jsx b/client/src/features/editors/list/EventListItem.jsx index df8ad7d44..f4c5d5a98 100644 --- a/client/src/features/editors/list/EventListItem.jsx +++ b/client/src/features/editors/list/EventListItem.jsx @@ -1,8 +1,8 @@ import DelayBlock from './DelayBlock'; import BlockBlock from './BlockBlock'; import EventBlock from './EventBlock'; -import { showErrorToast } from 'common/helpers/toastManager'; -import { memo } from 'react'; +import { memo, useContext } from 'react'; +import { LoggingContext } from '../../../app/context/LoggingContext'; const areEqual = (prevProps, nextProps) => { return ( @@ -26,6 +26,7 @@ const EventListItem = (props) => { delay, ...rest } = props; + const { emitError } = useContext(LoggingContext); // Create / delete new events const actionHandler = (action, payload) => { @@ -59,7 +60,7 @@ const EventListItem = (props) => { // request update in parent eventsHandler('patch', newData); } else { - showErrorToast('Field Error: ' + field); + emitError(`Unknown field: ${field}`); } break; default: diff --git a/client/src/features/editors/list/EventListWrapper.jsx b/client/src/features/editors/list/EventListWrapper.jsx index 86befdc66..562e563e7 100644 --- a/client/src/features/editors/list/EventListWrapper.jsx +++ b/client/src/features/editors/list/EventListWrapper.jsx @@ -1,5 +1,5 @@ import { useMutation, useQueryClient } from 'react-query'; -import { useCallback, useEffect, useState } from 'react'; +import { useCallback, useContext, useEffect, useState } from 'react'; import { fetchAllEvents, requestPatch, @@ -12,16 +12,17 @@ import { } from 'app/api/eventsApi.js'; import EventList from './EventList'; import EventListMenu from 'features/menu/EventListMenu.jsx'; -import { showErrorToast } from 'common/helpers/toastManager'; import { useFetch } from 'app/hooks/useFetch.js'; import Empty from 'common/state/Empty'; import { EVENTS_TABLE } from 'app/api/apiConstants'; import { BatchOperation } from 'app/context/collapseAtom'; import { useAtom } from 'jotai'; +import { LoggingContext } from '../../../app/context/LoggingContext'; export default function EventListWrapper() { const [, setCollapsed] = useAtom(BatchOperation); const queryClient = useQueryClient(); + const { emitError } = useContext(LoggingContext); const { data, status, isError, refetch } = useFetch( EVENTS_TABLE, fetchAllEvents @@ -230,9 +231,9 @@ export default function EventListWrapper() { // Show toasts on errors useEffect(() => { if (isError) { - showErrorToast('Error fetching data'); + emitError('Error fetching data'); } - }, [isError]); + }, [emitError, isError]); // Events API const eventsHandler = useCallback( @@ -242,35 +243,35 @@ export default function EventListWrapper() { try { await addEvent.mutateAsync(payload); } catch (error) { - showErrorToast('Error creating event', error.message); + emitError(`Error fetching data: ${error.message}`); } break; case 'update': try { await updateEvent.mutateAsync(payload); } catch (error) { - showErrorToast('Error updating event', error.message); + emitError(`Error updating event: ${error.message}`); } break; case 'patch': try { await patchEvent.mutateAsync(payload); } catch (error) { - showErrorToast('Error updating event', error.message); + emitError(`Error updating event: ${error.message}`); } break; case 'delete': try { await deleteEvent.mutateAsync(payload); } catch (error) { - showErrorToast('Error deleting event', error.message); + emitError(`Error deleting event: ${error.message}`); } break; case 'reorder': try { await reorderEvent.mutateAsync(payload); } catch (error) { - showErrorToast('Error reordering event', error.message); + emitError(`Error re-ordering event: ${error.message}`); } break; case 'applyDelay': @@ -293,13 +294,13 @@ export default function EventListWrapper() { // delete block after, if any if (blockAfter) await deleteEvent.mutateAsync(blockAfter); } catch (error) { - showErrorToast('Error applying delay', error.message); + emitError(`Error applying delay: ${error.message}`); } } else { try { await applyDelay.mutateAsync(payload.id); } catch (error) { - showErrorToast('Error applying delay', error.message); + emitError(`Error applying delay: ${error.message}`); } } break; @@ -317,11 +318,11 @@ export default function EventListWrapper() { try { await deleteAllEvents.mutateAsync(); } catch (error) { - showErrorToast('Error deleting events', error.message); + emitError(`Error deleting events: ${error.message}`); } break; default: - showErrorToast('Unrecognised request', action); + emitError(`Unhandled request: ${action}`); break; } }, diff --git a/client/src/features/editors/list/List.module.css b/client/src/features/editors/list/List.module.scss similarity index 96% rename from client/src/features/editors/list/List.module.css rename to client/src/features/editors/list/List.module.scss index 6670524f3..417dea6b8 100644 --- a/client/src/features/editors/list/List.module.css +++ b/client/src/features/editors/list/List.module.scss @@ -7,7 +7,7 @@ border-radius: 4px; padding: 8px; overflow-y: scroll; - height: 73vh; + height: 100%; } .list { diff --git a/client/src/features/info/Info.jsx b/client/src/features/info/Info.jsx index a519b3e97..87cac9397 100644 --- a/client/src/features/info/Info.jsx +++ b/client/src/features/info/Info.jsx @@ -15,11 +15,10 @@ export default function Info() { titleNext: '', subtitleNext: '', presenterNext: '', - noteNext: '', + noteNext: '' }); const [selected, setSelected] = useState('No events'); const [playback, setPlayback] = useState(null); - const logData = []; // handle incoming messages useEffect(() => { @@ -61,20 +60,19 @@ export default function Info() { }; }, [socket]); - // TODO: Put this in use effect // prepare data const titlesNow = { title: titles.titleNow, subtitle: titles.subtitleNow, presenter: titles.presenterNow, - note: titles.noteNow, + note: titles.noteNow }; const titlesNext = { title: titles.titleNext, subtitle: titles.subtitleNext, presenter: titles.presenterNext, - note: titles.noteNext, + note: titles.noteNext }; return ( @@ -83,10 +81,10 @@ export default function Info() { {`Running on port 4001`} {selected} - {/* */} + ); } diff --git a/client/src/features/info/Info.module.scss b/client/src/features/info/Info.module.scss index 318ead39e..6d5d2e3da 100644 --- a/client/src/features/info/Info.module.scss +++ b/client/src/features/info/Info.module.scss @@ -1,4 +1,6 @@ -.container { +@use '../../main' as *; + +@mixin container { margin-top: 1em; display: flex; flex-direction: column; @@ -8,9 +10,13 @@ padding: 8px; } +.container { + @include container; +} + .main { font-size: 0.9em; - color: #ff7597; + color: $ontime-pink; display: flex; justify-content: space-between; } @@ -20,17 +26,17 @@ padding: 0; margin: 0; font-size: 0.9em; - color: #ccc; + color: $header-gray; display: flex; justify-content: space-between; } .header { - color: #ccc; + color: $header-gray; } .headerRoll { - color: #2b6cb0; + color: $ontime-roll; } .collapsedTitle { @@ -57,7 +63,7 @@ .label { font-size: 0.9em; - color: #aaa; + color: $label-gray; } .label::after { @@ -70,43 +76,22 @@ } .notes { - color: #d69e2e; + color: $notes-color; overflow: hidden; text-overflow: ellipsis; } .if { font-size: 0.8em; - color: #4bffabcc; - background-color: rgba(0, 0, 0, 0.13); - border-radius: 2px; - padding: 0 0.5em; - margin: 0 0.5em; + color: $ontime-accent; + @include container-bg; } -.log { - overflow-y: scroll; - height: 30vh; - - ul > li { - font-size: 0.9em; - color: #fff; - } - -} - -.info { +ul > li { + font-size: 0.9em; color: #fff; } -.error { - color: red; -} - -.client { - color: lightblue; -} - .moreExpanded, .moreCollapsed { cursor: pointer; diff --git a/client/src/features/info/InfoLogger.jsx b/client/src/features/info/InfoLogger.jsx index 6e58dc3a2..36cd8e265 100644 --- a/client/src/features/info/InfoLogger.jsx +++ b/client/src/features/info/InfoLogger.jsx @@ -1,34 +1,117 @@ -import { Icon } from '@chakra-ui/react'; -import { useState } from 'react'; -import { FiChevronUp } from 'react-icons/fi'; -import style from './Info.module.scss'; +import { useContext, useEffect, useState } from 'react'; +import style from './InfoLogger.module.scss'; +import CollapseBar from "../../common/components/collapseBar/CollapseBar"; +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); + // 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 ( -
-
- Log - setCollapsed((c) => !c)} - /> -
+
+ setCollapsed((c) => !c)}/> {!collapsed && ( -
    -
  • 10:35:23 [PLAYBACK] Next
  • -
  • - 10:32:10 [CLIENT] New socket client (total: 3) -
  • -
  • 10:28:23 [PLAYBACK] Next
  • -
  • 10:25:23 [PLAYBACK] Play
  • -
  • 10:23:13 [SERVER] Server Reconnected
  • -
  • 10:23:10 [SERVER] Server Disconnected
  • -
+ <> +
+
setShowUser((s) => !s)} + onAuxClick={() => disableOthers('USER')} + className={(showUser) ? style.active : null}> + USER +
+
setShowClient((s) => !s)} + onAuxClick={() => disableOthers('CLIENT')} + className={(showClient) ? style.active : null}> + CLIENT +
+
setShowServer((s) => !s)} + onAuxClick={() => disableOthers('SERVER')} + className={(showServer) ? style.active : null}> + SERVER +
+
setShowPlayback((s) => !s)} + onAuxClick={() => disableOthers('PLAYBACK')} + className={(showPlayback) ? style.active : null}> + Playback +
+
setShowRx((s) => !s)} + onAuxClick={() => disableOthers('RX')} + className={(showRx) ? style.active : null}> + RX +
+
setShowTx((s) => !s)} + onAuxClick={() => disableOthers('TX')} + className={(showTx) ? style.active : null}> + TX +
+
+ Clear +
+
+
    + {data.map((d) => ( +
  • +
    {d.time}
    +
    {d.origin}
    +
    {d.text}
    +
  • + ))} +
+ )}
); diff --git a/client/src/features/info/InfoLogger.module.scss b/client/src/features/info/InfoLogger.module.scss new file mode 100644 index 000000000..51087c484 --- /dev/null +++ b/client/src/features/info/InfoLogger.module.scss @@ -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); + } +} \ No newline at end of file diff --git a/client/src/features/info/InfoNif.jsx b/client/src/features/info/InfoNif.jsx index bf563de0d..647c35176 100644 --- a/client/src/features/info/InfoNif.jsx +++ b/client/src/features/info/InfoNif.jsx @@ -1,10 +1,9 @@ -import { Icon } from '@chakra-ui/react'; import { useState } from 'react'; -import { FiChevronUp } from 'react-icons/fi'; import { APP_TABLE } from 'app/api/apiConstants'; import { getInfo, ontimePlaceholderInfo } from 'app/api/ontimeApi'; import { useFetch } from 'app/hooks/useFetch'; import style from './Info.module.scss'; +import CollapseBar from '../../common/components/collapseBar/CollapseBar'; export default function InfoNif() { const { data, status } = useFetch(APP_TABLE, getInfo, { @@ -22,16 +21,8 @@ export default function InfoNif() { }; return ( -
-
- Network Info - setCollapsed((c) => !c)} - /> -
- +
+ setCollapsed((c) => !c)}/> {!collapsed && (
{status === 'success' && ( diff --git a/client/src/features/menu/MenuBar.jsx b/client/src/features/menu/MenuBar.jsx index 4bfe0f7fd..b902bebff 100644 --- a/client/src/features/menu/MenuBar.jsx +++ b/client/src/features/menu/MenuBar.jsx @@ -9,10 +9,12 @@ import QuitIconBtn from './buttons/QuitIconBtn'; import style from './MenuBar.module.css'; import HelpIconBtn from './buttons/HelpIconBtn'; 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) { const { onOpen } = props; + const { emitError } = useContext(LoggingContext); const hiddenFileInput = useRef(null); const queryClient = useQueryClient(); const uploaddb = useMutation(uploadEvents, { @@ -34,28 +36,24 @@ export default function MenuBar(props) { const handleUpload = (event) => { const fileUploaded = event.target.files[0]; if (fileUploaded == null) return; - console.log(fileUploaded); // Limit file size to 1MB if (fileUploaded.size > 1000000) { - console.log('Error: File size limit (1MB) exceeded'); + emitError('Error: File size limit (1MB) exceeded') return; } // Check file extension - if (fileUploaded.name.endsWith('.xlsx')) { - console.log('excel file'); - } else if (fileUploaded.name.endsWith('.json')) { - console.log('json file'); - } else { - console.log('Error: File type unknown'); + if (! fileUploaded.name.endsWith('.xlsx') + || !fileUploaded.name.endsWith('.json')) { + emitError('Error: File type unknown') return; } try { uploaddb.mutate(fileUploaded); } catch (error) { - console.log(error); + emitError(`Failed uploading file: ${error}`) } // reset input value diff --git a/client/src/features/modals/AliasesModal.jsx b/client/src/features/modals/AliasesModal.jsx index 08a9cc296..8a9663ee1 100644 --- a/client/src/features/modals/AliasesModal.jsx +++ b/client/src/features/modals/AliasesModal.jsx @@ -23,6 +23,7 @@ export default function AliasesModal() { const smLink = 'http://localhost:4001/sm'; const publicLink = 'http://localhost:4001/public'; const pipLink = 'http://localhost:4001/pip'; + const studioLink = 'http://localhost:4001/studio'; return ( <> @@ -81,6 +82,17 @@ export default function AliasesModal() { {pipLink}

+

+ Studio Clock
+ + {studioLink} + +

Manage custom aliases diff --git a/client/src/features/modals/AppSettingsModal.jsx b/client/src/features/modals/AppSettingsModal.jsx index da66984b2..03ed5f975 100644 --- a/client/src/features/modals/AppSettingsModal.jsx +++ b/client/src/features/modals/AppSettingsModal.jsx @@ -1,14 +1,15 @@ import { ModalBody } from '@chakra-ui/modal'; import { FormLabel, FormControl, Input, Button } from '@chakra-ui/react'; 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 { OSC_SETTINGS } from 'app/api/apiConstants'; -import { showErrorToast } from 'common/helpers/toastManager'; import style from './Modals.module.scss'; +import { LoggingContext } from '../../app/context/LoggingContext'; export default function AppSettingsModal() { const { data, status } = useFetch(OSC_SETTINGS, getOSC); + const { emitError } = useContext(LoggingContext); const [formData, setFormData] = useState(oscPlaceholderSettings); const [changed, setChanged] = useState(false); const [submitting, setSubmitting] = useState(false); @@ -41,7 +42,7 @@ export default function AppSettingsModal() { // set fields with error if (e.status) { - showErrorToast('Invalid Input', e.message); + emitError(`Invalid Input: ${e.message}`); return; } diff --git a/client/src/features/modals/IntegrationSettingsModal.jsx b/client/src/features/modals/IntegrationSettingsModal.jsx index fd735c3bd..8f32c7a37 100644 --- a/client/src/features/modals/IntegrationSettingsModal.jsx +++ b/client/src/features/modals/IntegrationSettingsModal.jsx @@ -12,14 +12,15 @@ import { ontimeVars, postInfo, } from 'app/api/ontimeApi'; -import { useEffect, useState } from 'react'; +import { useContext, useEffect, useState } from 'react'; import { useFetch } from 'app/hooks/useFetch'; import { APP_TABLE } from 'app/api/apiConstants'; -import { showErrorToast } from 'common/helpers/toastManager'; import style from './Modals.module.scss'; +import { LoggingContext } from '../../app/context/LoggingContext'; export default function IntegrationSettingsModal() { const { data, status } = useFetch(APP_TABLE, getInfo); + const { emitError } = useContext(LoggingContext); const [formData, setFormData] = useState(httpPlaceholder); const [changed, setChanged] = useState(false); const [submitting, setSubmitting] = useState(false); @@ -51,7 +52,7 @@ export default function IntegrationSettingsModal() { // set fields with error if (e.status) { - showErrorToast('Invalid Input', e.message); + emitError(`Invalid Input: ${e.message}`); return; } diff --git a/client/src/features/viewers/ViewWrapper.jsx b/client/src/features/viewers/ViewWrapper.jsx index 7f58dea19..95f54736b 100644 --- a/client/src/features/viewers/ViewWrapper.jsx +++ b/client/src/features/viewers/ViewWrapper.jsx @@ -2,7 +2,7 @@ import { useEffect, useState } from 'react'; import { fetchAllEvents } from 'app/api/eventsApi'; import { fetchEvent } from 'app/api/eventApi'; import { useSocket } from 'app/context/socketContext'; -import { stringFromMillis } from 'common/utils/dateConfig'; +import { stringFromMillis } from 'ontime-server/utils/time'; import { useFetch } from 'app/hooks/useFetch'; import { EVENTS_TABLE, EVENT_TABLE } from 'app/api/apiConstants'; diff --git a/server/main.js b/server/main.js index 5b92123a9..42cb37b97 100644 --- a/server/main.js +++ b/server/main.js @@ -119,7 +119,7 @@ app.whenReady().then(() => { createWindow(); // register global shortcuts - // (available regardless of wheter app is in focus) + // (available regardless of whether app is in focus) // bring focus to window globalShortcut.register('Alt+1', () => { win.show(); diff --git a/server/package.json b/server/package.json index 7db4c45d7..b845aef15 100644 --- a/server/package.json +++ b/server/package.json @@ -1,6 +1,6 @@ { "name": "ontime", - "version": "0.4.7", + "version": "0.4.8", "author": "Carlos Valente", "description": "Time keeping for live events", "repository": "https://github.com/cpvalente/ontime", diff --git a/server/src/app.js b/server/src/app.js index c3028230e..fdb72b17a 100644 --- a/server/src/app.js +++ b/server/src/app.js @@ -153,7 +153,7 @@ export const startServer = async (overrideConfig = null) => { const port = 4001; // 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)); // OSC Config diff --git a/server/src/classes/EventTimer.js b/server/src/classes/EventTimer.js index 03a79b57c..59c981ba5 100644 --- a/server/src/classes/EventTimer.js +++ b/server/src/classes/EventTimer.js @@ -1,110 +1,85 @@ -import {Timer} from './Timer.js'; -import {Server} from 'socket.io'; -import {DAY_TO_MS, getSelectionByRoll, replacePlaceholder, updateRoll} from './classUtils.js'; -import {OSCIntegration} from './integrations/Osc.js'; -import {HTTPIntegration} from "./integrations/Http.js"; -import {cleanURL} from "../utils/url.js"; +import { Timer } from './Timer.js'; +import { Server } from 'socket.io'; +import { + DAY_TO_MS, + getSelectionByRoll, + replacePlaceholder, + updateRoll, +} from './classUtils.js'; +import { OSCIntegration } from './integrations/Osc.js'; +import { HTTPIntegration } from './integrations/Http.js'; +import { cleanURL } from '../utils/url.js'; +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 - * namely: - * - Presenter message, text and status - * - Public message, text and status - * + * Class EventTimer adds functions specific to APP + * @extends Timer */ export class EventTimer extends Timer { - - // Keep track of Timer lifecycle - // 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', - onLoad: 'onLoad', - armed: 'armed', - onStart: 'onStart', - onUpdate: 'onUpdate', - onPause: 'onPause', - onStop: 'onStop', - onFinish: 'onFinish', - }; - ontimeCycle = 'idle'; - prevCycle = null; - lastUpdate = null; - - // Socket IO Object - io = null; - - // OSC Object - osc = null; - - // HTTP Client Object - http = null; - - _numClients = 0; - _interval = null; - - presenter = { - text: '', - visible: false, - }; - public = { - text: '', - visible: false, - }; - lower = { - text: '', - visible: false, - }; - - titlesPublic = { - titleNow: null, - 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] + * @param {object} httpServer + * @param {object} timerConfig + * @param {object} [oscConfig] + * @param {object} [httpConfig] */ constructor(httpServer, timerConfig, oscConfig, httpConfig) { - // call super constructor super(); - // initialise class variables + this.cycleState = { + /* idle: before it is initialised */ + idle: 'idle', + /* onLoad: when a new event is loaded */ + onLoad: 'onLoad', + /* armed: when a new event is loaded but hasn't started */ + armed: 'armed', + onStart: 'onStart', + /* update: every update call cycle (1 x second) */ + onUpdate: 'onUpdate', + onPause: 'onPause', + onStop: 'onStop', + onFinish: 'onFinish', + }; + this.ontimeCycle = 'idle'; + this.prevCycle = null; + + // OSC Object + this.osc = null; + + // HTTP Client Object + this.http = null; + + this._numClients = 0; + this._interval = null; + + this.presenter = { + text: '', + visible: false, + }; + this.public = { + text: '', + visible: false, + }; + this.lower = { + text: '', + visible: false, + }; + + // call general title reset + this._resetSelection(); + this.numEvents = 0; + this._eventlist = null; + this.onAir = false; // initialise socketIO server + this.messageStack = []; + this.MAX_MESSAGES = 100; + this._clientNames = {}; this.io = new Server(httpServer, { cors: { 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 this._interval = setInterval( () => this.runCycle(), @@ -138,28 +97,65 @@ export class EventTimer extends Timer { // listen to new connections this._listenToConnections(); + + if (oscConfig != null) { + this._initOscClient(oscConfig); + } + + if (httpConfig != null) { + this._initHTTPClient(httpConfig); + } } /** * @description Shutdown process */ shutdown() { - console.log('Shutting down integrations') - console.log('... Closing socket server'); + this.info('SERVER', 'Shutting down ontime'); + this.info('TX', '... Closing socket server'); this.io.close(); - console.log('... Closing osc server'); + this.info('TX', '... Closing OSC Client'); 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() { // through websockets - this.io.emit('timer', this.getTimes()); + this.io.emit('timer', this.getTimeObject()); } - // broadcast state - broadcastState(update = true) { - this.io.emit('timer', this.getTimes(update)); + /** + * Broadcasts complete object state + */ + broadcastState() { + this.broadcastTimer(); this.io.emit('playstate', this.state); this.io.emit('selected', { id: this.selectedEventId, @@ -168,6 +164,7 @@ export class EventTimer extends Timer { }); this.io.emit('selected-id', this.selectedEventId); this.io.emit('next-id', this.nextEventId); + this.io.emit('numevents', this.numEvents); this.io.emit('publicselected-id', this.selectedPublicEventId); this.io.emit('publicnext-id', this.nextPublicEventId); this.io.emit('titles', this.titles); @@ -175,7 +172,11 @@ export class EventTimer extends Timer { 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) { this.io.emit(address, payload); } @@ -192,69 +193,73 @@ export class EventTimer extends Timer { case 'start': if (this.numEvents === 0 || this.numEvents == null) return false; // Call action and force update + this.info('PLAYBACK', 'Play Mode Start'); this.start(); - this.runCycle(); break; case 'pause': if (this.numEvents === 0 || this.numEvents == null) return false; // Call action and force update + this.info('PLAYBACK', 'Play Mode Pause'); this.pause(); - this.runCycle(); break; case 'stop': if (this.numEvents === 0 || this.numEvents == null) return false; // Call action and force update + this.info('PLAYBACK', 'Play Mode Stop'); this.stop(); - this.runCycle(); break; case 'roll': if (this.numEvents === 0 || this.numEvents == null) return false; // Call action and force update + this.info('PLAYBACK', 'Play Mode Roll'); this.roll(); - this.runCycle(); break; case 'previous': if (this.numEvents === 0 || this.numEvents == null) return false; // Call action and force update + this.info('PLAYBACK', 'Play Mode Previous'); this.previous(); - this.runCycle(); break; case 'next': if (this.numEvents === 0 || this.numEvents == null) return false; // Call action and force update + this.info('PLAYBACK', 'Play Mode Next'); this.next(); - this.runCycle(); break; case 'unload': if (this.numEvents === 0 || this.numEvents == null) return false; // Call action and force update + this.info('PLAYBACK', 'Events unloaded'); this.unload(); - this.runCycle(); break; case 'reload': if (this.numEvents === 0 || this.numEvents == null) return false; // Call action and force update + this.info('PLAYBACK', 'Reloaded event'); this.reload(); - this.runCycle(); break; case 'onAir': // Call action + this.info('PLAYBACK', 'Going On Air'); this.setonAir(true); break; case 'offAir': // Call action and force update + this.info('PLAYBACK', 'Going Off Air'); this.setonAir(false); break; default: // Error, disable flag - console.log('ERROR: Unhandled action triggered') + this.error('RX', `Unhandled action triggered ${action}`); reply = false; break; } + + // update state + this.runCycle(); return reply; } - /** * @description State machine checks what actions need to * happen at every app cycle @@ -264,18 +269,19 @@ export class EventTimer extends Timer { let httpMessage = null; switch (this.ontimeCycle) { - case "idle": + case 'idle': break; - case "armed": + case 'armed': // if we come from roll, see if we can start if (this.state === 'roll') { this.update(); } break; - case "onLoad": + case 'onLoad': // broadcast change this.broadcastState(); + // Todo: wrap in reusable function // check integrations - http if (h?.onLoad?.enabled) { if (h?.onLoad?.url != null || h?.onLoad?.url !== '') { @@ -286,13 +292,13 @@ export class EventTimer extends Timer { // update lifecycle: armed this.ontimeCycle = this.cycleState.armed; break; - case "onStart": + case 'onStart': // broadcast current state this.broadcastState(); // send OSC if there is something running // _finish at is only set when an event is loaded if (this._finishAt > 0) { - this.osc.send(this.osc.implemented.play); + this.sendOsc(this.osc.implemented.play); } // check integrations - http @@ -305,7 +311,7 @@ export class EventTimer extends Timer { // update lifecycle: onUpdate this.ontimeCycle = this.cycleState.onUpdate; break; - case "onUpdate": + case 'onUpdate': // call update this.update(); // broadcast current state @@ -313,9 +319,15 @@ export class EventTimer extends Timer { // through OSC, only if running if (this.state === 'start' || this.state === 'roll') { if (this.current != null && this.secondaryTimer == null) { - this.osc.send(this.osc.implemented.time, this.timeTag); - this.osc.send(this.osc.implemented.overtime, this.current > 0 ? 0 : 1); - this.osc.send(this.osc.implemented.title, this.titles?.titleNow || ''); + this.sendOsc(this.osc.implemented.time, this.timeTag); + this.sendOsc( + this.osc.implemented.overtime, + this.current > 0 ? 0 : 1 + ); + this.sendOsc( + this.osc.implemented.title, + this.titles?.titleNow || '' + ); } } @@ -327,11 +339,11 @@ export class EventTimer extends Timer { } break; - case "onPause": + case 'onPause': // broadcast current state this.broadcastState(); // send OSC - this.osc.send(this.osc.implemented.pause); + this.sendOsc(this.osc.implemented.pause); // check integrations - http if (h?.onLoad?.enabled) { @@ -344,13 +356,13 @@ export class EventTimer extends Timer { this.ontimeCycle = this.cycleState.armed; break; - case "onStop": + case 'onStop': // broadcast change this.broadcastState(); // send OSC if something was actually stopped if (this.prevCycle === this.cycleState.onUpdate) { - this.osc.send(this.osc.implemented.stop); + this.sendOsc(this.osc.implemented.stop); } // check integrations - http @@ -363,12 +375,11 @@ export class EventTimer extends Timer { // update lifecycle: idle this.ontimeCycle = this.cycleState.idle; break; - case "onFinish": - console.log('onFinish') + case 'onFinish': // broadcast change - this.broadcastState(false); + this.broadcastState(); // finished an event - this.osc.send(this.osc.implemented.finished); + this.sendOsc(this.osc.implemented.finished); // check integrations - http if (h?.onLoad?.enabled) { @@ -381,20 +392,20 @@ export class EventTimer extends Timer { this.ontimeCycle = this.cycleState.onUpdate; break; default: - console.log(`ERROR: Unhandled cycle: ${this.ontimeCycle}`) + this.error('SERVER', `Unhandled cycle: ${this.ontimeCycle}`); } // send http message if any if (httpMessage != null) { const v = { - '$timer': this.timeTag, - '$title': this.titles.titleNow, - '$presenter': this.titles.presenterNow, - '$subtitle': this.titles.subtitleNow, + $timer: this.timeTag, + $title: this.titles.titleNow, + $presenter: this.titles.presenterNow, + $subtitle: this.titles.subtitleNow, '$next-title': this.titles.titleNext, '$next-presenter': this.titles.presenterNext, '$next-subtitle': this.titles.subtitleNext, - } + }; const m = cleanURL(replacePlaceholder(httpMessage, v)); this.http.send(m); } @@ -407,7 +418,6 @@ export class EventTimer extends Timer { } update() { - // if there is nothing selected, update clock const now = this._getCurrentTime(); @@ -444,19 +454,23 @@ export class EventTimer extends Timer { this.runCycle(); } - // only implement roll here, rest implemented in super + // only implement roll here, rest implemented in super if (this.state === 'roll') { const u = { selectedEventId: this.selectedEventId, current: this.current, // safeguard on midnight rollover - _finishAt: this._finishAt >= this._startedAt ? this._finishAt : this._finishAt + DAY_TO_MS, + _finishAt: + this._finishAt >= this._startedAt + ? this._finishAt + : this._finishAt + DAY_TO_MS, clock: this.clock, secondaryTimer: this.secondaryTimer, _secondaryTarget: this._secondaryTarget, - } + }; - const {updatedTimer, updatedSecondaryTimer, doRollLoad, isFinished} = updateRoll(u); + const { updatedTimer, updatedSecondaryTimer, doRollLoad, isFinished } = + updateRoll(u); this.current = updatedTimer; this.secondaryTimer = updatedSecondaryTimer; @@ -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) { /*******************************************/ // Presenter message @@ -513,6 +533,10 @@ export class EventTimer extends Timer { } } + /** + * Handle socket io connections + * @private + */ _listenToConnections() { this.io.on('connection', (socket) => { /*******************************/ @@ -521,12 +545,14 @@ export class EventTimer extends Timer { /*******************************/ // keep track of connections this._numClients++; - console.log( - `EventTimer: ${this._numClients} Clients with new connection: ${socket.id}` - ); + this._clientNames[socket.id] = getRandomName(); + const m = `${this._numClients} Clients with new connection: ${ + this._clientNames[socket.id] + }`; + this.info('CLIENT', m); // send state - socket.emit('timer', this.getTimes()); + socket.emit('timer', this.getTimeObject()); socket.emit('playstate', this.state); socket.emit('selected-id', this.selectedEventId); socket.emit('next-id', this.nextEventId); @@ -539,9 +565,11 @@ export class EventTimer extends Timer { /********************************/ socket.on('disconnect', () => { this._numClients--; - console.log( - `EventTimer: Client disconnected, total now: ${this._numClients}` - ); + const m = `${this._numClients} Clients with disconnection: ${ + 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 socket.on('get-state', () => { - socket.emit('timer', this.getTimes()); + socket.emit('timer', this.getTimeObject()); socket.emit('playstate', this.state); socket.emit('selected-id', this.selectedEventId); socket.emit('next-id', this.nextEventId); @@ -567,7 +595,7 @@ export class EventTimer extends Timer { }); socket.on('get-timer', () => { - socket.emit('timer', this.getTimes()); + socket.emit('timer', this.getTimeObject()); }); socket.on('increment-timer', (data) => { @@ -651,11 +679,11 @@ export class EventTimer extends Timer { // Presenter message socket.on('set-presenter-text', (data) => { - this._setterManager('set-presenter-text', data); + this._setTitles('set-presenter-text', data); }); socket.on('set-presenter-visible', (data) => { - this._setterManager('set-presenter-visible', data); + this._setTitles('set-presenter-visible', data); }); socket.on('get-presenter', () => { @@ -664,11 +692,11 @@ export class EventTimer extends Timer { /*******************************************/ // Public message socket.on('set-public-text', (data) => { - this._setterManager('set-public-text', data); + this._setTitles('set-public-text', data); }); socket.on('set-public-visible', (data) => { - this._setterManager('set-public-visible', data); + this._setTitles('set-public-visible', data); }); socket.on('get-public', () => { @@ -678,11 +706,11 @@ export class EventTimer extends Timer { /*******************************************/ // Lower third message socket.on('set-lower-text', (data) => { - this._setterManager('set-lower-text', data); + this._setTitles('set-lower-text', data); }); socket.on('set-lower-visible', (data) => { - this._setterManager('set-lower-visible', data); + this._setTitles('set-lower-visible', data); }); socket.on('get-lower', () => { @@ -691,6 +719,9 @@ export class EventTimer extends Timer { }); } + /** + * Deletes running event list from object + */ clearEventList() { // unload events this.unload(); @@ -706,6 +737,10 @@ export class EventTimer extends Timer { this.broadcastThis('numevents', this.numEvents); } + /** + * Adds an event list to object + * @param {array} eventlist + */ setupWithEventList(eventlist) { if (!Array.isArray(eventlist) || eventlist.length < 1) return; @@ -717,19 +752,23 @@ export class EventTimer extends Timer { this._eventlist = events; this.numEvents = numEvents; - // list may be empty + // list may contain no events if (numEvents < 1) return; // load first event this.loadEvent(0); // update clients - this.broadcastThis('numevents', this.numEvents); + this.broadcastState(); // run cycle this.runCycle(); } + /** + * Updates event list in object + * @param {array} eventlist + */ updateEventList(eventlist) { // filter only events const events = eventlist.filter((e) => e.type === 'event'); @@ -771,12 +810,17 @@ export class EventTimer extends Timer { } // update clients - this.broadcastThis('numevents', this.numEvents); + this.broadcastState(); // run cycle this.runCycle(); } + /** + * Updates a single id in the object list + * @param {string} id + * @param {object} entry - new event object + */ updateSingleEvent(id, entry) { // find object in events const eventIndex = this._eventlist.findIndex((e) => e.id === id); @@ -810,16 +854,20 @@ export class EventTimer extends Timer { this._loadTitlesNow(); } } catch (error) { - console.log(error); + this.error('SERVER', error); } // update clients - this.broadcastThis('numevents', this.numEvents); + this.broadcastState(); // run cycle this.runCycle(); } + /** + * Deleted an event from the list by its id + * @param {string} eventId + */ deleteId(eventId) { // find object in events const eventIndex = this._eventlist.findIndex((e) => e.id === eventId); @@ -848,7 +896,7 @@ export class EventTimer extends Timer { } // update clients - this.broadcastThis('numevents', this.numEvents); + this.broadcastState(); // run cycle this.runCycle(); @@ -856,34 +904,35 @@ export class EventTimer extends Timer { /** * @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) { const eventIndex = this._eventlist.findIndex((e) => e.id === eventId); if (eventIndex === -1) return; this.pause(); - this.loadEvent(eventIndex, 'load', true); + this.loadEvent(eventIndex, 'load'); // run cycle this.runCycle(); } /** * @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) { if (eventIndex === -1 || eventIndex > this.numEvents) return; this.pause(); - this.loadEvent(eventIndex, 'load', true); + this.loadEvent(eventIndex, 'load'); // run cycle this.runCycle(); } - // Loads a given event - // load timers - // load selectedEventIndex - // load titles + /** + * Loads a given event by index + * @param {object} eventIndex + * @param {string} [type='load'] - 'load' or 'reload', whether we are keeping running time + */ loadEvent(eventIndex, type = 'load') { const e = this._eventlist[eventIndex]; if (e == null) return; @@ -894,7 +943,6 @@ export class EventTimer extends Timer { if (end < start) end += DAY_TO_MS; // time stuff changes on whether we keep the running clock - if (type === 'load') { this._resetTimers(); @@ -1092,75 +1140,6 @@ export class EventTimer extends Timer { 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 * @param {boolean} onAir - whether flag is active @@ -1241,7 +1220,7 @@ export class EventTimer extends Timer { // nothing to play, unload if (nowIndex === null && nextIndex === null) { this.unload(); - console.log('Roll: no events found'); + this.warning('SERVER', 'Roll: no events found'); return; } @@ -1267,8 +1246,9 @@ export class EventTimer extends Timer { // Set running timers if (nowIndex === null) { // only warn the first time - if (this.secondaryTimer === null) - console.log('Roll: waiting for event start'); + if (this.secondaryTimer === null) { + this.info('SERVER', 'Roll: waiting for event start'); + } // reset running timer // ??? should this not have been reset? @@ -1319,9 +1299,6 @@ export class EventTimer extends Timer { // load into event this.rollLoad(); - - // broadcast change - this.broadcastState(); } previous() { @@ -1338,7 +1315,7 @@ export class EventTimer extends Timer { } // send OSC - this.osc.send(this.osc.implemented.previous); + this.sendOsc(this.osc.implemented.previous); // change playstate this.pause(); @@ -1364,7 +1341,7 @@ export class EventTimer extends Timer { } // send OSC - this.osc.send(this.osc.implemented.next); + this.sendOsc(this.osc.implemented.next); // change playstate this.pause(); @@ -1399,9 +1376,94 @@ export class EventTimer extends Timer { this.pause(); // send OSC - this.osc.send(this.osc.implemented.reload); + this.sendOsc(this.osc.implemented.reload); // reload data 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); + } + } } diff --git a/server/src/classes/Timer.js b/server/src/classes/Timer.js index 485e35b99..820610d67 100644 --- a/server/src/classes/Timer.js +++ b/server/src/classes/Timer.js @@ -7,22 +7,11 @@ import { stringFromMillis } from '../utils/time.js'; export class Timer { - clock = null; - duration = null; - current = null; - timeTag = null; - secondaryTimer = null; - _secondaryTarget = null; - _finishAt = null; - _finishedAt = null; - _finishedFlag = false; - _startedAt = null; - _pausedAt = null; - _pausedInterval = null; - _pausedTotal = null; - state = 'stop'; - - constructor() {} + constructor() { + this.clock = null; + this._resetTimers(true); + this.state = 'stop'; + } // call setup separately setupWithSeconds(seconds, autoStart = false) { @@ -74,11 +63,11 @@ export class Timer { if (this._startedAt != null) { // update current timer this.current = - this._startedAt - + this.duration - + this._pausedTotal - + this._pausedInterval - - now; + this._startedAt + + this.duration + + this._pausedTotal + + this._pausedInterval - + now; } // enable flag @@ -92,13 +81,14 @@ export class Timer { if (checkFinish) { // is event finished? const isTimeOver = this.current <= 0; - const isUpdating = (this.state !== 'pause'); + const isUpdating = this.state !== 'pause'; if (isTimeOver && isUpdating && this._finishedAt == null) { if (this._finishedAt === null) this._finishedAt = now; this._finishedFlag = true; } } + this.timeTag = stringFromMillis(this.current); } // helpers @@ -136,6 +126,7 @@ export class Timer { _resetTimers(total = false) { if (total) this.duration = null; this.current = this.duration; + this.timeTag = null; this.running = null; this.secondaryTimer = null; this._secondaryTarget = null; @@ -153,14 +144,11 @@ export class Timer { return this.duration - this.current; } - // get time object - getTimes(update = true) { - // update timer - if (update) this.update(); - - // update timetag - this.timeTag = stringFromMillis(this.current); - + /** + * Builds time object + * @returns {{running: number, secondary: number, expectedFinish: number, durationSeconds: number, startedAt: null, clock: null}} + */ + getTimeObject() { return { clock: this.clock, running: Timer.toSeconds(this.current), diff --git a/server/src/classes/classUtils.js b/server/src/classes/classUtils.js index 8465fa03a..13b5857ee 100644 --- a/server/src/classes/classUtils.js +++ b/server/src/classes/classUtils.js @@ -1,4 +1,3 @@ - /** * Utility variable: 24 hour in milliseconds . * @type {number} @@ -11,7 +10,8 @@ export const DAY_TO_MS = 86400000; * @param {number} end - When does the event end * @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 @@ -36,7 +36,6 @@ export const sortArrayByProperty = (arr, property) => { export const replacePlaceholder = (str, values) => { for (let [k, v] of Object.entries(values)) { str = str.replace(k, v); - console.log(k, v); } return str; }; @@ -72,7 +71,10 @@ export const getSelectionByRoll = (arr, now) => { // exit early if we are past the events const lastEvent = orderedEvents[orderedEvents.length - 1]; - const lastNormalEnd = normaliseEndTime(lastEvent.timeStart, lastEvent.timeEnd); + const lastNormalEnd = normaliseEndTime( + lastEvent.timeStart, + lastEvent.timeEnd + ); if (now > lastNormalEnd) { return { nowIndex, @@ -128,7 +130,7 @@ export const getSelectionByRoll = (arr, now) => { // check how far the start is from now const wait = e.timeStart - now; - if (nextIndex === null || wait < timeToNext) { + if (nextIndex == null || wait < timeToNext) { timeToNext = wait; nextIndex = arr.findIndex((a) => a.id === e.id); } @@ -161,8 +163,14 @@ export const getSelectionByRoll = (arr, now) => { * @returns {object} object with selection variables */ export const updateRoll = (currentTimers) => { - - const {selectedEventId,current,_finishAt,clock,secondaryTimer,_secondaryTarget} = currentTimers; + const { + selectedEventId, + current, + _finishAt, + clock, + secondaryTimer, + _secondaryTarget, + } = currentTimers; // timers let updatedTimer = current; @@ -181,8 +189,6 @@ export const updateRoll = (currentTimers) => { if (updatedTimer < 0) { isFinished = true; } - console.log(updatedTimer, isFinished, _finishAt) - } else if (secondaryTimer >= 0) { // if secondaryTimer is running we are in waiting to roll @@ -202,6 +208,5 @@ export const updateRoll = (currentTimers) => { doRollLoad = true; } - return {updatedTimer, updatedSecondaryTimer, doRollLoad, isFinished}; -} - + return { updatedTimer, updatedSecondaryTimer, doRollLoad, isFinished }; +}; diff --git a/server/src/classes/integrations/Osc.js b/server/src/classes/integrations/Osc.js index 8c78250ff..0752700f2 100644 --- a/server/src/classes/integrations/Osc.js +++ b/server/src/classes/integrations/Osc.js @@ -1,12 +1,10 @@ /** Class contains logic towards outgoing OSC communications. */ -import {Client, Message} from 'node-osc'; +import { Client, Message } from 'node-osc'; export class OSCIntegration { - - ADDRESS = '/ontime'; - constructor() { // OSC Client + this.ADDRESS = '/ontime'; this.oscClient = null; } @@ -26,8 +24,8 @@ export class OSCIntegration { time: 'time', overtime: 'overtime', title: 'title', - presenter:'presenter', - } + presenter: 'presenter', + }; } /** @@ -37,13 +35,19 @@ export class OSCIntegration { * @param {number} oscConfig.port - OSC Destination Port */ init(oscConfig) { - const {ip, port} = oscConfig; + const { ip, port } = oscConfig; try { 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) { 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 */ async send(messageType, payload) { + const reply = { + success: true, + message: 'OSC Message sent', + }; + if (this.oscClient == null) { - console.log('OSC ERROR: Client not initialised'); - return; + reply.success = false; + reply.message = 'Client not initialised'; + return reply; } if (messageType == null) { - console.log('OSC ERROR: Message undefined'); - return; + reply.success = false; + reply.message = 'Message undefined'; + return reply; } // only specify special cases @@ -68,35 +79,58 @@ export class OSCIntegration { case 'overtime': // Whether timer is negative this.oscClient.send(`${this.ADDRESS}/overtime`, payload, (err) => { - if (err) console.error(err); + if (err) { + reply.success = false; + reply.message = err; + } }); break; + case 'title': - if (payload != null && payload !== "") { + if (payload != null && payload !== '') { // Send Title of current event 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; + case 'presenter': - if (payload != null && payload !== "") { + if (payload != null && payload !== '') { // Send presenter data on current event 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; + default: // catch all for messages, allows to add new messages // but should be used with the integrations definition - const message = new Message(`${this.ADDRESS}/${messageType}`) - if (payload != null) message.append(payload) + // eslint-disable-next-line no-case-declarations + const message = new Message(`${this.ADDRESS}/${messageType}`); + if (payload != null) message.append(payload); this.oscClient.send(message, (err) => { - if (err) console.error(err); + if (err) { + reply.success = false; + reply.message = err; + } }); break; } + return reply; } shutdown() { @@ -104,4 +138,4 @@ export class OSCIntegration { this.oscClient.close(); this.oscClient = null; } -} \ No newline at end of file +} diff --git a/server/src/controllers/OscController.js b/server/src/controllers/OscController.js index 398d11937..a0a0fb586 100644 --- a/server/src/controllers/OscController.js +++ b/server/src/controllers/OscController.js @@ -7,9 +7,7 @@ export const shutdownOSCServer = () => { }; export const initiateOSC = (config) => { - oscServer = new Server(config.port, '0.0.0.0', () => { - console.log(`OSC Server is listening on port ${config.port}`); - }); + oscServer = new Server(config.port, '0.0.0.0'); // error oscServer.on('error', console.error); @@ -19,7 +17,6 @@ export const initiateOSC = (config) => { // ontime: fixed message for app // path: command to be called // args: extra data, only used on some of the API entries (delay, goto) - console.log('OSC received', msg); // split message const [, address, path] = msg[0].split('/'); @@ -46,40 +43,35 @@ export const initiateOSC = (config) => { break; case 'start': case 'play': - console.log('calling play'); global.timer.trigger('start'); break; case 'pause': - console.log('calling pause'); global.timer.trigger('pause'); break; case 'prev': - console.log('calling prev'); global.timer.trigger('previous'); break; case 'next': - console.log('calling next'); global.timer.trigger('next'); break; case 'unload': case 'stop': - console.log('calling unload'); global.timer.trigger('unload'); break; case 'reload': - console.log('calling reload'); global.timer.trigger('reload'); break; case 'roll': - console.log('calling roll'); global.timer.trigger('roll'); break; case 'delay': - console.log('calling delay with', args); try { const t = parseInt(args); if (isNaN(t)) { - console.error(`OSC IN: delay time not recognised ${args}`); + global.timer.error( + 'RX', + `OSC IN: delay time not recognised ${args}` + ); return; } global.timer.increment(t * 1000 * 60); @@ -88,36 +80,37 @@ export const initiateOSC = (config) => { } break; case 'goto': - console.log('calling goto with', args); try { const eventIndex = parseInt(args); if (isNaN(eventIndex) || eventIndex <= 0 || eventIndex == null) { - console.error( + global.timer.error( + 'RX', `OSC IN: event index not recognised or out of range ${eventIndex}` ); } global.timer.loadEventByIndex(eventIndex - 1); } catch (error) { - console.log('error calling goto: ', error); + global.timer.error('RX', `OSC IN: error calling goto ${error}`); } break; case 'gotoid': console.log('calling gotoid with', args); if (args == null) { - console.error( - `OSC IN: event id not recognised or out of range ${args}` + global.timer.error( + 'RX', + `OSC IN: event id not recognised or out of range ${args}}` ); return; } try { global.timer.loadEventById(args.toString().toLowerCase()); } catch (error) { - console.log('error calling goto: ', error); + global.timer.error('RX', `OSC IN: error calling goto ${error}`); } break; default: - console.log(`Error: unhandled message ${path}`); + global.timer.warning('RX', `OSC IN: unhandled message ${path}`); break; } }); diff --git a/server/src/controllers/eventController.js b/server/src/controllers/eventController.js index 938e412b7..b40eb5f79 100644 --- a/server/src/controllers/eventController.js +++ b/server/src/controllers/eventController.js @@ -21,6 +21,5 @@ export const postEvent = async (req, res) => { res.sendStatus(200); } catch (error) { res.status(400).send(error); - console.log(error); } }; diff --git a/server/src/controllers/ontimeController.js b/server/src/controllers/ontimeController.js index 3e9555bba..00b7dfeb9 100644 --- a/server/src/controllers/ontimeController.js +++ b/server/src/controllers/ontimeController.js @@ -130,7 +130,6 @@ export const postInfo = async (req, res) => { res.sendStatus(200); } catch (error) { res.status(400).send(error); - console.log(error); } }; diff --git a/server/src/controllers/playbackController.js b/server/src/controllers/playbackController.js index b97ba1b26..54176421d 100644 --- a/server/src/controllers/playbackController.js +++ b/server/src/controllers/playbackController.js @@ -7,69 +7,59 @@ export const pbGet = async (req, res) => { // Create controller for GET request to '/playback/onAir' // Turns onAir flag to true export const onAir = async (req, res) => { - console.log('Setting onAir to true'); global.timer.trigger('onAir') ? res.sendStatus(200) : res.sendStatus(400); }; // Create controller for GET request to '/playback/onAir' // Turns onAir flag to true export const offAir = async (req, res) => { - console.log('Setting onAir to false'); global.timer.trigger('offAir') ? res.sendStatus(200) : res.sendStatus(400); }; // Create controller for GET request to '/playback/start' // Starts timer object export const pbStart = async (req, res) => { - console.log('Calling start'); global.timer.trigger('start') ? res.sendStatus(200) : res.sendStatus(400); }; // Create controller for GET request to '/playback/pause' // Pauses timer object export const pbPause = async (req, res) => { - console.log('Calling pause'); global.timer.trigger('pause') ? res.sendStatus(200) : res.sendStatus(400); }; // Create controller for GET request to '/playback/stop' // Stops timer object export const pbStop = async (req, res) => { - console.log('Calling stop'); global.timer.trigger('stop') ? res.sendStatus(200) : res.sendStatus(400); }; // Create controller for GET request to '/playback/roll' // Sets timer object to roll mode export const pbRoll = async (req, res) => { - console.log('Calling roll'); global.timer.trigger('roll') ? res.sendStatus(200) : res.sendStatus(400); }; // Create controller for GET request to '/playback/previous' // Sets timer object to roll mode export const pbPrevious = async (req, res) => { - console.log('Calling previous'); global.timer.trigger('previous') ? res.sendStatus(200) : res.sendStatus(400); }; // Create controller for GET request to '/playback/next' // Sets timer object to roll mode export const pbNext = async (req, res) => { - console.log('Calling next'); global.timer.trigger('next') ? res.sendStatus(200) : res.sendStatus(400); }; // Create controller for GET request to '/playback/unload' // Unloads any events export const pbUnload = async (req, res) => { - console.log('Calling unload'); global.timer.trigger('unload') ? res.sendStatus(200) : res.sendStatus(400); }; // Create controller for GET request to '/playback/reload' // Reloads current event export const pbReload = async (req, res) => { - console.log('Calling reload'); global.timer.trigger('reload') ? res.sendStatus(200) : res.sendStatus(400); }; diff --git a/server/src/package.json b/server/src/package.json index f04c7aded..4277cb3c4 100644 --- a/server/src/package.json +++ b/server/src/package.json @@ -1,4 +1,5 @@ { + "name": "ontime-server", "type": "module", "dependencies": { "body-parser": "~1.19.0", diff --git a/server/src/utils/__tests__/getRandomName.test.js b/server/src/utils/__tests__/getRandomName.test.js new file mode 100644 index 000000000..308faa090 --- /dev/null +++ b/server/src/utils/__tests__/getRandomName.test.js @@ -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); +}); \ No newline at end of file diff --git a/server/src/utils/getRandomName.js b/server/src/utils/getRandomName.js new file mode 100644 index 000000000..423d21f5c --- /dev/null +++ b/server/src/utils/getRandomName.js @@ -0,0 +1,2860 @@ +const adjective = [ + 'abandoned', + 'able', + 'absolute', + 'adorable', + 'adventurous', + 'academic', + 'acceptable', + 'acclaimed', + 'accomplished', + 'accurate', + 'aching', + 'acidic', + 'acrobatic', + 'active', + 'actual', + 'adept', + 'admirable', + 'admired', + 'adolescent', + 'adorable', + 'adored', + 'advanced', + 'afraid', + 'affectionate', + 'aged', + 'aggravating', + 'aggressive', + 'agile', + 'agitated', + 'agonizing', + 'agreeable', + 'ajar', + 'alarmed', + 'alarming', + 'alert', + 'alienated', + 'alive', + 'all', + 'altruistic', + 'amazing', + 'ambitious', + 'ample', + 'amused', + 'amusing', + 'anchored', + 'ancient', + 'angelic', + 'angry', + 'anguished', + 'animated', + 'annual', + 'another', + 'antique', + 'anxious', + 'any', + 'apprehensive', + 'appropriate', + 'apt', + 'arctic', + 'arid', + 'aromatic', + 'artistic', + 'ashamed', + 'assured', + 'astonishing', + 'athletic', + 'attached', + 'attentive', + 'attractive', + 'austere', + 'authentic', + 'authorized', + 'automatic', + 'avaricious', + 'average', + 'aware', + 'awesome', + 'awful', + 'awkward', + 'babyish', + 'bad', + 'back', + 'baggy', + 'bare', + 'barren', + 'basic', + 'beautiful', + 'belated', + 'beloved', + 'beneficial', + 'better', + 'best', + 'bewitched', + 'big', + 'big-hearted', + 'biodegradable', + 'bite-sized', + 'bitter', + 'black', + 'black-and-white', + 'bland', + 'blank', + 'blaring', + 'bleak', + 'blind', + 'blissful', + 'blond', + 'blue', + 'blushing', + 'bogus', + 'boiling', + 'bold', + 'bony', + 'boring', + 'bossy', + 'both', + 'bouncy', + 'bountiful', + 'bowed', + 'brave', + 'breakable', + 'brief', + 'bright', + 'brilliant', + 'brisk', + 'broken', + 'bronze', + 'brown', + 'bruised', + 'bubbly', + 'bulky', + 'bumpy', + 'buoyant', + 'burdensome', + 'burly', + 'bustling', + 'busy', + 'buttery', + 'buzzing', + 'calculating', + 'calm', + 'candid', + 'canine', + 'capital', + 'carefree', + 'careful', + 'careless', + 'caring', + 'cautious', + 'cavernous', + 'celebrated', + 'charming', + 'cheap', + 'cheerful', + 'cheery', + 'chief', + 'chilly', + 'chubby', + 'circular', + 'classic', + 'clean', + 'clear', + 'clear-cut', + 'clever', + 'close', + 'closed', + 'cloudy', + 'clueless', + 'clumsy', + 'cluttered', + 'coarse', + 'cold', + 'colorful', + 'colorless', + 'colossal', + 'comfortable', + 'common', + 'compassionate', + 'competent', + 'complete', + 'complex', + 'complicated', + 'composed', + 'concerned', + 'concrete', + 'confused', + 'conscious', + 'considerate', + 'constant', + 'content', + 'conventional', + 'cooked', + 'cool', + 'cooperative', + 'coordinated', + 'corny', + 'corrupt', + 'costly', + 'courageous', + 'courteous', + 'crafty', + 'crazy', + 'creamy', + 'creative', + 'creepy', + 'criminal', + 'crisp', + 'critical', + 'crooked', + 'crowded', + 'cruel', + 'crushing', + 'cuddly', + 'cultivated', + 'cultured', + 'cumbersome', + 'curly', + 'curvy', + 'cute', + 'cylindrical', + 'damaged', + 'damp', + 'dangerous', + 'dapper', + 'daring', + 'darling', + 'dark', + 'dazzling', + 'dead', + 'deadly', + 'deafening', + 'dear', + 'dearest', + 'decent', + 'decimal', + 'decisive', + 'deep', + 'defenseless', + 'defensive', + 'defiant', + 'deficient', + 'definite', + 'definitive', + 'delayed', + 'delectable', + 'delicious', + 'delightful', + 'delirious', + 'demanding', + 'dense', + 'dental', + 'dependable', + 'dependent', + 'descriptive', + 'deserted', + 'detailed', + 'determined', + 'devoted', + 'different', + 'difficult', + 'digital', + 'diligent', + 'dim', + 'dimpled', + 'dimwitted', + 'direct', + 'disastrous', + 'discrete', + 'disfigured', + 'disgusting', + 'disloyal', + 'dismal', + 'distant', + 'downright', + 'dreary', + 'dirty', + 'disguised', + 'dishonest', + 'dismal', + 'distant', + 'distinct', + 'distorted', + 'dizzy', + 'dopey', + 'doting', + 'double', + 'downright', + 'drab', + 'drafty', + 'dramatic', + 'dreary', + 'droopy', + 'dry', + 'dual', + 'dull', + 'dutiful', + 'each', + 'eager', + 'earnest', + 'early', + 'easy', + 'easy-going', + 'ecstatic', + 'edible', + 'educated', + 'elaborate', + 'elastic', + 'elated', + 'elderly', + 'electric', + 'elegant', + 'elementary', + 'elliptical', + 'embarrassed', + 'embellished', + 'eminent', + 'emotional', + 'empty', + 'enchanted', + 'enchanting', + 'energetic', + 'enlightened', + 'enormous', + 'enraged', + 'entire', + 'envious', + 'equal', + 'equatorial', + 'essential', + 'esteemed', + 'ethical', + 'euphoric', + 'even', + 'evergreen', + 'everlasting', + 'every', + 'evil', + 'exalted', + 'excellent', + 'exemplary', + 'exhausted', + 'excitable', + 'excited', + 'exciting', + 'exotic', + 'expensive', + 'experienced', + 'expert', + 'extraneous', + 'extroverted', + 'extra-large', + 'extra-small', + 'fabulous', + 'failing', + 'faint', + 'fair', + 'faithful', + 'fake', + 'false', + 'familiar', + 'famous', + 'fancy', + 'fantastic', + 'far', + 'faraway', + 'far-flung', + 'far-off', + 'fast', + 'fat', + 'fatal', + 'fatherly', + 'favorable', + 'favorite', + 'fearful', + 'fearless', + 'feisty', + 'feline', + 'female', + 'feminine', + 'few', + 'fickle', + 'filthy', + 'fine', + 'finished', + 'firm', + 'first', + 'firsthand', + 'fitting', + 'fixed', + 'flaky', + 'flamboyant', + 'flashy', + 'flat', + 'flawed', + 'flawless', + 'flickering', + 'flimsy', + 'flippant', + 'flowery', + 'fluffy', + 'fluid', + 'flustered', + 'focused', + 'fond', + 'foolhardy', + 'foolish', + 'forceful', + 'forked', + 'formal', + 'forsaken', + 'forthright', + 'fortunate', + 'fragrant', + 'frail', + 'frank', + 'frayed', + 'free', + 'French', + 'fresh', + 'frequent', + 'friendly', + 'frightened', + 'frightening', + 'frigid', + 'frilly', + 'frizzy', + 'frivolous', + 'front', + 'frosty', + 'frozen', + 'frugal', + 'fruitful', + 'full', + 'fumbling', + 'functional', + 'funny', + 'fussy', + 'fuzzy', + 'gargantuan', + 'gaseous', + 'general', + 'generous', + 'gentle', + 'genuine', + 'giant', + 'giddy', + 'gigantic', + 'gifted', + 'giving', + 'glamorous', + 'glaring', + 'glass', + 'gleaming', + 'gleeful', + 'glistening', + 'glittering', + 'gloomy', + 'glorious', + 'glossy', + 'glum', + 'golden', + 'good', + 'good-natured', + 'gorgeous', + 'graceful', + 'gracious', + 'grand', + 'grandiose', + 'granular', + 'grateful', + 'grave', + 'gray', + 'great', + 'greedy', + 'green', + 'gregarious', + 'grim', + 'grimy', + 'gripping', + 'grizzled', + 'gross', + 'grotesque', + 'grouchy', + 'grounded', + 'growing', + 'growling', + 'grown', + 'grubby', + 'gruesome', + 'grumpy', + 'guilty', + 'gullible', + 'gummy', + 'hairy', + 'half', + 'handmade', + 'handsome', + 'handy', + 'happy', + 'happy-go-lucky', + 'hard', + 'hard-to-find', + 'harmful', + 'harmless', + 'harmonious', + 'harsh', + 'hasty', + 'hateful', + 'haunting', + 'healthy', + 'heartfelt', + 'hearty', + 'heavenly', + 'heavy', + 'hefty', + 'helpful', + 'helpless', + 'hidden', + 'hideous', + 'high', + 'high-level', + 'hilarious', + 'hoarse', + 'hollow', + 'homely', + 'honest', + 'honorable', + 'honored', + 'hopeful', + 'horrible', + 'hospitable', + 'hot', + 'huge', + 'humble', + 'humiliating', + 'humming', + 'humongous', + 'hungry', + 'hurtful', + 'husky', + 'icky', + 'icy', + 'ideal', + 'idealistic', + 'identical', + 'idle', + 'idiotic', + 'idolized', + 'ignorant', + 'ill', + 'illegal', + 'ill-fated', + 'ill-informed', + 'illiterate', + 'illustrious', + 'imaginary', + 'imaginative', + 'immaculate', + 'immaterial', + 'immediate', + 'immense', + 'impassioned', + 'impeccable', + 'impartial', + 'imperfect', + 'imperturbable', + 'impish', + 'impolite', + 'important', + 'impossible', + 'impractical', + 'impressionable', + 'impressive', + 'improbable', + 'impure', + 'inborn', + 'incomparable', + 'incompatible', + 'incomplete', + 'inconsequential', + 'incredible', + 'indelible', + 'inexperienced', + 'indolent', + 'infamous', + 'infantile', + 'infatuated', + 'inferior', + 'infinite', + 'informal', + 'innocent', + 'insecure', + 'insidious', + 'insignificant', + 'insistent', + 'instructive', + 'insubstantial', + 'intelligent', + 'intent', + 'intentional', + 'interesting', + 'internal', + 'international', + 'intrepid', + 'ironclad', + 'irresponsible', + 'irritating', + 'itchy', + 'jaded', + 'jagged', + 'jam-packed', + 'jaunty', + 'jealous', + 'jittery', + 'joint', + 'jolly', + 'jovial', + 'joyful', + 'joyous', + 'jubilant', + 'judicious', + 'juicy', + 'jumbo', + 'junior', + 'jumpy', + 'juvenile', + 'kaleidoscopic', + 'keen', + 'key', + 'kind', + 'kindhearted', + 'kindly', + 'klutzy', + 'knobby', + 'knotty', + 'knowledgeable', + 'knowing', + 'known', + 'kooky', + 'kosher', + 'lame', + 'lanky', + 'large', + 'last', + 'lasting', + 'late', + 'lavish', + 'lawful', + 'lazy', + 'leading', + 'lean', + 'leafy', + 'left', + 'legal', + 'legitimate', + 'light', + 'lighthearted', + 'likable', + 'likely', + 'limited', + 'limp', + 'limping', + 'linear', + 'lined', + 'liquid', + 'little', + 'live', + 'lively', + 'livid', + 'loathsome', + 'lone', + 'lonely', + 'long', + 'long-term', + 'loose', + 'lopsided', + 'lost', + 'loud', + 'lovable', + 'lovely', + 'loving', + 'low', + 'loyal', + 'lucky', + 'lumbering', + 'luminous', + 'lumpy', + 'lustrous', + 'luxurious', + 'mad', + 'made-up', + 'magnificent', + 'majestic', + 'major', + 'male', + 'mammoth', + 'married', + 'marvelous', + 'masculine', + 'massive', + 'mature', + 'meager', + 'mealy', + 'mean', + 'measly', + 'meaty', + 'medical', + 'mediocre', + 'medium', + 'meek', + 'mellow', + 'melodic', + 'memorable', + 'menacing', + 'merry', + 'messy', + 'metallic', + 'mild', + 'milky', + 'mindless', + 'miniature', + 'minor', + 'minty', + 'miserable', + 'miserly', + 'misguided', + 'misty', + 'mixed', + 'modern', + 'modest', + 'moist', + 'monstrous', + 'monthly', + 'monumental', + 'moral', + 'mortified', + 'motherly', + 'motionless', + 'mountainous', + 'muddy', + 'muffled', + 'multicolored', + 'mundane', + 'murky', + 'mushy', + 'musty', + 'muted', + 'mysterious', + 'naive', + 'narrow', + 'nasty', + 'natural', + 'naughty', + 'nautical', + 'near', + 'neat', + 'necessary', + 'needy', + 'negative', + 'neglected', + 'negligible', + 'neighboring', + 'nervous', + 'new', + 'next', + 'nice', + 'nifty', + 'nimble', + 'nippy', + 'nocturnal', + 'noisy', + 'nonstop', + 'normal', + 'notable', + 'noted', + 'noteworthy', + 'novel', + 'noxious', + 'numb', + 'nutritious', + 'nutty', + 'obedient', + 'obese', + 'oblong', + 'oily', + 'oblong', + 'obvious', + 'occasional', + 'odd', + 'oddball', + 'offbeat', + 'offensive', + 'official', + 'old', + 'old-fashioned', + 'only', + 'open', + 'optimal', + 'optimistic', + 'opulent', + 'orange', + 'orderly', + 'organic', + 'ornate', + 'ornery', + 'ordinary', + 'original', + 'other', + 'our', + 'outlying', + 'outgoing', + 'outlandish', + 'outrageous', + 'outstanding', + 'oval', + 'overcooked', + 'overdue', + 'overjoyed', + 'overlooked', + 'palatable', + 'pale', + 'paltry', + 'parallel', + 'parched', + 'partial', + 'passionate', + 'past', + 'pastel', + 'peaceful', + 'peppery', + 'perfect', + 'perfumed', + 'periodic', + 'perky', + 'personal', + 'pertinent', + 'pesky', + 'pessimistic', + 'petty', + 'phony', + 'physical', + 'piercing', + 'pink', + 'pitiful', + 'plain', + 'plaintive', + 'plastic', + 'playful', + 'pleasant', + 'pleased', + 'pleasing', + 'plump', + 'plush', + 'polished', + 'polite', + 'political', + 'pointed', + 'pointless', + 'poised', + 'poor', + 'popular', + 'portly', + 'posh', + 'positive', + 'possible', + 'potable', + 'powerful', + 'powerless', + 'practical', + 'precious', + 'present', + 'prestigious', + 'pretty', + 'precious', + 'previous', + 'pricey', + 'prickly', + 'primary', + 'prime', + 'pristine', + 'private', + 'prize', + 'probable', + 'productive', + 'profitable', + 'profuse', + 'proper', + 'proud', + 'prudent', + 'punctual', + 'pungent', + 'puny', + 'pure', + 'purple', + 'pushy', + 'putrid', + 'puzzled', + 'puzzling', + 'quaint', + 'qualified', + 'quarrelsome', + 'quarterly', + 'queasy', + 'querulous', + 'questionable', + 'quick', + 'quick-witted', + 'quiet', + 'quintessential', + 'quirky', + 'quixotic', + 'quizzical', + 'radiant', + 'ragged', + 'rapid', + 'rare', + 'rash', + 'raw', + 'recent', + 'reckless', + 'rectangular', + 'ready', + 'real', + 'realistic', + 'reasonable', + 'red', + 'reflecting', + 'regal', + 'regular', + 'reliable', + 'relieved', + 'remarkable', + 'remorseful', + 'remote', + 'repentant', + 'required', + 'respectful', + 'responsible', + 'repulsive', + 'revolving', + 'rewarding', + 'rich', + 'rigid', + 'right', + 'ringed', + 'ripe', + 'roasted', + 'robust', + 'rosy', + 'rotating', + 'rotten', + 'rough', + 'round', + 'rowdy', + 'royal', + 'rubbery', + 'rundown', + 'ruddy', + 'rude', + 'runny', + 'rural', + 'rusty', + 'sad', + 'safe', + 'salty', + 'same', + 'sandy', + 'sane', + 'sarcastic', + 'sardonic', + 'satisfied', + 'scaly', + 'scarce', + 'scared', + 'scary', + 'scented', + 'scholarly', + 'scientific', + 'scornful', + 'scratchy', + 'scrawny', + 'second', + 'secondary', + 'second-hand', + 'secret', + 'self-assured', + 'self-reliant', + 'selfish', + 'sentimental', + 'separate', + 'serene', + 'serious', + 'serpentine', + 'several', + 'severe', + 'shabby', + 'shadowy', + 'shady', + 'shallow', + 'shameful', + 'shameless', + 'sharp', + 'shimmering', + 'shiny', + 'shocked', + 'shocking', + 'shoddy', + 'short', + 'short-term', + 'showy', + 'shrill', + 'shy', + 'sick', + 'silent', + 'silky', + 'silly', + 'silver', + 'similar', + 'simple', + 'simplistic', + 'sinful', + 'single', + 'sizzling', + 'skeletal', + 'skinny', + 'sleepy', + 'slight', + 'slim', + 'slimy', + 'slippery', + 'slow', + 'slushy', + 'small', + 'smart', + 'smoggy', + 'smooth', + 'smug', + 'snappy', + 'snarling', + 'sneaky', + 'sniveling', + 'snoopy', + 'sociable', + 'soft', + 'soggy', + 'solid', + 'somber', + 'some', + 'spherical', + 'sophisticated', + 'sore', + 'sorrowful', + 'soulful', + 'soupy', + 'sour', + 'Spanish', + 'sparkling', + 'sparse', + 'specific', + 'spectacular', + 'speedy', + 'spicy', + 'spiffy', + 'spirited', + 'spiteful', + 'splendid', + 'spotless', + 'spotted', + 'spry', + 'square', + 'squeaky', + 'squiggly', + 'stable', + 'staid', + 'stained', + 'stale', + 'standard', + 'starchy', + 'stark', + 'starry', + 'steep', + 'sticky', + 'stiff', + 'stimulating', + 'stingy', + 'stormy', + 'straight', + 'strange', + 'steel', + 'strict', + 'strident', + 'striking', + 'striped', + 'strong', + 'studious', + 'stunning', + 'stupendous', + 'stupid', + 'sturdy', + 'stylish', + 'subdued', + 'submissive', + 'substantial', + 'subtle', + 'suburban', + 'sudden', + 'sugary', + 'sunny', + 'super', + 'superb', + 'superficial', + 'superior', + 'supportive', + 'sure-footed', + 'surprised', + 'suspicious', + 'svelte', + 'sweaty', + 'sweet', + 'sweltering', + 'swift', + 'sympathetic', + 'tall', + 'talkative', + 'tame', + 'tan', + 'tangible', + 'tart', + 'tasty', + 'tattered', + 'taut', + 'tedious', + 'teeming', + 'tempting', + 'tender', + 'tense', + 'tepid', + 'terrible', + 'terrific', + 'testy', + 'thankful', + 'that', + 'these', + 'thick', + 'thin', + 'third', + 'thirsty', + 'this', + 'thorough', + 'thorny', + 'those', + 'thoughtful', + 'threadbare', + 'thrifty', + 'thunderous', + 'tidy', + 'tight', + 'timely', + 'tinted', + 'tiny', + 'tired', + 'torn', + 'total', + 'tough', + 'traumatic', + 'treasured', + 'tremendous', + 'tragic', + 'trained', + 'tremendous', + 'triangular', + 'tricky', + 'trifling', + 'trim', + 'trivial', + 'troubled', + 'true', + 'trusting', + 'trustworthy', + 'trusty', + 'truthful', + 'tubby', + 'turbulent', + 'twin', + 'ugly', + 'ultimate', + 'unacceptable', + 'unaware', + 'uncomfortable', + 'uncommon', + 'unconscious', + 'understated', + 'unequaled', + 'uneven', + 'unfinished', + 'unfit', + 'unfolded', + 'unfortunate', + 'unhappy', + 'unhealthy', + 'uniform', + 'unimportant', + 'unique', + 'united', + 'unkempt', + 'unknown', + 'unlawful', + 'unlined', + 'unlucky', + 'unnatural', + 'unpleasant', + 'unrealistic', + 'unripe', + 'unruly', + 'unselfish', + 'unsightly', + 'unsteady', + 'unsung', + 'untidy', + 'untimely', + 'untried', + 'untrue', + 'unused', + 'unusual', + 'unwelcome', + 'unwieldy', + 'unwilling', + 'unwitting', + 'unwritten', + 'upbeat', + 'upright', + 'upset', + 'urban', + 'usable', + 'used', + 'useful', + 'useless', + 'utilized', + 'utter', + 'vacant', + 'vague', + 'vain', + 'valid', + 'valuable', + 'vapid', + 'variable', + 'vast', + 'velvety', + 'venerated', + 'vengeful', + 'verifiable', + 'vibrant', + 'vicious', + 'victorious', + 'vigilant', + 'vigorous', + 'villainous', + 'violet', + 'violent', + 'virtual', + 'virtuous', + 'visible', + 'vital', + 'vivacious', + 'vivid', + 'voluminous', + 'wan', + 'warlike', + 'warm', + 'warmhearted', + 'warped', + 'wary', + 'wasteful', + 'watchful', + 'waterlogged', + 'watery', + 'wavy', + 'wealthy', + 'weak', + 'weary', + 'webbed', + 'wee', + 'weekly', + 'weepy', + 'weighty', + 'weird', + 'welcome', + 'well-documented', + 'well-groomed', + 'well-informed', + 'well-lit', + 'well-made', + 'well-off', + 'well-to-do', + 'well-worn', + 'wet', + 'which', + 'whimsical', + 'whirlwind', + 'whispered', + 'white', + 'whole', + 'whopping', + 'wicked', + 'wide', + 'wide-eyed', + 'wiggly', + 'wild', + 'willing', + 'wilted', + 'winding', + 'windy', + 'winged', + 'wiry', + 'wise', + 'witty', + 'wobbly', + 'woeful', + 'wonderful', + 'wooden', + 'woozy', + 'wordy', + 'worldly', + 'worn', + 'worried', + 'worrisome', + 'worse', + 'worst', + 'worthless', + 'worthwhile', + 'worthy', + 'wrathful', + 'wretched', + 'writhing', + 'wrong', + 'wry', + 'yawning', + 'yearly', + 'yellow', + 'yellowish', + 'young', + 'youthful', + 'yummy', + 'zany', + 'zealous', + 'zesty', + 'zigzag', + 'rocky', +]; + +const object = [ + 'people', + 'history', + 'way', + 'art', + 'world', + 'information', + 'map', + 'family', + 'government', + 'health', + 'system', + 'computer', + 'meat', + 'year', + 'thanks', + 'music', + 'person', + 'reading', + 'method', + 'data', + 'food', + 'understanding', + 'theory', + 'law', + 'bird', + 'literature', + 'problem', + 'software', + 'control', + 'knowledge', + 'power', + 'ability', + 'economics', + 'love', + 'internet', + 'television', + 'science', + 'library', + 'nature', + 'fact', + 'product', + 'idea', + 'temperature', + 'investment', + 'area', + 'society', + 'activity', + 'story', + 'industry', + 'media', + 'thing', + 'oven', + 'community', + 'definition', + 'safety', + 'quality', + 'development', + 'language', + 'management', + 'player', + 'variety', + 'video', + 'week', + 'security', + 'country', + 'exam', + 'movie', + 'organization', + 'equipment', + 'physics', + 'analysis', + 'policy', + 'series', + 'thought', + 'basis', + 'boyfriend', + 'direction', + 'strategy', + 'technology', + 'army', + 'camera', + 'freedom', + 'paper', + 'environment', + 'child', + 'instance', + 'month', + 'truth', + 'marketing', + 'university', + 'writing', + 'article', + 'department', + 'difference', + 'goal', + 'news', + 'audience', + 'fishing', + 'growth', + 'income', + 'marriage', + 'user', + 'combination', + 'failure', + 'meaning', + 'medicine', + 'philosophy', + 'teacher', + 'communication', + 'night', + 'chemistry', + 'disease', + 'disk', + 'energy', + 'nation', + 'road', + 'role', + 'soup', + 'advertising', + 'location', + 'success', + 'addition', + 'apartment', + 'education', + 'math', + 'moment', + 'painting', + 'politics', + 'attention', + 'decision', + 'event', + 'property', + 'shopping', + 'student', + 'wood', + 'competition', + 'distribution', + 'entertainment', + 'office', + 'population', + 'president', + 'unit', + 'category', + 'cigarette', + 'context', + 'introduction', + 'opportunity', + 'performance', + 'driver', + 'flight', + 'length', + 'magazine', + 'newspaper', + 'relationship', + 'teaching', + 'cell', + 'dealer', + 'debate', + 'finding', + 'lake', + 'member', + 'message', + 'phone', + 'scene', + 'appearance', + 'association', + 'concept', + 'customer', + 'death', + 'discussion', + 'housing', + 'inflation', + 'insurance', + 'mood', + 'woman', + 'advice', + 'blood', + 'effort', + 'expression', + 'importance', + 'opinion', + 'payment', + 'reality', + 'responsibility', + 'situation', + 'skill', + 'statement', + 'wealth', + 'application', + 'city', + 'county', + 'depth', + 'estate', + 'foundation', + 'grandmother', + 'heart', + 'perspective', + 'photo', + 'recipe', + 'studio', + 'topic', + 'collection', + 'depression', + 'imagination', + 'passion', + 'percentage', + 'resource', + 'setting', + 'ad', + 'agency', + 'college', + 'connection', + 'criticism', + 'debt', + 'description', + 'memory', + 'patience', + 'secretary', + 'solution', + 'administration', + 'aspect', + 'attitude', + 'director', + 'personality', + 'psychology', + 'recommendation', + 'response', + 'selection', + 'storage', + 'version', + 'alcohol', + 'argument', + 'complaint', + 'contract', + 'emphasis', + 'highway', + 'loss', + 'membership', + 'possession', + 'preparation', + 'steak', + 'union', + 'agreement', + 'cancer', + 'currency', + 'employment', + 'engineering', + 'entry', + 'interaction', + 'limit', + 'mixture', + 'preference', + 'region', + 'republic', + 'seat', + 'tradition', + 'virus', + 'actor', + 'classroom', + 'delivery', + 'device', + 'difficulty', + 'drama', + 'election', + 'engine', + 'football', + 'guidance', + 'hotel', + 'match', + 'owner', + 'priority', + 'protection', + 'suggestion', + 'tension', + 'variation', + 'anxiety', + 'atmosphere', + 'awareness', + 'bread', + 'climate', + 'comparison', + 'confusion', + 'construction', + 'elevator', + 'emotion', + 'employee', + 'employer', + 'guest', + 'height', + 'leadership', + 'mall', + 'manager', + 'operation', + 'recording', + 'respect', + 'sample', + 'transportation', + 'boring', + 'charity', + 'cousin', + 'disaster', + 'editor', + 'efficiency', + 'excitement', + 'extent', + 'feedback', + 'guitar', + 'homework', + 'leader', + 'mom', + 'outcome', + 'permission', + 'presentation', + 'promotion', + 'reflection', + 'refrigerator', + 'resolution', + 'revenue', + 'session', + 'singer', + 'tennis', + 'basket', + 'bonus', + 'cabinet', + 'childhood', + 'church', + 'clothes', + 'coffee', + 'dinner', + 'drawing', + 'hair', + 'hearing', + 'initiative', + 'judgment', + 'lab', + 'measurement', + 'mode', + 'mud', + 'orange', + 'poetry', + 'police', + 'possibility', + 'procedure', + 'queen', + 'ratio', + 'relation', + 'restaurant', + 'satisfaction', + 'sector', + 'signature', + 'significance', + 'song', + 'tooth', + 'town', + 'vehicle', + 'volume', + 'wife', + 'accident', + 'airport', + 'appointment', + 'arrival', + 'assumption', + 'baseball', + 'chapter', + 'committee', + 'conversation', + 'database', + 'enthusiasm', + 'error', + 'explanation', + 'farmer', + 'gate', + 'girl', + 'hall', + 'historian', + 'hospital', + 'injury', + 'instruction', + 'maintenance', + 'manufacturer', + 'meal', + 'perception', + 'pie', + 'poem', + 'presence', + 'proposal', + 'reception', + 'replacement', + 'revolution', + 'river', + 'son', + 'speech', + 'tea', + 'village', + 'warning', + 'winner', + 'worker', + 'writer', + 'assistance', + 'breath', + 'buyer', + 'chest', + 'chocolate', + 'conclusion', + 'contribution', + 'cookie', + 'courage', + 'desk', + 'drawer', + 'establishment', + 'examination', + 'garbage', + 'grocery', + 'honey', + 'impression', + 'improvement', + 'independence', + 'insect', + 'inspection', + 'inspector', + 'king', + 'ladder', + 'menu', + 'penalty', + 'piano', + 'potato', + 'profession', + 'professor', + 'quantity', + 'reaction', + 'requirement', + 'salad', + 'sister', + 'supermarket', + 'tongue', + 'weakness', + 'wedding', + 'affair', + 'ambition', + 'analyst', + 'apple', + 'assignment', + 'assistant', + 'bathroom', + 'bedroom', + 'beer', + 'birthday', + 'celebration', + 'championship', + 'cheek', + 'client', + 'consequence', + 'departure', + 'diamond', + 'dirt', + 'ear', + 'fortune', + 'friendship', + 'funeral', + 'gene', + 'girlfriend', + 'hat', + 'indication', + 'intention', + 'lady', + 'midnight', + 'negotiation', + 'obligation', + 'passenger', + 'pizza', + 'platform', + 'poet', + 'pollution', + 'recognition', + 'reputation', + 'shirt', + 'speaker', + 'stranger', + 'surgery', + 'sympathy', + 'tale', + 'throat', + 'trainer', + 'uncle', + 'youth', + 'time', + 'work', + 'film', + 'water', + 'money', + 'example', + 'while', + 'business', + 'study', + 'game', + 'life', + 'form', + 'air', + 'day', + 'place', + 'number', + 'part', + 'field', + 'fish', + 'back', + 'process', + 'heat', + 'hand', + 'experience', + 'job', + 'book', + 'end', + 'point', + 'type', + 'home', + 'economy', + 'value', + 'body', + 'market', + 'guide', + 'interest', + 'state', + 'radio', + 'course', + 'company', + 'price', + 'size', + 'card', + 'list', + 'mind', + 'trade', + 'line', + 'care', + 'group', + 'risk', + 'word', + 'fat', + 'force', + 'key', + 'light', + 'training', + 'name', + 'school', + 'top', + 'amount', + 'level', + 'order', + 'practice', + 'research', + 'sense', + 'service', + 'piece', + 'web', + 'boss', + 'sport', + 'fun', + 'house', + 'page', + 'term', + 'test', + 'answer', + 'sound', + 'focus', + 'matter', + 'kind', + 'soil', + 'board', + 'oil', + 'picture', + 'access', + 'garden', + 'range', + 'rate', + 'reason', + 'future', + 'site', + 'demand', + 'exercise', + 'image', + 'case', + 'cause', + 'coast', + 'action', + 'age', + 'bad', + 'boat', + 'record', + 'result', + 'section', + 'building', + 'mouse', + 'cash', + 'class', + 'period', + 'plan', + 'store', + 'tax', + 'side', + 'subject', + 'space', + 'rule', + 'stock', + 'weather', + 'chance', + 'figure', + 'man', + 'model', + 'source', + 'beginning', + 'earth', + 'program', + 'chicken', + 'design', + 'feature', + 'head', + 'material', + 'purpose', + 'question', + 'rock', + 'salt', + 'act', + 'birth', + 'car', + 'dog', + 'object', + 'scale', + 'sun', + 'note', + 'profit', + 'rent', + 'speed', + 'style', + 'war', + 'bank', + 'craft', + 'half', + 'inside', + 'outside', + 'standard', + 'bus', + 'exchange', + 'eye', + 'fire', + 'position', + 'pressure', + 'stress', + 'advantage', + 'benefit', + 'box', + 'frame', + 'issue', + 'step', + 'cycle', + 'face', + 'item', + 'metal', + 'paint', + 'review', + 'room', + 'screen', + 'structure', + 'view', + 'account', + 'ball', + 'discipline', + 'medium', + 'share', + 'balance', + 'bit', + 'black', + 'bottom', + 'choice', + 'gift', + 'impact', + 'machine', + 'shape', + 'tool', + 'wind', + 'address', + 'average', + 'career', + 'culture', + 'morning', + 'pot', + 'sign', + 'table', + 'task', + 'condition', + 'contact', + 'credit', + 'egg', + 'hope', + 'ice', + 'network', + 'north', + 'square', + 'attempt', + 'date', + 'effect', + 'link', + 'post', + 'star', + 'voice', + 'capital', + 'challenge', + 'friend', + 'self', + 'shot', + 'brush', + 'couple', + 'exit', + 'front', + 'function', + 'lack', + 'living', + 'plant', + 'plastic', + 'spot', + 'summer', + 'taste', + 'theme', + 'track', + 'wing', + 'brain', + 'button', + 'click', + 'desire', + 'foot', + 'gas', + 'influence', + 'notice', + 'rain', + 'wall', + 'base', + 'damage', + 'distance', + 'feeling', + 'pair', + 'savings', + 'staff', + 'sugar', + 'target', + 'text', + 'animal', + 'author', + 'budget', + 'discount', + 'file', + 'ground', + 'lesson', + 'minute', + 'officer', + 'phase', + 'reference', + 'register', + 'sky', + 'stage', + 'stick', + 'title', + 'trouble', + 'bowl', + 'bridge', + 'campaign', + 'character', + 'club', + 'edge', + 'evidence', + 'fan', + 'letter', + 'lock', + 'maximum', + 'novel', + 'option', + 'pack', + 'park', + 'quarter', + 'skin', + 'sort', + 'weight', + 'baby', + 'background', + 'carry', + 'dish', + 'factor', + 'fruit', + 'glass', + 'joint', + 'master', + 'muscle', + 'red', + 'strength', + 'traffic', + 'trip', + 'vegetable', + 'appeal', + 'chart', + 'gear', + 'ideal', + 'kitchen', + 'land', + 'log', + 'mother', + 'net', + 'party', + 'principle', + 'relative', + 'sale', + 'season', + 'signal', + 'spirit', + 'street', + 'tree', + 'wave', + 'belt', + 'bench', + 'commission', + 'copy', + 'drop', + 'minimum', + 'path', + 'progress', + 'project', + 'sea', + 'south', + 'status', + 'stuff', + 'ticket', + 'tour', + 'angle', + 'blue', + 'breakfast', + 'confidence', + 'daughter', + 'degree', + 'doctor', + 'dot', + 'dream', + 'duty', + 'essay', + 'father', + 'fee', + 'finance', + 'hour', + 'juice', + 'luck', + 'milk', + 'mouth', + 'peace', + 'pipe', + 'stable', + 'storm', + 'substance', + 'team', + 'trick', + 'afternoon', + 'bat', + 'beach', + 'blank', + 'catch', + 'chain', + 'consideration', + 'cream', + 'crew', + 'detail', + 'gold', + 'interview', + 'kid', + 'mark', + 'mission', + 'pain', + 'pleasure', + 'score', + 'screw', + 'sex', + 'shop', + 'shower', + 'suit', + 'tone', + 'window', + 'agent', + 'band', + 'bath', + 'block', + 'bone', + 'calendar', + 'candidate', + 'cap', + 'coat', + 'contest', + 'corner', + 'court', + 'cup', + 'district', + 'door', + 'east', + 'finger', + 'garage', + 'guarantee', + 'hole', + 'hook', + 'implement', + 'layer', + 'lecture', + 'lie', + 'manner', + 'meeting', + 'nose', + 'parking', + 'partner', + 'profile', + 'rice', + 'routine', + 'schedule', + 'swimming', + 'telephone', + 'tip', + 'winter', + 'airline', + 'bag', + 'battle', + 'bed', + 'bill', + 'bother', + 'cake', + 'code', + 'curve', + 'designer', + 'dimension', + 'dress', + 'ease', + 'emergency', + 'evening', + 'extension', + 'farm', + 'fight', + 'gap', + 'grade', + 'holiday', + 'horror', + 'horse', + 'host', + 'husband', + 'loan', + 'mistake', + 'mountain', + 'nail', + 'noise', + 'occasion', + 'package', + 'patient', + 'pause', + 'phrase', + 'proof', + 'race', + 'relief', + 'sand', + 'sentence', + 'shoulder', + 'smoke', + 'stomach', + 'string', + 'tourist', + 'towel', + 'vacation', + 'west', + 'wheel', + 'wine', + 'arm', + 'aside', + 'associate', + 'bet', + 'blow', + 'border', + 'branch', + 'breast', + 'brother', + 'buddy', + 'bunch', + 'chip', + 'coach', + 'cross', + 'document', + 'draft', + 'dust', + 'expert', + 'floor', + 'god', + 'golf', + 'habit', + 'iron', + 'judge', + 'knife', + 'landscape', + 'league', + 'mail', + 'mess', + 'native', + 'opening', + 'parent', + 'pattern', + 'pin', + 'pool', + 'pound', + 'request', + 'salary', + 'shame', + 'shelter', + 'shoe', + 'silver', + 'tackle', + 'tank', + 'trust', + 'assist', + 'bake', + 'bar', + 'bell', + 'bike', + 'blame', + 'boy', + 'brick', + 'chair', + 'closet', + 'clue', + 'collar', + 'comment', + 'conference', + 'devil', + 'diet', + 'fear', + 'fuel', + 'glove', + 'jacket', + 'lunch', + 'monitor', + 'mortgage', + 'nurse', + 'pace', + 'panic', + 'peak', + 'plane', + 'reward', + 'row', + 'sandwich', + 'shock', + 'spite', + 'spray', + 'surprise', + 'till', + 'transition', + 'weekend', + 'welcome', + 'yard', + 'alarm', + 'bend', + 'bicycle', + 'bite', + 'blind', + 'bottle', + 'cable', + 'candle', + 'clerk', + 'cloud', + 'concert', + 'counter', + 'flower', + 'grandfather', + 'harm', + 'knee', + 'lawyer', + 'leather', + 'load', + 'mirror', + 'neck', + 'pension', + 'plate', + 'purple', + 'ruin', + 'ship', + 'skirt', + 'slice', + 'snow', + 'specialist', + 'stroke', + 'switch', + 'trash', + 'tune', + 'zone', + 'anger', + 'award', + 'bid', + 'bitter', + 'boot', + 'bug', + 'camp', + 'candy', + 'carpet', + 'cat', + 'champion', + 'channel', + 'clock', + 'comfort', + 'cow', + 'crack', + 'engineer', + 'entrance', + 'fault', + 'grass', + 'guy', + 'hell', + 'highlight', + 'incident', + 'island', + 'joke', + 'jury', + 'leg', + 'lip', + 'mate', + 'motor', + 'nerve', + 'passage', + 'pen', + 'pride', + 'priest', + 'prize', + 'promise', + 'resident', + 'resort', + 'ring', + 'roof', + 'rope', + 'sail', + 'scheme', + 'script', + 'sock', + 'station', + 'toe', + 'tower', + 'truck', + 'witness', + 'can', + 'will', + 'other', + 'use', + 'make', + 'good', + 'look', + 'help', + 'go', + 'great', + 'being', + 'still', + 'public', + 'read', + 'keep', + 'start', + 'give', + 'human', + 'local', + 'general', + 'specific', + 'long', + 'play', + 'feel', + 'high', + 'put', + 'common', + 'set', + 'change', + 'simple', + 'past', + 'big', + 'possible', + 'particular', + 'major', + 'personal', + 'current', + 'national', + 'cut', + 'natural', + 'physical', + 'show', + 'try', + 'check', + 'second', + 'call', + 'move', + 'pay', + 'let', + 'increase', + 'single', + 'individual', + 'turn', + 'ask', + 'buy', + 'guard', + 'hold', + 'main', + 'offer', + 'potential', + 'professional', + 'international', + 'travel', + 'cook', + 'alternative', + 'special', + 'working', + 'whole', + 'dance', + 'excuse', + 'cold', + 'commercial', + 'low', + 'purchase', + 'deal', + 'primary', + 'worth', + 'fall', + 'necessary', + 'positive', + 'produce', + 'search', + 'present', + 'spend', + 'talk', + 'creative', + 'tell', + 'cost', + 'drive', + 'green', + 'support', + 'glad', + 'remove', + 'return', + 'run', + 'complex', + 'due', + 'effective', + 'middle', + 'regular', + 'reserve', + 'independent', + 'leave', + 'original', + 'reach', + 'rest', + 'serve', + 'watch', + 'beautiful', + 'charge', + 'active', + 'break', + 'negative', + 'safe', + 'stay', + 'visit', + 'visual', + 'affect', + 'cover', + 'report', + 'rise', + 'walk', + 'white', + 'junior', + 'pick', + 'unique', + 'classic', + 'final', + 'lift', + 'mix', + 'private', + 'stop', + 'teach', + 'western', + 'concern', + 'familiar', + 'fly', + 'official', + 'broad', + 'comfortable', + 'gain', + 'rich', + 'save', + 'stand', + 'young', + 'heavy', + 'lead', + 'listen', + 'valuable', + 'worry', + 'handle', + 'leading', + 'meet', + 'release', + 'sell', + 'finish', + 'normal', + 'press', + 'ride', + 'secret', + 'spread', + 'spring', + 'tough', + 'wait', + 'brown', + 'deep', + 'display', + 'flow', + 'hit', + 'objective', + 'shoot', + 'touch', + 'cancel', + 'chemical', + 'cry', + 'dump', + 'extreme', + 'push', + 'conflict', + 'eat', + 'fill', + 'formal', + 'jump', + 'kick', + 'opposite', + 'pass', + 'pitch', + 'remote', + 'total', + 'treat', + 'vast', + 'abuse', + 'beat', + 'burn', + 'deposit', + 'print', + 'raise', + 'sleep', + 'somewhere', + 'advance', + 'consist', + 'dark', + 'double', + 'draw', + 'equal', + 'fix', + 'hire', + 'internal', + 'join', + 'kill', + 'sensitive', + 'tap', + 'win', + 'attack', + 'claim', + 'constant', + 'drag', + 'drink', + 'guess', + 'minor', + 'pull', + 'raw', + 'soft', + 'solid', + 'wear', + 'weird', + 'wonder', + 'annual', + 'count', + 'dead', + 'doubt', + 'feed', + 'forever', + 'impress', + 'repeat', + 'round', + 'sing', + 'slide', + 'strip', + 'wish', + 'combine', + 'command', + 'dig', + 'divide', + 'equivalent', + 'hang', + 'hunt', + 'initial', + 'march', + 'mention', + 'spiritual', + 'survey', + 'tie', + 'adult', + 'brief', + 'crazy', + 'escape', + 'gather', + 'hate', + 'prior', + 'repair', + 'rough', + 'sad', + 'scratch', + 'sick', + 'strike', + 'employ', + 'external', + 'hurt', + 'illegal', + 'laugh', + 'lay', + 'mobile', + 'nasty', + 'ordinary', + 'respond', + 'royal', + 'senior', + 'split', + 'strain', + 'struggle', + 'swim', + 'train', + 'upper', + 'wash', + 'yellow', + 'convert', + 'crash', + 'dependent', + 'fold', + 'funny', + 'grab', + 'hide', + 'miss', + 'permit', + 'quote', + 'recover', + 'resolve', + 'roll', + 'sink', + 'slip', + 'spare', + 'suspect', + 'sweet', + 'swing', + 'twist', + 'upstairs', + 'usual', + 'abroad', + 'brave', + 'calm', + 'concentrate', + 'estimate', + 'grand', + 'male', + 'mine', + 'prompt', + 'quiet', + 'refuse', + 'regret', + 'reveal', + 'rush', + 'shake', + 'shift', + 'shine', + 'steal', + 'suck', + 'surround', + 'bear', + 'brilliant', + 'dare', + 'dear', + 'delay', + 'drunk', + 'female', + 'hurry', + 'inevitable', + 'invite', + 'kiss', + 'neat', + 'pop', + 'punch', + 'quit', + 'reply', + 'representative', + 'resist', + 'rip', + 'rub', + 'silly', + 'smile', + 'spell', + 'stretch', + 'stupid', + 'tear', + 'temporary', + 'tomorrow', + 'wake', + 'wrap', + 'yesterday', + 'Thomas', + 'Tom', + 'Lieuwe', +]; + +export default function getRandomName() { + return `${adjective[Math.floor(Math.random() * adjective.length)]} ${ + object[Math.floor(Math.random() * object.length)] + }`; +}; \ No newline at end of file diff --git a/server/src/utils/time.js b/server/src/utils/time.js index 771943b11..1a121dfee 100644 --- a/server/src/utils/time.js +++ b/server/src/utils/time.js @@ -2,10 +2,26 @@ const mts = 1000; // millis to seconds const mtm = 1000 * 60; // millis to minutes const mth = 1000 * 60 * 60; // millis to hours +/** + * Returns current time in milliseconds + * @returns {number} + */ +export const nowInMillis = () => { + const now = new Date(); + + // extract milliseconds since midnight + let elapsed = now.getHours() * 3600000; + elapsed += now.getMinutes() * 60000; + elapsed += now.getSeconds() * 1000; + elapsed += now.getMilliseconds(); + + return elapsed; +}; + /** * @description Converts milliseconds to string representing time * @param {number} ms - time in milliseconds - * @param {boolean} showSeconds - wether to show the seconds + * @param {boolean} showSeconds - weather to show the seconds * @param {string} delim - character between HH MM SS * @param {string} ifNull - what to return if value is null * @returns {string} String representing time 00:12:02 @@ -17,7 +33,7 @@ export const stringFromMillis = ( delim = ':', ifNull = '...' ) => { - if (ms === null || isNaN(ms)) return ifNull; + if (ms == null || isNaN(ms)) return ifNull; const isNegative = ms < 0 ? '-' : ''; const millis = Math.abs(ms); @@ -36,7 +52,7 @@ export const stringFromMillis = ( /** * @description Converts an excel date to milliseconds * @argument {string} excelDate - excel string date - * @returns {number} - time in millisenconds + * @returns {number} - time in milliseconds */ export const excelDateStringToMillis = (excelDate) => { const date = new Date(excelDate); @@ -47,5 +63,5 @@ export const excelDateStringToMillis = (excelDate) => { return h * mth + m * mtm + s * mts; } - return null; + return 0; };