diff --git a/client/.eslintrc b/client/.eslintrc index 0596d081d..b954a3a4f 100644 --- a/client/.eslintrc +++ b/client/.eslintrc @@ -7,7 +7,6 @@ "plugins": [ "react", "testing-library", - "jest", "simple-import-sort" ], "rules": { diff --git a/client/package.json b/client/package.json index f3f7c9ae5..f26aea432 100644 --- a/client/package.json +++ b/client/package.json @@ -1,6 +1,6 @@ { "name": "ontime-ui", - "version": "1.6.0", + "version": "1.7.1", "private": true, "dependencies": { "@chakra-ui/react": "^2.0.0-next.3", @@ -10,8 +10,10 @@ "@emotion/react": "^11.7.1", "@emotion/styled": "^11.6.0", "@react-icons/all-files": "^4.1.0", + "@tanstack/react-query": "^4.1.3", + "@tanstack/react-query-devtools": "^4.0.10", "autosize": "^5.0.1", - "axios": "^0.25.0", + "axios": "^0.27.2", "color": "^4.2.3", "framer-motion": "^6.3.3", "jotai": "^1.7.8", @@ -21,10 +23,9 @@ "react-dom": "^18.1.0", "react-fast-compare": "^3.2.0", "react-qr-code": "^2.0.5", - "react-query": "^3.38.0", "react-router-dom": "^6.3.0", "react-table": "^7.7.0", - "socket.io-client": "^4.5.0", + "socket.io-client": "^4.5.1", "typeface-open-sans": "^1.1.13", "use-fit-text": "^2.4.0", "web-vitals": "^2.1.4" diff --git a/client/src/App.jsx b/client/src/App.jsx index 249adea40..4f7da5705 100644 --- a/client/src/App.jsx +++ b/client/src/App.jsx @@ -1,7 +1,8 @@ import React, { Suspense, useCallback, useEffect } from 'react'; -import { QueryClient, QueryClientProvider } from 'react-query'; import { BrowserRouter } from 'react-router-dom'; import { ChakraProvider } from '@chakra-ui/react'; +import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; +import { ReactQueryDevtools } from '@tanstack/react-query-devtools'; import ErrorBoundary from 'common/components/errorBoundary/ErrorBoundary'; import { AppContextProvider } from './common/context/AppContext'; @@ -55,6 +56,7 @@ function App() { + diff --git a/client/src/AppRouter.jsx b/client/src/AppRouter.jsx index ba0bebeb6..0036769f8 100644 --- a/client/src/AppRouter.jsx +++ b/client/src/AppRouter.jsx @@ -4,6 +4,7 @@ import { Route, Routes, useLocation, useNavigate } from 'react-router-dom'; import { ALIASES } from './common/api/apiConstants'; import { getAliases } from './common/api/ontimeApi'; import { useFetch } from './common/hooks/useFetch'; +import { useSocketProvider } from './common/hooks/useSocketProvider'; import withSocket from './features/viewers/ViewWrapper'; const Editor = lazy(() => import('features/editors/ProtectedEditor')); @@ -35,6 +36,7 @@ const MessageControl = lazy(() => import('features/control/message/MessageContro const Info = lazy(() => import('features/info/InfoExport')); export default function AppRouter() { + useSocketProvider(); const { data } = useFetch(ALIASES, getAliases); const location = useLocation(); const navigate = useNavigate(); diff --git a/client/src/__mocks__/QueryClient.mock.js b/client/src/__mocks__/QueryClient.mock.js index d39682c74..00a9653a2 100644 --- a/client/src/__mocks__/QueryClient.mock.js +++ b/client/src/__mocks__/QueryClient.mock.js @@ -1,4 +1,4 @@ -import {QueryClient} from "react-query"; +import { QueryClient } from '@tanstack/react-query'; export const queryClientMock = new QueryClient({ defaultOptions: { @@ -6,4 +6,4 @@ export const queryClientMock = new QueryClient({ retry: false, }, }, -}); \ No newline at end of file +}); diff --git a/client/src/common/api/apiConstants.js b/client/src/common/api/apiConstants.js index 0548600fe..d4257dc39 100644 --- a/client/src/common/api/apiConstants.js +++ b/client/src/common/api/apiConstants.js @@ -1,11 +1,18 @@ export const STATIC_PORT = 4001; -export const EVENT_TABLE = 'event'; -export const ALIASES = 'aliases'; -export const USERFIELDS = 'userFields'; -export const EVENTS_TABLE = 'events'; -export const APP_TABLE = 'appinfo'; -export const OSC_SETTINGS = 'oscSettings'; -export const APP_SETTINGS = 'appSettings'; +export const EVENT_TABLE = ['event']; +export const ALIASES = ['aliases']; +export const USERFIELDS = ['userFields']; +export const EVENTS_TABLE = ['events']; +export const APP_TABLE = ['appinfo']; +export const OSC_SETTINGS = ['oscSettings']; +export const APP_SETTINGS = ['appSettings']; + +export const FEAT_EVENTLIST = ['feat-eventList']; +export const FEAT_MESSAGECONTROL = ['feat-messagecontrol']; +export const FEAT_PLAYBACKCONTROL = ['feat-playbackcontrol']; +export const FEAT_INFO = ['feat-info']; +export const FEAT_CUESHEET = ['feat-cuesheet']; +export const TIMER = ['ontime-timer']; /** * @description finds server path given the current location, it diff --git a/client/src/common/components/countdown/TimerDisplay.jsx b/client/src/common/components/countdown/TimerDisplay.jsx index b7bd25a6e..ae2dbbc07 100644 --- a/client/src/common/components/countdown/TimerDisplay.jsx +++ b/client/src/common/components/countdown/TimerDisplay.jsx @@ -5,9 +5,11 @@ import PropTypes from 'prop-types'; import styles from './TimerDisplay.module.scss'; const TimerDisplay = ({ time, small, isNegative, hideZeroHours }) => { - // prepare display string + const display = - time != null && !isNaN(time) ? formatDisplay(time, hideZeroHours) : '-- : -- : --'; + (time === null || typeof time === 'undefined' || isNaN(time)) + ? '-- : -- : --' + : formatDisplay(time, hideZeroHours); const classes = `${small ? styles.countdownClockSmall : styles.countdownClock} ${isNegative ? styles.negative : ''}`; diff --git a/client/src/common/hooks/useFetch.js b/client/src/common/hooks/useFetch.js index 49e94bc65..7cab3a892 100644 --- a/client/src/common/hooks/useFetch.js +++ b/client/src/common/hooks/useFetch.js @@ -1,4 +1,5 @@ -import { useQuery } from 'react-query'; +import { useQuery } from '@tanstack/react-query'; + const refetchIntervalMs = 10000; /** @@ -10,7 +11,6 @@ export const useFetch = (namespace, fn) => { const { data, status, isError, refetch } = useQuery(namespace, fn, { refetchInterval: refetchIntervalMs, cacheTime: Infinity, - notifyOnChangeProps: 'tracked', }); return { data, status, isError, refetch }; diff --git a/client/src/common/hooks/useMutateEvents.js b/client/src/common/hooks/useMutateEvents.js index ef4a40285..66086ecfe 100644 --- a/client/src/common/hooks/useMutateEvents.js +++ b/client/src/common/hooks/useMutateEvents.js @@ -1,4 +1,4 @@ -import { useMutation, useQueryClient } from 'react-query'; +import { useMutation, useQueryClient } from '@tanstack/react-query'; import { EVENTS_TABLE } from '../api/apiConstants'; diff --git a/client/src/common/hooks/useSocketProvider.js b/client/src/common/hooks/useSocketProvider.js new file mode 100644 index 000000000..daa79dbb0 --- /dev/null +++ b/client/src/common/hooks/useSocketProvider.js @@ -0,0 +1,239 @@ +import { useCallback, useEffect, useMemo } from 'react'; +import { useQuery, useQueryClient } from '@tanstack/react-query'; + +import { + FEAT_CUESHEET, + FEAT_EVENTLIST, + FEAT_INFO, + FEAT_MESSAGECONTROL, + FEAT_PLAYBACKCONTROL, + TIMER, +} from '../api/apiConstants'; +import { useSocket } from '../context/socketContext'; + +export const useEventListProvider = () => { + const { data } = useQuery(FEAT_EVENTLIST, () => undefined, { + cacheTime: Infinity, + staleTime: Infinity, + }); + const placeholder = useMemo( + () => ({ + selectedEventId: null, + nextEventId: null, + }), + [] + ); + return data ?? placeholder; +}; + +export const useMessageControlProvider = () => { + const socket = useSocket(); + const { data } = useQuery(FEAT_MESSAGECONTROL, () => undefined, { + cacheTime: Infinity, + staleTime: Infinity, + }); + const placeholder = useMemo( + () => ({ + presenter: { + text: '', + visible: false, + }, + public: { + text: '', + visible: false, + }, + lower: { + text: '', + visible: false, + }, + onAir: false, + }), + [] + ); + + const returnData = data ?? placeholder; + + const setMessage = useMemo( + () => ({ + presenterText: (payload) => socket.emit('set-timer-message-text', payload), + presenterVisible: (payload) => socket.emit('set-timer-message-visible', payload), + publicText: (payload) => socket.emit('set-public-message-text', payload), + publicVisible: (payload) => socket.emit('set-public-message-visible', payload), + lowerText: (payload) => socket.emit('set-lower-message-text', payload), + lowerVisible: (payload) => socket.emit('set-lower-message-visible', payload), + onAir: (payload) => socket.emit('set-onAir', payload), + }), + [socket] + ); + + return { data: returnData, setMessage }; +}; + +export const usePlaybackControlProvider = () => { + const socket = useSocket(); + const queryClient = useQueryClient(); + const { data } = useQuery(FEAT_PLAYBACKCONTROL, () => undefined, { + cacheTime: Infinity, + staleTime: Infinity, + }); + const placeholder = useMemo( + () => ({ + playback: 'stop', + selectedEventId: null, + numEvents: 0, + }), + [] + ); + + const resetData = useCallback(() => { + queryClient.setQueryData(FEAT_PLAYBACKCONTROL, (data) => ({ + ...data, + playback: 'stop', + selectedEventId: null, + })); + }, [queryClient]); + + const setPlayback = useMemo( + () => ({ + start: () => socket.emit('set-start'), + pause: () => socket.emit('set-pause'), + roll: () => socket.emit('set-roll'), + previous: () => { + socket.emit('set-previous'); + resetData(); + }, + next: () => { + socket.emit('set-next'); + resetData(); + }, + stop: () => { + socket.emit('set-stop'); + resetData(); + }, + reload: () => { + socket.emit('set-reload'); + resetData(); + }, + delay: (amount) => { + socket.emit('set-delay', amount); + }, + }), + [resetData, socket] + ); + + const returnData = data ?? placeholder; + + return { data: returnData, resetData, setPlayback }; +}; + +export const useInfoProvider = () => { + const { data } = useQuery(FEAT_INFO, () => undefined, { + cacheTime: Infinity, + staleTime: Infinity, + }); + const placeholder = useMemo( + () => ({ + titles: { + titleNow: '', + subtitleNow: '', + presenterNow: '', + noteNow: '', + titleNext: '', + subtitleNext: '', + presenterNext: '', + noteNext: '', + }, + playback: 'stop', + selectedEventId: null, + selectedEventIndex: null, + numEvents: 0, + }), + [] + ); + return data ?? placeholder; +}; + +export const useCuesheetProvider = () => { + const { data } = useQuery(FEAT_CUESHEET, () => undefined, { + cacheTime: Infinity, + staleTime: Infinity, + }); + const placeholder = useMemo( + () => ({ + selectedEventId: null, + titleNow: '', + }), + [] + ); + + return data ?? placeholder; +}; + +export const useTimerProvider = () => { + const { data } = useQuery(TIMER, () => undefined, { + cacheTime: Infinity, + staleTime: Infinity, + }); + const placeholder = useMemo( + () => ({ + clock: 0, + current: null, + secondaryTimer: null, + duration: null, + expectedFinish: null, + startedAt: null, + }), + [] + ); + return data ?? placeholder; +}; + +export const useSocketProvider = () => { + const queryClient = useQueryClient(); + const socket = useSocket(); + + useEffect(() => { + if (!socket) { + return; + } + + socket.emit('get-ontime-feat-eventlist'); + socket.on('ontime-feat-eventlist', (featureData) => { + queryClient.setQueryData(FEAT_EVENTLIST, () => featureData); + }); + + socket.emit('get-ontime-feat-messagecontrol'); + socket.on('ontime-feat-messagecontrol', (featureData) => { + queryClient.setQueryData(FEAT_MESSAGECONTROL, () => featureData); + }); + + socket.emit('get-ontime-feat-playbackcontrol'); + socket.on('ontime-feat-playbackcontrol', (featureData) => { + queryClient.setQueryData(FEAT_PLAYBACKCONTROL, () => featureData); + }); + + socket.emit('get-ontime-feat-info'); + socket.on('ontime-feat-info', (featureData) => { + queryClient.setQueryData(FEAT_INFO, () => featureData); + }); + + socket.emit('get-ontime-feat-cuesheet'); + socket.on('ontime-feat-cuesheet', (featureData) => { + queryClient.setQueryData(FEAT_CUESHEET, () => featureData); + }); + + socket.emit('get-ontime-timer'); + socket.on('ontime-timer', (featureData) => { + queryClient.setQueryData(TIMER, () => featureData); + }); + + return () => { + socket.off('ontime-feat-eventlist'); + socket.off('ontime-feat-messagecontrol'); + socket.off('ontime-feat-playbackcontrol'); + socket.off('ontime-feat-info'); + socket.off('ontime-feat-cuesheet'); + socket.off('ontime-timer'); + }; + }, [queryClient, socket]); +}; diff --git a/client/src/features/control/__tests__/MessageControl.test.js b/client/src/features/control/__tests__/MessageControl.test.js index f04c9ed23..6c25f882c 100644 --- a/client/src/features/control/__tests__/MessageControl.test.js +++ b/client/src/features/control/__tests__/MessageControl.test.js @@ -1,16 +1,21 @@ import React from 'react'; +import { QueryClientProvider } from '@tanstack/react-query'; import { render, screen } from '@testing-library/react'; import SocketProvider from 'common/context/socketContext'; +import { queryClientMock } from '../../../__mocks__/QueryClient.mock'; import MessageControl from '../message/MessageControl'; + // need to inject the socket provider to make component // render without failing const MockMessageControl = () => { return ( - - - + + + + + ); }; diff --git a/client/src/features/control/__tests__/PlaybackControl.test.js b/client/src/features/control/__tests__/PlaybackControl.test.js index efa064528..999c4feb2 100644 --- a/client/src/features/control/__tests__/PlaybackControl.test.js +++ b/client/src/features/control/__tests__/PlaybackControl.test.js @@ -1,16 +1,21 @@ import React from 'react'; +import { QueryClientProvider } from '@tanstack/react-query'; import { render, screen } from '@testing-library/react'; import SocketProvider from 'common/context/socketContext'; +import { queryClientMock } from '../../../__mocks__/QueryClient.mock'; import PlaybackControl from '../playback/PlaybackControl'; + test('check that playback control renders', async () => { // need to inject the socket provider to make component // render without failing render( - - - + + + + + ); // Text labels for times diff --git a/client/src/features/control/message/InputRow.jsx b/client/src/features/control/message/InputRow.jsx index fd012739c..52cd57bcd 100644 --- a/client/src/features/control/message/InputRow.jsx +++ b/client/src/features/control/message/InputRow.jsx @@ -1,4 +1,4 @@ -import React from 'react'; +import React, { useEffect, useState } from 'react'; import { IconButton } from '@chakra-ui/button'; import { Editable, EditableInput, EditablePreview } from '@chakra-ui/editable'; import { Tooltip } from '@chakra-ui/tooltip'; @@ -11,14 +11,27 @@ import style from './MessageControl.module.scss'; export default function InputRow(props) { const { label, placeholder, text, visible, actionHandler, changeHandler } = props; + const [inputText, setInputText] = useState(text || ''); + + const handleInputChange = (newValue) => { + setInputText(newValue); + changeHandler(newValue); + }; + + useEffect(() => { + if (text) { + setInputText(text); + } + }, [text]); + return (
{label}
changeHandler(event)} - value={text} + onChange={(newValue) => handleInputChange(newValue)} + value={inputText} placeholder={placeholder} className={style.inline} color={text === '' ? '#666' : 'inherit'} diff --git a/client/src/features/control/message/MessageControl.jsx b/client/src/features/control/message/MessageControl.jsx index 8ea1d308a..6e27616ab 100644 --- a/client/src/features/control/message/MessageControl.jsx +++ b/client/src/features/control/message/MessageControl.jsx @@ -1,10 +1,10 @@ -import React, { useCallback, useEffect, useState } from 'react'; +import React from 'react'; import { IconButton } from '@chakra-ui/button'; import { Tooltip } from '@chakra-ui/tooltip'; import { IoMicOffOutline } from '@react-icons/all-files/io5/IoMicOffOutline'; import { IoMicSharp } from '@react-icons/all-files/io5/IoMicSharp'; -import { useSocket } from 'common/context/socketContext'; +import { useMessageControlProvider } from '../../../common/hooks/useSocketProvider'; import { tooltipDelayMid } from '../../../ontimeConfig'; import InputRow from './InputRow'; @@ -12,88 +12,7 @@ import InputRow from './InputRow'; import style from './MessageControl.module.scss'; export default function MessageControl() { - const socket = useSocket(); - const [pres, setPres] = useState({ - text: '', - visible: false, - }); - const [publ, setPubl] = useState({ - text: '', - visible: false, - }); - const [lower, setLower] = useState({ - text: '', - visible: false, - }); - const [onAir, setonAir] = useState(false); - - useEffect(() => { - if (socket == null) return; - - // Handle timer messages - socket.on('messages-timer', (data) => { - setPres({ ...data }); - }); - - // Handle public messages - socket.on('messages-public', (data) => { - setPubl({ ...data }); - }); - - // Handle lower third messages - socket.on('messages-lower', (data) => { - setLower({ ...data }); - }); - - // Handle lower third messages - socket.on('onAir', (data) => { - setonAir(data); - }); - - // Ask for up to date data - socket.emit('get-messages'); - // Ask for onAir state - socket.emit('get-onAir'); - - // Clear listeners - return () => { - socket.off('messages-public'); - socket.off('messages-timer'); - socket.off('messages-lower'); - socket.off('onAir'); - }; - }, [socket]); - - const messageControl = useCallback( - (action, payload) => { - switch (action) { - case 'pres-text': - socket.emit('set-timer-message-text', payload); - break; - case 'toggle-pres-visible': - socket.emit('set-timer-message-visible', payload); - break; - case 'publ-text': - socket.emit('set-public-message-text', payload); - break; - case 'toggle-publ-visible': - socket.emit('set-public-message-visible', payload); - break; - case 'lower-text': - socket.emit('set-lower-message-text', payload); - break; - case 'toggle-lower-visible': - socket.emit('set-lower-message-visible', payload); - break; - case 'toggle-onAir': - socket.emit('set-onAir', payload); - break; - default: - break; - } - }, - [socket] - ); + const { data, setMessage } = useMessageControlProvider(); return ( <> @@ -101,37 +20,37 @@ export default function MessageControl() { messageControl('pres-text', event)} - actionHandler={() => messageControl('toggle-pres-visible', !pres.visible)} + text={data.presenter.text} + visible={data.presenter.visible} + changeHandler={(newValue) => setMessage.presenterText(newValue)} + actionHandler={() => setMessage.presenterVisible(!data.presenter.visible)} /> messageControl('publ-text', event)} - actionHandler={() => messageControl('toggle-publ-visible', !publ.visible)} + text={data.public.text} + visible={data.public.visible} + changeHandler={(newValue) => setMessage.publicText(newValue)} + actionHandler={() => setMessage.publicVisible(!data.public.visible)} /> messageControl('lower-text', event)} - actionHandler={() => messageControl('toggle-lower-visible', !lower.visible)} + text={data.lower.text} + visible={data.lower.visible} + changeHandler={(newValue) => setMessage.lowerText(newValue)} + actionHandler={() => setMessage.lowerVisible(!data.lower.visible)} />
- + : } + icon={data.onAir ? : } colorScheme='blue' - variant={onAir ? 'solid' : 'outline'} - onClick={() => messageControl('toggle-onAir', !onAir)} + variant={data.onAir ? 'solid' : 'outline'} + onClick={() => setMessage.onAir(!data.onAir)} aria-label='Toggle On Air' /> diff --git a/client/src/features/control/message/MessageControl.module.scss b/client/src/features/control/message/MessageControl.module.scss index 3b199e751..c3f99a4c0 100644 --- a/client/src/features/control/message/MessageControl.module.scss +++ b/client/src/features/control/message/MessageControl.module.scss @@ -29,6 +29,7 @@ border-radius: 4px; background-color: $input-bg; border: $input-border; + overflow-x: hidden; } .padleft { @@ -42,11 +43,12 @@ .inputRowActive { .label { - color: $light-bg; + color: $action-blue; } .inline { background-color: $light-bg-transparent; + border-color: $action-blue; } } } diff --git a/client/src/features/control/playback/Playback.jsx b/client/src/features/control/playback/Playback.jsx index 26aee78cf..52b1ac90e 100644 --- a/client/src/features/control/playback/Playback.jsx +++ b/client/src/features/control/playback/Playback.jsx @@ -15,18 +15,18 @@ export default function Playback(props) {
playbackControl('start')} + clickhandler={() => playbackControl.start()} disabled={!selectedId || isRolling || noEvents} /> playbackControl('pause')} + clickhandler={() => playbackControl.pause()} disabled={!selectedId || isRolling || noEvents || playback !== 'start'} /> playbackControl('roll')} + clickhandler={() => playbackControl.roll()} />
); @@ -35,6 +35,10 @@ export default function Playback(props) { Playback.propTypes = { playback: PropTypes.string, selectedId: PropTypes.string, - playbackControl: PropTypes.func.isRequired, + playbackControl: PropTypes.shape({ + start: PropTypes.func, + pause: PropTypes.func, + roll: PropTypes.func, + }).isRequired, noEvents: PropTypes.bool.isRequired, }; diff --git a/client/src/features/control/playback/PlaybackButtons.jsx b/client/src/features/control/playback/PlaybackButtons.jsx index f4edf39bb..68c3b2220 100644 --- a/client/src/features/control/playback/PlaybackButtons.jsx +++ b/client/src/features/control/playback/PlaybackButtons.jsx @@ -37,6 +37,6 @@ export default memo(PlaybackButtons, areEqual); PlaybackButtons.propTypes = { playback: PropTypes.string, selectedId: PropTypes.string, - playbackControl: PropTypes.func.isRequired, + playbackControl: PropTypes.object, noEvents: PropTypes.bool.isRequired, }; diff --git a/client/src/features/control/playback/PlaybackControl.jsx b/client/src/features/control/playback/PlaybackControl.jsx index 1e36b50ee..8ae723b48 100644 --- a/client/src/features/control/playback/PlaybackControl.jsx +++ b/client/src/features/control/playback/PlaybackControl.jsx @@ -1,5 +1,6 @@ -import React, { useCallback, useEffect, useState } from 'react'; -import { useSocket } from 'common/context/socketContext'; +import React from 'react'; + +import { usePlaybackControlProvider } from '../../../common/hooks/useSocketProvider'; import PlaybackButtons from './PlaybackButtons'; import PlaybackTimer from './PlaybackTimer'; @@ -7,112 +8,20 @@ import PlaybackTimer from './PlaybackTimer'; import style from './PlaybackControl.module.scss'; export default function PlaybackControl() { - const socket = useSocket(); - const [playback, setPlayback] = useState(null); - const [timer, setTimer] = useState({ - clock: null, - running: null, - startedAt: null, - expectedFinish: null, - secondary: null, - }); - const [selectedId, setSelectedId] = useState(null); - const [numEvents, setNumEvents] = useState(0); - - const resetTimer = () => { - setTimer({ - running: null, - startedAt: null, - expectedFinish: null, - secondary: null, - }); - }; - - // handle incoming messages - useEffect(() => { - if (socket == null) return; - - socket.emit('get-timer'); - socket.emit('get-playstate'); - socket.emit('get-selected-id'); - socket.emit('get-numevents'); - - // Handle playstate - socket.on('playstate', (data) => { - setPlayback(data); - }); - - // Handle timer - socket.on('timer', (data) => { - setTimer(data); - }); - - // Handle selected event - socket.on('selected-id', (data) => { - setSelectedId(data); - }); - - socket.on('numevents', (data) => { - setNumEvents(data); - }); - - // Clear listener - return () => { - socket.off('playstate'); - socket.off('timer'); - socket.off('selected-id'); - socket.off('numevents'); - }; - }, [socket]); - - const playbackControl = useCallback( - (action) => { - switch (action) { - case 'start': - socket.emit('set-playstate', 'start'); - break; - case 'pause': - socket.emit('set-playstate', 'pause'); - break; - case 'roll': - socket.emit('set-playstate', 'roll'); - break; - case 'previous': - socket.emit('set-playstate', 'previous'); - resetTimer(); - break; - case 'next': - socket.emit('set-playstate', 'next'); - resetTimer(); - break; - case 'unload': - socket.emit('set-playstate', 'unload'); - resetTimer(); - break; - case 'reload': - socket.emit('set-playstate', 'reload'); - resetTimer(); - break; - default: - break; - } - }, - [socket] - ); + const { data, setPlayback } = usePlaybackControlProvider(); return (
socket.emit('increment-timer', amount)} + playback={data.playback} + selectedId={data.selectedEventId} + handleIncrement={(amount) => setPlayback.delay(amount)} />
); diff --git a/client/src/features/control/playback/PlaybackTimer.jsx b/client/src/features/control/playback/PlaybackTimer.jsx index 24cbec5f9..6ca163911 100644 --- a/client/src/features/control/playback/PlaybackTimer.jsx +++ b/client/src/features/control/playback/PlaybackTimer.jsx @@ -4,6 +4,8 @@ import { Tooltip } from '@chakra-ui/react'; import TimerDisplay from 'common/components/countdown/TimerDisplay'; import PropTypes from 'prop-types'; +import { useTimerProvider } from '../../../common/hooks/useSocketProvider'; +import { millisToSeconds } from '../../../common/utils/dateConfig'; import { stringFromMillis } from '../../../common/utils/time'; import { tooltipDelayMid } from '../../../ontimeConfig'; @@ -11,12 +13,7 @@ import style from './PlaybackControl.module.scss'; const areEqual = (prevProps, nextProps) => { return ( - prevProps.timer.running === nextProps.timer.running && - prevProps.timer.isNegative === nextProps.timer.isNegative && - prevProps.timer.expectedFinish === nextProps.timer.expectedFinish && - prevProps.timer.startedAt === nextProps.timer.startedAt && prevProps.playback === nextProps.playback && - prevProps.timer.secondary === nextProps.timer.secondary && prevProps.selectedId === nextProps.selectedId ); }; @@ -29,12 +26,14 @@ const incrementProps = { }; const PlaybackTimer = (props) => { - const { timer, playback, handleIncrement, selectedId } = props; - const started = stringFromMillis(timer.startedAt, true); - const finish = stringFromMillis(timer.expectedFinish, true); + const { playback, handleIncrement, selectedId } = props; + const timerData = useTimerProvider(); + const started = stringFromMillis(timerData.startedAt, true); + const finish = stringFromMillis(timerData.expectedFinish, true); const isRolling = playback === 'roll'; - const isWaiting = timer.secondary > 0 && timer.running == null; + const isWaiting = timerData.secondaryTimer > 0 && timerData.current == null; const disableButtons = selectedId == null || isRolling; + const isOvertime = timerData.current < 0; return (
@@ -42,13 +41,13 @@ const PlaybackTimer = (props) => {
-
+
@@ -118,7 +117,6 @@ const PlaybackTimer = (props) => { export default memo(PlaybackTimer, areEqual); PlaybackTimer.propTypes = { - timer: PropTypes.object.isRequired, playback: PropTypes.string, handleIncrement: PropTypes.func.isRequired, selectedId: PropTypes.string, diff --git a/client/src/features/control/playback/Transport.jsx b/client/src/features/control/playback/Transport.jsx index 417a4db45..52b402916 100644 --- a/client/src/features/control/playback/Transport.jsx +++ b/client/src/features/control/playback/Transport.jsx @@ -16,25 +16,25 @@ export default function Transport(props) { return (
playbackControl('previous')} + clickHandler={() => playbackControl.previous()} disabled={isRolling || noEvents} tooltip='Previous event' icon={} /> playbackControl('next')} + clickHandler={() => playbackControl.next()} disabled={isRolling || noEvents} tooltip='Next event' icon={} /> playbackControl('reload')} + clickHandler={() => playbackControl.reload()} disabled={selectedId == null || isRolling || noEvents} tooltip='Reload event' icon={} /> playbackControl('unload')} + clickHandler={() => playbackControl.stop()} disabled={(selectedId == null && !isRolling) || noEvents} />
@@ -44,6 +44,11 @@ export default function Transport(props) { Transport.propTypes = { playback: PropTypes.string, selectedId: PropTypes.string, - playbackControl: PropTypes.func.isRequired, + playbackControl: PropTypes.shape({ + previous: PropTypes.func, + next: PropTypes.func, + reload: PropTypes.func, + stop: PropTypes.func, + }).isRequired, noEvents: PropTypes.bool.isRequired, }; diff --git a/client/src/features/editors/list/EventList.jsx b/client/src/features/editors/list/EventList.jsx index 8e7015e7c..0d75929c7 100644 --- a/client/src/features/editors/list/EventList.jsx +++ b/client/src/features/editors/list/EventList.jsx @@ -1,11 +1,11 @@ -import React, { createRef, useCallback, useContext, useEffect, useState } from 'react'; +import React, { createRef, useCallback, useContext, useEffect } from 'react'; import { DragDropContext, Droppable } from 'react-beautiful-dnd'; import Empty from 'common/components/state/Empty'; -import { useSocket } from 'common/context/socketContext'; import { useAtomValue } from 'jotai'; import { showQuickEntryAtom } from '../../../common/atoms/LocalEventSettings'; import { CursorContext } from '../../../common/context/CursorContext'; +import { useEventListProvider } from '../../../common/hooks/useSocketProvider'; import EntryBlock from '../EntryBlock/EntryBlock'; import EventListItem from './EventListItem'; @@ -16,11 +16,9 @@ export default function EventList(props) { const { events, eventsHandler } = props; const { cursor, moveCursorUp, moveCursorDown, setCursor, isCursorLocked } = useContext(CursorContext); - const socket = useSocket(); - const [selectedId, setSelectedId] = useState(null); - const [nextId, setNextId] = useState(null); const cursorRef = createRef(); const showQuickEntry = useAtomValue(showQuickEntryAtom); + const data = useEventListProvider(); const insertAtCursor = useCallback( (type, cursor) => { @@ -90,30 +88,6 @@ export default function EventList(props) { }; }, [handleKeyPress, cursor, events, setCursor]); - // handle incoming messages - useEffect(() => { - if (socket == null) return; - - // ask for playstate - socket.emit('get-selected'); - socket.emit('get-next-id'); - - // Handle playstate - socket.on('selected', (data) => { - setSelectedId(data.id); - }); - - socket.on('next-id', (data) => { - setNextId(data); - }); - - // Clear listener - return () => { - socket.off('selected'); - socket.off('next-id'); - }; - }, [socket]); - // when cursor moves, view should follow useEffect(() => { if (cursorRef.current == null) return; @@ -129,14 +103,14 @@ export default function EventList(props) { // or cursor settings changed useEffect(() => { // and if we are locked - if (!isCursorLocked || selectedId == null) return; + if (!isCursorLocked || data.selectedEventId == null) return; // move cursor let gotoIndex = -1; let found = false; for (const e of events) { gotoIndex++; - if (e.id === selectedId) { + if (e.id === data.selectedEventId) { found = true; break; } @@ -146,7 +120,7 @@ export default function EventList(props) { setCursor(gotoIndex); } // eslint-disable-next-line react-hooks/exhaustive-deps - }, [selectedId, isCursorLocked]); + }, [data.selectedEventId, isCursorLocked]); // DND const handleOnDragEnd = useCallback( @@ -210,8 +184,8 @@ export default function EventList(props) { index={index} eventIndex={eventIndex} data={e} - selected={selectedId === e.id} - next={nextId === e.id} + selected={data.selectedEventId === e.id} + next={data.nextEventId === e.id} eventsHandler={eventsHandler} delay={cumulativeDelay} previousEnd={previousEnd} diff --git a/client/src/features/editors/list/EventListWrapper.jsx b/client/src/features/editors/list/EventListWrapper.jsx index c14559df9..63bdc3b7c 100644 --- a/client/src/features/editors/list/EventListWrapper.jsx +++ b/client/src/features/editors/list/EventListWrapper.jsx @@ -1,5 +1,5 @@ import React, { useCallback, useContext, useEffect, useState } from 'react'; -import { useMutation, useQueryClient } from 'react-query'; +import { useMutation, useQueryClient } from '@tanstack/react-query'; import { EVENTS_TABLE } from 'common/api/apiConstants'; import { fetchAllEvents, diff --git a/client/src/features/info/Info.jsx b/client/src/features/info/Info.jsx index 6d947a574..00485d9d0 100644 --- a/client/src/features/info/Info.jsx +++ b/client/src/features/info/Info.jsx @@ -1,5 +1,6 @@ -import React, { useEffect,useState } from 'react'; -import { useSocket } from 'common/context/socketContext'; +import React from 'react'; + +import { useInfoProvider } from '../../common/hooks/useSocketProvider'; import InfoLogger from './InfoLogger'; import InfoNif from './InfoNif'; @@ -8,75 +9,28 @@ import InfoTitle from './InfoTitle'; import style from './Info.module.scss'; export default function Info() { - const socket = useSocket(); - const [titles, setTitles] = useState({ - titleNow: '', - subtitleNow: '', - presenterNow: '', - noteNow: '', - titleNext: '', - subtitleNext: '', - presenterNext: '', - noteNext: '' - }); - const [selected, setSelected] = useState('No events'); - const [playback, setPlayback] = useState(null); + const data = useInfoProvider(); - // handle incoming messages - useEffect(() => { - if (socket == null) return; - - // Ask for titles - socket.emit('get-titles'); - - // Handle titles - socket.on('titles', (data) => { - setTitles(data); - }); - - // Handle playstate - socket.on('playstate', (data) => { - setPlayback(data); - }); - - // Ask for selection data - socket.emit('get-selected'); - - // Handle selection data - socket.on('selected', (data) => { - if (data.total === 0 || data.total == null) { - setSelected('No events'); - } else { - const formattedCurrent = `Event ${ - data.index != null ? data.index + 1 : '-' - }/${data.total ? data.total : '-'}`; - setSelected(formattedCurrent); - } - }); - - // Clear listener - return () => { - socket.off('titles'); - socket.off('selected'); - socket.off('playstate'); - }; - }, [socket]); - - // prepare data const titlesNow = { - title: titles.titleNow, - subtitle: titles.subtitleNow, - presenter: titles.presenterNow, - note: titles.noteNow + title: data.titles.titleNow, + subtitle: data.titles.subtitleNow, + presenter: data.titles.presenterNow, + note: data.titles.noteNow, }; const titlesNext = { - title: titles.titleNext, - subtitle: titles.subtitleNext, - presenter: titles.presenterNext, - note: titles.noteNext + title: data.titles.titleNext, + subtitle: data.titles.subtitleNext, + presenter: data.titles.presenterNext, + note: data.titles.noteNext, }; + const selected = !data.numEvents + ? 'No events' + : `Event ${data.selectedEventIndex != null ? data.selectedEventIndex + 1 : '-'}/${ + data.numEvents ? data.numEvents : '-' + }`; + return ( <>
@@ -84,8 +38,8 @@ export default function Info() { {selected}
- - + + ); diff --git a/client/src/features/info/__tests__/Info.test.js b/client/src/features/info/__tests__/Info.test.js index 4bfae6d52..da24ecb76 100644 --- a/client/src/features/info/__tests__/Info.test.js +++ b/client/src/features/info/__tests__/Info.test.js @@ -1,17 +1,16 @@ import React from 'react'; -import { QueryClient, QueryClientProvider } from 'react-query'; +import { QueryClientProvider } from '@tanstack/react-query'; import { render, screen } from '@testing-library/react'; import SocketProvider from 'common/context/socketContext'; +import { queryClientMock } from '../../../__mocks__/QueryClient.mock'; import Info from '../Info'; -const queryClient = new QueryClient(); - test('check static info render', async () => { // need to inject the socket provider to make component // render without failing render( - + diff --git a/client/src/features/menu/MenuBar.jsx b/client/src/features/menu/MenuBar.jsx index feb859aff..4151262fd 100644 --- a/client/src/features/menu/MenuBar.jsx +++ b/client/src/features/menu/MenuBar.jsx @@ -1,5 +1,4 @@ import React, { useCallback, useContext, useEffect, useRef } from 'react'; -import { useMutation, useQueryClient } from 'react-query'; import { VStack } from '@chakra-ui/react'; import { FiDownload } from '@react-icons/all-files/fi/FiDownload'; import { FiHelpCircle } from '@react-icons/all-files/fi/FiHelpCircle'; @@ -7,6 +6,7 @@ import { FiMaximize } from '@react-icons/all-files/fi/FiMaximize'; import { FiMinimize } from '@react-icons/all-files/fi/FiMinimize'; import { FiSettings } from '@react-icons/all-files/fi/FiSettings'; import { FiUpload } from '@react-icons/all-files/fi/FiUpload'; +import { useMutation, useQueryClient } from '@tanstack/react-query'; import { EVENTS_TABLE } from 'common/api/apiConstants'; import { downloadEvents, uploadEvents } from 'common/api/ontimeApi'; import PropTypes from 'prop-types'; diff --git a/client/src/features/menu/__tests__/EventListMenu.test.js b/client/src/features/menu/__tests__/EventListMenu.test.js index d65a06209..69f298e28 100644 --- a/client/src/features/menu/__tests__/EventListMenu.test.js +++ b/client/src/features/menu/__tests__/EventListMenu.test.js @@ -1,5 +1,5 @@ import React from 'react'; -import { QueryClientProvider } from 'react-query'; +import { QueryClientProvider } from '@tanstack/react-query'; import { render, screen } from '@testing-library/react'; import { queryClientMock } from '../../../__mocks__/QueryClient.mock'; diff --git a/client/src/features/menu/__tests__/MenuBar.test.js b/client/src/features/menu/__tests__/MenuBar.test.js index d65a06209..69f298e28 100644 --- a/client/src/features/menu/__tests__/MenuBar.test.js +++ b/client/src/features/menu/__tests__/MenuBar.test.js @@ -1,5 +1,5 @@ import React from 'react'; -import { QueryClientProvider } from 'react-query'; +import { QueryClientProvider } from '@tanstack/react-query'; import { render, screen } from '@testing-library/react'; import { queryClientMock } from '../../../__mocks__/QueryClient.mock'; diff --git a/client/src/features/table/OntimeTable.jsx b/client/src/features/table/OntimeTable.jsx index 02007144d..c82ef6d05 100644 --- a/client/src/features/table/OntimeTable.jsx +++ b/client/src/features/table/OntimeTable.jsx @@ -31,7 +31,7 @@ import { defaultColumnOrder, defaultHiddenColumns } from './defaults'; import style from './Table.module.scss'; -export default function OntimeTable({ tableData, userFields, handleUpdate, selectedId }) { +export default function OntimeTable({ tableData, userFields, selectedId, handleUpdate }) { const { followSelected, showSettings } = useContext(TableSettingsContext); const [columnOrder, saveColumnOrder] = useLocalStorage('table-order', defaultColumnOrder); const [columnSize, saveColumnSize] = useLocalStorage('table-sizes', {}); diff --git a/client/src/features/table/TableHeader.jsx b/client/src/features/table/TableHeader.jsx index fc75fc2ff..dae927134 100644 --- a/client/src/features/table/TableHeader.jsx +++ b/client/src/features/table/TableHeader.jsx @@ -1,4 +1,4 @@ -import React, { useContext, useEffect, useState } from 'react'; +import React, { useContext } from 'react'; import { Divider } from '@chakra-ui/layout'; import { Tooltip } from '@chakra-ui/tooltip'; import { FiSettings } from '@react-icons/all-files/fi/FiSettings'; @@ -10,10 +10,10 @@ import PropTypes from 'prop-types'; import { EVENT_TABLE } from '../../common/api/apiConstants'; import { fetchEvent } from '../../common/api/eventApi'; -import { useSocket } from '../../common/context/socketContext'; import { TableSettingsContext } from '../../common/context/TableSettingsContext'; import { useFetch } from '../../common/hooks/useFetch'; import useFullscreen from '../../common/hooks/useFullscreen'; +import { useTimerProvider } from '../../common/hooks/useSocketProvider'; import { formatDisplay } from '../../common/utils/dateConfig'; import { formatTime } from '../../common/utils/time'; import { tooltipDelayFast } from '../../ontimeConfig'; @@ -22,85 +22,18 @@ import PlaybackIcon from './tableElements/PlaybackIcon'; import style from './Table.module.scss'; -export default function TableHeader({ handleCSVExport }) { +export default function TableHeader({handleCSVExport, featureData}) { const { followSelected, showSettings, toggleTheme, toggleSettings, toggleFollow } = useContext(TableSettingsContext); - const { data } = useFetch(EVENT_TABLE, fetchEvent); + const timer = useTimerProvider(); const { isFullScreen, toggleFullScreen } = useFullscreen(); + const { data: event } = useFetch(EVENT_TABLE, fetchEvent); - const socket = useSocket(); - const [timer, setTimer] = useState({ - clock: null, - running: null, - startedAt: null, - expectedFinish: null, - secondary: null, - }); - - const [titles, setTitles] = useState({ - titleNow: '', - subtitleNow: '', - presenterNow: '', - noteNow: '', - titleNext: '', - subtitleNext: '', - presenterNext: '', - noteNext: '', - }); - - const [playback, setPlayback] = useState(''); - const [selected, setSelected] = useState(''); - - /** - * Handle incoming data from socket - */ - useEffect(() => { - if (socket == null) return; - - // Ask for titles - socket.emit('get-titles'); - - // Ask for timer - socket.emit('get-timer'); - - // Ask for playback state - socket.emit('get-playstate'); - - // Handle titles - socket.on('titles', (data) => { - setTitles(data); - }); - - // Handle timer - socket.on('timer', (data) => { - setTimer(data); - }); - - // Handle playstate - socket.on('playstate', (data) => { - setPlayback(data); - }); - - // Handle selection data - socket.on('selected', (data) => { - if (data.total === 0 || data.total == null) { - setSelected(''); - } else { - const formattedCurrent = `${data.index != null ? data.index + 1 : '-'}/${ - data.total ? data.total : '-' - }`; - setSelected(formattedCurrent); - } - }); - - // Clear listener - return () => { - socket.off('titles'); - socket.off('timer'); - socket.off('playstate'); - socket.off('selected'); - }; - }, [socket]); + const selected = !featureData.numEvents + ? 'No events' + : `Event ${featureData.selectedEventIndex != null ? featureData.selectedEventIndex + 1 : '-'}/${ + featureData.numEvents ? featureData.numEvents : '-' + }`; // prepare presentation variables const timerNow = `${timer.running < 0 ? '-' : ''}${formatDisplay(timer.running)}`; @@ -111,12 +44,12 @@ export default function TableHeader({ handleCSVExport }) { return (
-
{data?.title || ''}
-
{titles.titleNow}
+
{event?.title || ''}
+
{featureData.titleNow}
{selected}
- +
Running Timer @@ -155,7 +88,7 @@ export default function TableHeader({ handleCSVExport }) { - handleCSVExport(data)}> + handleCSVExport(event)}> CSV @@ -166,4 +99,11 @@ export default function TableHeader({ handleCSVExport }) { TableHeader.propTypes = { handleCSVExport: PropTypes.func.isRequired, + featureData: PropTypes.shape({ + playback: PropTypes.string, + selectedEventId: PropTypes.string, + selectedEventIndex: PropTypes.string, + numEvents: PropTypes.number, + titleNow: PropTypes.string, + }), }; diff --git a/client/src/features/table/TableWrapper.jsx b/client/src/features/table/TableWrapper.jsx index 71e2c5f47..c6fc583a0 100644 --- a/client/src/features/table/TableWrapper.jsx +++ b/client/src/features/table/TableWrapper.jsx @@ -1,12 +1,12 @@ -import React, { useCallback, useContext, useEffect, useState } from 'react'; +import React, { useCallback, useContext,useEffect } from 'react'; import { EVENTS_TABLE, USERFIELDS } from '../../common/api/apiConstants'; import { fetchAllEvents, requestPatch } from '../../common/api/eventsApi'; import { getUserFields } from '../../common/api/ontimeApi'; -import { useSocket } from '../../common/context/socketContext'; import { TableSettingsContext } from '../../common/context/TableSettingsContext'; import { useFetch } from '../../common/hooks/useFetch'; import useMutateEvents from '../../common/hooks/useMutateEvents'; +import { useCuesheetProvider } from '../../common/hooks/useSocketProvider'; import OntimeTable from './OntimeTable'; import TableHeader from './TableHeader'; @@ -15,35 +15,16 @@ import { makeCSV, makeTable } from './utils'; import style from './Table.module.scss'; export default function TableWrapper() { - const { data: tableData } = useFetch(EVENTS_TABLE, fetchAllEvents); + const { data: events } = useFetch(EVENTS_TABLE, fetchAllEvents); const { data: userFields } = useFetch(USERFIELDS, getUserFields); const mutation = useMutateEvents(requestPatch); - const socket = useSocket(); - const [selectedId, setSelectedId] = useState(null); const { theme } = useContext(TableSettingsContext); + const featureData = useCuesheetProvider(); // Set window title - document.title = 'ontime - Cuesheet'; - - /** - * Handle incoming data from socket - */ useEffect(() => { - if (socket == null) return; - - // Ask for selected - socket.emit('get-selected'); - - // Handle selected - socket.on('selected', (data) => { - setSelectedId(data.id); - }); - - // Clear listener - return () => { - socket.off('selected'); - }; - }, [socket]); + document.title = 'ontime - Cuesheet'; + }, []); const handleUpdate = useCallback( async (rowIndex, accessor, payload) => { @@ -51,11 +32,11 @@ export default function TableWrapper() { return; } - // check if value is the same - const event = tableData[rowIndex]; - if (event == null) { - return; - } + // check if value is the same + const event = events[rowIndex]; + if (event == null) { + return; + } if (event[accessor] === payload) { return; @@ -73,23 +54,21 @@ export default function TableWrapper() { [accessor]: cleanVal, }; - // submit - try { - await mutation.mutateAsync(mutationObject); - } catch (error) { - console.error(error); - } - }, - [mutation, tableData] - ); + // submit + try { + await mutation.mutateAsync(mutationObject); + } catch (error) { + console.error(error); + } + }, [mutation, events]); const exportHandler = useCallback( (headerData) => { - if (!headerData || !tableData || !userFields) { + if (!headerData || !events || !userFields) { return; } - const sheetData = makeTable(headerData, tableData, userFields); + const sheetData = makeTable(headerData, events, userFields); const csvContent = makeCSV(sheetData); const encodedUri = encodeURI(csvContent); const link = document.createElement('a'); @@ -98,20 +77,20 @@ export default function TableWrapper() { document.body.appendChild(link); link.click(); }, - [tableData, userFields] + [events, userFields] ); - if (typeof tableData === 'undefined' || typeof userFields === 'undefined') { + if (typeof events === 'undefined' || typeof userFields === 'undefined') { return loading...; } return (
- +
); diff --git a/client/src/theme/_main.scss b/client/src/theme/_main.scss index 23658c4ae..8e3903a0c 100644 --- a/client/src/theme/_main.scss +++ b/client/src/theme/_main.scss @@ -23,6 +23,7 @@ $ontime-pink: #ff7597; $ontime-pink-variant: #ff6969; $ontime-roll: #2b6cb0; $ontime-delay: #dd6b20; +$action-blue: #3182ce; //rgba(255, 255, 255, 0.39); - $bg-gray-700 diff --git a/client/yarn.lock b/client/yarn.lock index 5f0d2f163..506364f33 100644 --- a/client/yarn.lock +++ b/client/yarn.lock @@ -1052,7 +1052,7 @@ dependencies: regenerator-runtime "^0.13.4" -"@babel/runtime@^7.12.5", "@babel/runtime@^7.13.10", "@babel/runtime@^7.5.5", "@babel/runtime@^7.6.2", "@babel/runtime@^7.7.2": +"@babel/runtime@^7.12.5", "@babel/runtime@^7.13.10", "@babel/runtime@^7.7.2": version "7.17.2" resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.17.2.tgz#66f68591605e59da47523c631416b18508779941" integrity sha512-hzeyJyMA1YGdJTuWU0e/j4wKXrU4OMFvY2MSlaI9B7VQb0r5cxTE3EAIS2Q7Tn2RIcDkRvTA/v2JsAEhxe99uw== @@ -2339,6 +2339,36 @@ "@svgr/plugin-svgo" "^5.5.0" loader-utils "^2.0.0" +"@tanstack/match-sorter-utils@^8.0.0-alpha.82": + version "8.1.1" + resolved "https://registry.yarnpkg.com/@tanstack/match-sorter-utils/-/match-sorter-utils-8.1.1.tgz#895f407813254a46082a6bbafad9b39b943dc834" + integrity sha512-IdmEekEYxQsoLOR0XQyw3jD1GujBpRRYaGJYQUw1eOT1eUugWxdc7jomh1VQ1EKHcdwDLpLaCz/8y4KraU4T9A== + dependencies: + remove-accents "0.4.2" + +"@tanstack/query-core@^4.0.0-beta.1": + version "4.1.3" + resolved "https://registry.yarnpkg.com/@tanstack/query-core/-/query-core-4.1.3.tgz#06b24315577971376b5a15124fc132c0b3ebc1b3" + integrity sha512-mSWGovlZnEiVwC99t+Q9uLwGdV9hnq+Z2+sHNAr8tsujzMPRLFNSmWAh/AayAaxIRWPwF4Ce9dmhV2BrYmuK1A== + +"@tanstack/react-query-devtools@^4.0.10": + version "4.0.10" + resolved "https://registry.yarnpkg.com/@tanstack/react-query-devtools/-/react-query-devtools-4.0.10.tgz#d1b3e5b1917f1c22bcee5830ef7af1ccfc4879f4" + integrity sha512-3J7LLYQjfjTI0DbPo0bA+M3l4kdvYSWAqihpeG1u93WVyZj0OEFviUv+4cK7+k2AVgQJAPMZ5xvtewKxOOFVrw== + dependencies: + "@tanstack/match-sorter-utils" "^8.0.0-alpha.82" + "@types/use-sync-external-store" "^0.0.3" + use-sync-external-store "^1.2.0" + +"@tanstack/react-query@^4.1.3": + version "4.1.3" + resolved "https://registry.yarnpkg.com/@tanstack/react-query/-/react-query-4.1.3.tgz#ebe5b668e321ba63a21ef369b5d263e2466c0e14" + integrity sha512-BHI/dV3LOuVOZAaifM6u680Wi5rmiHu4aD3WkvrQT/9fYhHDhTzMdJO8l2Xh0UR7JhM8e9O3f89SJhD3T8Ejgw== + dependencies: + "@tanstack/query-core" "^4.0.0-beta.1" + "@types/use-sync-external-store" "^0.0.3" + use-sync-external-store "^1.2.0" + "@testing-library/dom@^8.5.0": version "8.13.0" resolved "https://registry.yarnpkg.com/@testing-library/dom/-/dom-8.13.0.tgz#bc00bdd64c7d8b40841e27a70211399ad3af46f5" @@ -2729,6 +2759,11 @@ resolved "https://registry.yarnpkg.com/@types/trusted-types/-/trusted-types-2.0.2.tgz#fc25ad9943bcac11cceb8168db4f275e0e72e756" integrity sha512-F5DIZ36YVLE+PN+Zwws4kJogq47hNgX3Nx6WyDJ3kcplxyke3XIzB8uK5n/Lpm1HBsbGzd6nmGehL8cPekP+Tg== +"@types/use-sync-external-store@^0.0.3": + version "0.0.3" + resolved "https://registry.yarnpkg.com/@types/use-sync-external-store/-/use-sync-external-store-0.0.3.tgz#b6725d5f4af24ace33b36fafd295136e75509f43" + integrity sha512-EwmlvuaxPNej9+T4v5AuBPJa2x2UOJVdjCtDHgcDqitUeOtjnJKJ+apYjVcAoBEMjKW1VVFGZLUb5+qqa09XFA== + "@types/ws@^8.2.2": version "8.5.3" resolved "https://registry.yarnpkg.com/@types/ws/-/ws-8.5.3.tgz#7d25a1ffbecd3c4f2d35068d0b283c037003274d" @@ -3287,12 +3322,13 @@ axe-core@^4.3.5: resolved "https://registry.yarnpkg.com/axe-core/-/axe-core-4.4.1.tgz#7dbdc25989298f9ad006645cd396782443757413" integrity sha512-gd1kmb21kwNuWr6BQz8fv6GNECPBnUasepcoLbekws23NVBLODdsClRZ+bQ8+9Uomf3Sm3+Vwn0oYG9NvwnJCw== -axios@^0.25.0: - version "0.25.0" - resolved "https://registry.yarnpkg.com/axios/-/axios-0.25.0.tgz#349cfbb31331a9b4453190791760a8d35b093e0a" - integrity sha512-cD8FOb0tRH3uuEe6+evtAbgJtfxr7ly3fQjYcMcuPlgkwVS9xboaVIpcDV+cYQe+yGykgwZCs1pzjntcGa6l5g== +axios@^0.27.2: + version "0.27.2" + resolved "https://registry.yarnpkg.com/axios/-/axios-0.27.2.tgz#207658cc8621606e586c85db4b41a750e756d972" + integrity sha512-t+yRIyySRTp/wua5xEr+z1q60QmLq8ABsS5O9Me1AsE5dfKqgnCFzwiCZZ/cGNd1lq4/7akDWMxdhVlucjmnOQ== dependencies: - follow-redirects "^1.14.7" + follow-redirects "^1.14.9" + form-data "^4.0.0" axobject-query@^2.2.0: version "2.2.0" @@ -3476,11 +3512,6 @@ bfj@^7.0.2: hoopy "^0.1.4" tryer "^1.0.1" -big-integer@^1.6.16: - version "1.6.51" - resolved "https://registry.yarnpkg.com/big-integer/-/big-integer-1.6.51.tgz#0df92a5d9880560d3ff2d5fd20245c889d130686" - integrity sha512-GPEid2Y9QU1Exl1rpO9B2IPJGHPSupF5GnVIP0blYvNOMer2bTvSWs1jGOUg04hTmu67nmLsQ9TBo1puaotBHg== - big.js@^5.2.2: version "5.2.2" resolved "https://registry.yarnpkg.com/big.js/-/big.js-5.2.2.tgz#65f0af382f578bcdc742bd9c281e9cb2d7768328" @@ -3544,20 +3575,6 @@ braces@^3.0.2, braces@~3.0.2: dependencies: fill-range "^7.0.1" -broadcast-channel@^3.4.1: - version "3.7.0" - resolved "https://registry.yarnpkg.com/broadcast-channel/-/broadcast-channel-3.7.0.tgz#2dfa5c7b4289547ac3f6705f9c00af8723889937" - integrity sha512-cIAKJXAxGJceNZGTZSBzMxzyOn72cVgPnKx4dc6LRjQgbaJUQqhy5rzL3zbMxkMWsGKkv2hSFkPRMEXfoMZ2Mg== - dependencies: - "@babel/runtime" "^7.7.2" - detect-node "^2.1.0" - js-sha3 "0.8.0" - microseconds "0.2.0" - nano-time "1.0.0" - oblivious-set "1.0.0" - rimraf "3.0.2" - unload "2.2.0" - browser-process-hrtime@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/browser-process-hrtime/-/browser-process-hrtime-1.0.0.tgz#3c9b4b7d782c8121e56f10106d84c0d0ffc94626" @@ -4423,7 +4440,7 @@ detect-node-es@^1.1.0: resolved "https://registry.yarnpkg.com/detect-node-es/-/detect-node-es-1.1.0.tgz#163acdf643330caa0b4cd7c21e7ee7755d6fa493" integrity sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ== -detect-node@^2.0.4, detect-node@^2.1.0: +detect-node@^2.0.4: version "2.1.0" resolved "https://registry.yarnpkg.com/detect-node/-/detect-node-2.1.0.tgz#c9c70775a49c3d03bc2c06d9a73be550f978f8b1" integrity sha512-T0NIuQpnTvFDATNuHN5roPwSBG83rFsuO+MXXH9/3N1eFbn4wcPjttvjMLEPWJ0RGUYgQE7cGgS3tNxbqCGM7g== @@ -5277,10 +5294,10 @@ follow-redirects@^1.0.0: resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.14.9.tgz#dd4ea157de7bfaf9ea9b3fbd85aa16951f78d8d7" integrity sha512-MQDfihBQYMcyy5dhRDJUHcw7lb2Pv/TuE6xP1vyraLukNDHKbDxDNaOE3NbCAdKQApno+GPRyo1YAp89yCjK4w== -follow-redirects@^1.14.7: - version "1.14.7" - resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.14.7.tgz#2004c02eb9436eee9a21446a6477debf17e81685" - integrity sha512-+hbxoLbFMbRKDwohX8GkTataGqO6Jb7jGwpAlwgy2bIz25XtRm7KEzJM76R1WiNT5SwZkX4Y75SwBolkpmE7iQ== +follow-redirects@^1.14.9: + version "1.15.1" + resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.15.1.tgz#0ca6a452306c9b276e4d3127483e29575e207ad5" + integrity sha512-yLAMQs+k0b2m7cVxpS1VKJVvoz7SS9Td1zss3XRwXj+ZDH00RJgnuLx7E44wx02kQLrdM3aOOy+FpzS7+8OizA== fork-ts-checker-webpack-plugin@^6.5.0: version "6.5.0" @@ -5310,6 +5327,15 @@ form-data@^3.0.0: combined-stream "^1.0.8" mime-types "^2.1.12" +form-data@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/form-data/-/form-data-4.0.0.tgz#93919daeaf361ee529584b9b31664dc12c9fa452" + integrity sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww== + dependencies: + asynckit "^0.4.0" + combined-stream "^1.0.8" + mime-types "^2.1.12" + forwarded@0.2.0: version "0.2.0" resolved "https://registry.yarnpkg.com/forwarded/-/forwarded-0.2.0.tgz#2269936428aad4c15c7ebe9779a84bf0b2a81811" @@ -6624,14 +6650,9 @@ jest@^27.4.3: jest-cli "^27.5.1" jotai@^1.7.8: - version "1.7.8" - resolved "https://registry.yarnpkg.com/jotai/-/jotai-1.7.8.tgz#1ac4daa2731e0f8e7fab8abb96aebf94b9dfc1a3" - integrity sha512-rXwWz6uLqyZUCRzWIPiYTjI1hjskVxVpDN3lBoOHi66z6uQFkFmKwR8YhcQd+Ex4lODlReuKNIx6WwsF9aKy3g== - -js-sha3@0.8.0: - version "0.8.0" - resolved "https://registry.yarnpkg.com/js-sha3/-/js-sha3-0.8.0.tgz#b9b7a5da73afad7dedd0f8c463954cbde6818840" - integrity sha512-gF1cRrHhIzNfToc802P800N8PpXS+evLLXfsVpowqmAFR9uwbi89WvXg2QspOmXL8QL86J4T1EpFu+yUkwJY3Q== + version "1.8.3" + resolved "https://registry.yarnpkg.com/jotai/-/jotai-1.8.3.tgz#06595dd66bcf64dd370386eee9c7b386c2ccb2e7" + integrity sha512-bNAs/DMD6BEB4xxVku33CI+ZIFyCWG82Ms6JnvYunEvGCnviTTDBl+m0HKgijoHmcNM3fxQ5MARdG+wwokNxog== "js-tokens@^3.0.0 || ^4.0.0", js-tokens@^4.0.0: version "4.0.0" @@ -6934,9 +6955,9 @@ lru-cache@^6.0.0: yallist "^4.0.0" luxon@^3.0.1: - version "3.0.1" - resolved "https://registry.yarnpkg.com/luxon/-/luxon-3.0.1.tgz#6901111d10ad06fd267ad4e4128a84bef8a77299" - integrity sha512-hF3kv0e5gwHQZKz4wtm4c+inDtyc7elkanAsBq+fundaCdUBNJB1dHEGUZIM6SfSBUlbVFduPwEtNjFK8wLtcw== + version "3.0.3" + resolved "https://registry.yarnpkg.com/luxon/-/luxon-3.0.3.tgz#573e65531efd3d92265feb640f02ba7a192e2388" + integrity sha512-+EfHWnF+UT7GgTnq5zXg3ldnTKL2zdv7QJgsU5bjjpbH17E3qi/puMhQyJVYuCq+FRkogvB5WB6iVvUr+E4a7w== lz-string@^1.4.4: version "1.4.4" @@ -6974,14 +6995,6 @@ map-obj@^4.0.0: resolved "https://registry.yarnpkg.com/map-obj/-/map-obj-4.3.0.tgz#9304f906e93faae70880da102a9f1df0ea8bb05a" integrity sha512-hdN1wVrZbb29eBGiGjJbeP8JbKjq1urkHJ/LIP/NY48MZ1QVXUsQBV1G1zvYFHn1XE06cwjBsOI2K3Ulnj1YXQ== -match-sorter@^6.0.2: - version "6.3.1" - resolved "https://registry.yarnpkg.com/match-sorter/-/match-sorter-6.3.1.tgz#98cc37fda756093424ddf3cbc62bfe9c75b92bda" - integrity sha512-mxybbo3pPNuA+ZuCUhm5bwNkXrJTbsk5VWbR5wiwz/GC6LIiegBGn2w3O08UG/jdbYLinw51fSQ5xNU1U3MgBw== - dependencies: - "@babel/runtime" "^7.12.5" - remove-accents "0.4.2" - mathml-tag-names@^2.1.3: version "2.1.3" resolved "https://registry.yarnpkg.com/mathml-tag-names/-/mathml-tag-names-2.1.3.tgz#4ddadd67308e780cf16a47685878ee27b736a0a3" @@ -7060,11 +7073,6 @@ micromatch@^4.0.2, micromatch@^4.0.4, micromatch@^4.0.5: braces "^3.0.2" picomatch "^2.3.1" -microseconds@0.2.0: - version "0.2.0" - resolved "https://registry.yarnpkg.com/microseconds/-/microseconds-0.2.0.tgz#233b25f50c62a65d861f978a4a4f8ec18797dc39" - integrity sha512-n7DHHMjR1avBbSpsTBj6fmMGh2AGrifVV4e+WYc3Q9lO+xnSZ3NyhcBND3vzzatt05LFhoKFRxrIyklmLlUtyA== - mime-db@1.52.0, "mime-db@>= 1.43.0 < 2": version "1.52.0" resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.52.0.tgz#bbabcdc02859f4987301c856e3387ce5ec43bf70" @@ -7167,13 +7175,6 @@ multicast-dns@^6.0.1: dns-packet "^1.3.1" thunky "^1.0.2" -nano-time@1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/nano-time/-/nano-time-1.0.0.tgz#b0554f69ad89e22d0907f7a12b0993a5d96137ef" - integrity sha1-sFVPaa2J4i0JB/ehKwmTpdlhN+8= - dependencies: - big-integer "^1.6.16" - nanoid@^3.3.1: version "3.3.1" resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.1.tgz#6347a18cac88af88f58af0b3594b723d5e99bb35" @@ -7365,11 +7366,6 @@ object.values@^1.1.0, object.values@^1.1.5: define-properties "^1.1.3" es-abstract "^1.19.1" -oblivious-set@1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/oblivious-set/-/oblivious-set-1.0.0.tgz#c8316f2c2fb6ff7b11b6158db3234c49f733c566" - integrity sha512-z+pI07qxo4c2CulUHCDf9lcqDlMSo72N/4rLUpRXf6fu+q8vjt8y0xS+Tlf8NTJDdTXHbdeO1n3MlbctwEoXZw== - obuf@^1.0.0, obuf@^1.1.2: version "1.1.2" resolved "https://registry.yarnpkg.com/obuf/-/obuf-1.1.2.tgz#09bea3343d41859ebd446292d11c9d4db619084e" @@ -8500,15 +8496,6 @@ react-qr-code@^2.0.5: prop-types "^15.7.2" qr.js "0.0.0" -react-query@^3.38.0: - version "3.38.0" - resolved "https://registry.yarnpkg.com/react-query/-/react-query-3.38.0.tgz#e7f4081e45ba47a4119e548ddcfd0fab18f0580e" - integrity sha512-VRbCTRrDfC5FsB70+JfZuxFRv9SAvkZ1h36MsN8+QaDN+NWB6s1vJndqpoLQnJqN0COTG2zsInMq0KFdYze6TA== - dependencies: - "@babel/runtime" "^7.5.5" - broadcast-channel "^3.4.1" - match-sorter "^6.0.2" - react-redux@^7.2.0: version "7.2.4" resolved "https://registry.yarnpkg.com/react-redux/-/react-redux-7.2.4.tgz#1ebb474032b72d806de2e0519cd07761e222e225" @@ -8899,7 +8886,7 @@ reusify@^1.0.4: resolved "https://registry.yarnpkg.com/reusify/-/reusify-1.0.4.tgz#90da382b1e126efc02146e90845a88db12925d76" integrity sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw== -rimraf@3.0.2, rimraf@^3.0.0, rimraf@^3.0.2: +rimraf@^3.0.0, rimraf@^3.0.2: version "3.0.2" resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-3.0.2.tgz#f1a5402ba6220ad52cc1282bac1ae3aa49fd061a" integrity sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA== @@ -9192,10 +9179,10 @@ slice-ansi@^4.0.0: astral-regex "^2.0.0" is-fullwidth-code-point "^3.0.0" -socket.io-client@^4.5.0: - version "4.5.0" - resolved "https://registry.yarnpkg.com/socket.io-client/-/socket.io-client-4.5.0.tgz#3858b6183bab31c5c4eaf3efd0fa50840ebb4504" - integrity sha512-HW61c1G7OrYGxaI79WRn17+b03iBCdvhBj4iqyXHBoL5M8w2MSO/vChsjA93knG4GYEai1/vbXWJna9dzxXtSg== +socket.io-client@^4.5.1: + version "4.5.1" + resolved "https://registry.yarnpkg.com/socket.io-client/-/socket.io-client-4.5.1.tgz#cab8da71976a300d3090414e28c2203a47884d84" + integrity sha512-e6nLVgiRYatS+AHXnOnGi4ocOpubvOUCGhyWw8v+/FxW8saHkinG6Dfhi9TU0Kt/8mwJIAASxvw6eujQmjdZVA== dependencies: "@socket.io/component-emitter" "~3.1.0" debug "~4.3.2" @@ -10031,14 +10018,6 @@ universalify@^2.0.0: resolved "https://registry.yarnpkg.com/universalify/-/universalify-2.0.0.tgz#75a4984efedc4b08975c5aeb73f530d02df25717" integrity sha512-hAZsKq7Yy11Zu1DE0OzWjw7nnLZmJZYTDZZyEFHZdUhV8FkH5MCfoU1XMaxXovpyW5nq5scPqq0ZDP9Zyl04oQ== -unload@2.2.0: - version "2.2.0" - resolved "https://registry.yarnpkg.com/unload/-/unload-2.2.0.tgz#ccc88fdcad345faa06a92039ec0f80b488880ef7" - integrity sha512-B60uB5TNBLtN6/LsgAf3udH9saB5p7gqJwcFfbOEZ8BcBHnGwCf6G/TGiEqkRAxX7zAFIUtzdrXQSdL3Q/wqNA== - dependencies: - "@babel/runtime" "^7.6.2" - detect-node "^2.0.4" - unpipe@1.0.0, unpipe@~1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/unpipe/-/unpipe-1.0.0.tgz#b2bf4ee8514aae6165b4817829d21b2ef49904ec" @@ -10086,6 +10065,11 @@ use-sidecar@^1.0.1, use-sidecar@^1.0.5: detect-node-es "^1.1.0" tslib "^1.9.3" +use-sync-external-store@^1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/use-sync-external-store/-/use-sync-external-store-1.2.0.tgz#7dbefd6ef3fe4e767a0cf5d7287aacfb5846928a" + integrity sha512-eEgnFxGQ1Ife9bzYs6VLi8/4X6CObHMw9Qr9tPY43iKwsPw8xE8+EFsf/2cFZ5S3esXgpWgtSCtLNS41F+sKPA== + util-deprecate@^1.0.1, util-deprecate@^1.0.2, util-deprecate@~1.0.1: version "1.0.2" resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf" diff --git a/server/package.json b/server/package.json index 892d41fce..ead1c1365 100644 --- a/server/package.json +++ b/server/package.json @@ -1,6 +1,6 @@ { "name": "ontime", - "version": "1.7.0", + "version": "1.7.1", "author": "Carlos Valente", "description": "Time keeping for live events", "repository": "https://github.com/cpvalente/ontime", diff --git a/server/src/classes/timer/EventTimer.js b/server/src/classes/timer/EventTimer.js index b4c580498..c9c336c4f 100644 --- a/server/src/classes/timer/EventTimer.js +++ b/server/src/classes/timer/EventTimer.js @@ -149,10 +149,99 @@ export class EventTimer extends Timer { this.io.emit('timer', this.getTimeObject()); } + /** + * @description Broadcast timer data + * @private + */ + _broadcastFeatureTimer() { + const featureData = { + clock: this.clock, + current: this.current, + secondaryTimer: this.secondaryTimer, + duration: this.duration, + expectedFinish: this._getExpectedFinish(), + startedAt: this._startedAt, + }; + this.io.emit('ontime-timer', featureData); + } + + /** + * @description Broadcast data for Event List feature + * @private + */ + _broadcastFeatureEventList() { + const featureData = { + selectedEventId: this.selectedEventId, + nextEventId: this.nextEventId, + }; + this.io.emit('ontime-feat-eventlist', featureData); + } + + /** + * @description Broadcast data for Message Control feature + * @private + */ + _broadcastFeatureMessageControl() { + const featureData = { + presenter: this.presenter, + public: this.public, + lower: this.lower, + onAir: this.onAir, + }; + this.io.emit('ontime-feat-messagecontrol', featureData); + } + + /** + * @description Broadcast data for Playback Control feature + * @private + */ + _broadcastFeaturePlaybackControl() { + const featureData = { + playback: this.state, + selectedEventId: this.selectedEventId, + numEvents: this._eventlist.length, + }; + this.io.emit('ontime-feat-playbackcontrol', featureData); + } + + /** + * @description Broadcast data for Info feature + * @private + */ + _broadcastFeatureInfo() { + const featureData = { + titles: this.titles, + playback: this.state, + selectedEventId: this.selectedEventId, + selectedEventIndex: this.selectedEventIndex, + numEvents: this._eventlist.length, + }; + this.io.emit('ontime-feat-info', featureData); + } + + _broadcastFeatureCuesheet() { + const featureData = { + playback: this.state, + selectedEventId: this.selectedEventId, + selectedEventIndex: this.selectedEventIndex, + numEvents: this._eventlist.length, + titleNow: this.titles.titleNow, + }; + this.io.emit('ontime-feat-cuesheet', featureData); + } + /** * Broadcasts complete object state */ broadcastState() { + // feature sync + this._broadcastFeatureEventList(); + this._broadcastFeatureMessageControl(); + this._broadcastFeaturePlaybackControl(); + this._broadcastFeatureInfo(); + this._broadcastFeatureCuesheet(); + this._broadcastFeatureTimer(); + const numEvents = this._eventlist.length; this.broadcastTimer(); this.io.emit('playstate', this.state); @@ -333,9 +422,6 @@ export class EventTimer extends Timer { } break; case 'onLoad': - // broadcast change - this.broadcastState(); - // check integrations - http if (h?.onLoad?.enabled) { if (h?.onLoad?.url != null || h?.onLoad?.url !== '') { @@ -347,8 +433,6 @@ export class EventTimer extends Timer { this.ontimeCycle = this.cycleState.armed; break; 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) { @@ -368,8 +452,6 @@ export class EventTimer extends Timer { case 'onUpdate': // call update this.update(); - // broadcast current state - this.broadcastTimer(); // through OSC, only if running if (this.state === 'start' || this.state === 'roll') { if (this.current != null && this.secondaryTimer == null) { @@ -389,9 +471,6 @@ export class EventTimer extends Timer { break; case 'onPause': - // broadcast current state - this.broadcastState(); - // send OSC if (this.prevCycle === this.cycleState.onUpdate) { this.sendOsc(this.osc.implemented.pause); @@ -409,9 +488,6 @@ export class EventTimer extends Timer { break; case 'onStop': - // broadcast change - this.broadcastState(); - // send OSC if something was actually stopped if (this.prevCycle === this.cycleState.onUpdate) { this.sendOsc(this.osc.implemented.stop); @@ -428,8 +504,6 @@ export class EventTimer extends Timer { this.ontimeCycle = this.cycleState.idle; break; case 'onFinish': - // broadcast change - this.broadcastState(); // finished an event this.sendOsc(this.osc.implemented.finished); @@ -464,6 +538,7 @@ export class EventTimer extends Timer { // update this.update(); + this.broadcastState(); // reset cycle this.prevCycle = this.ontimeCycle; @@ -472,6 +547,8 @@ export class EventTimer extends Timer { update() { // if there is nothing selected, update clock this.clock = this._getCurrentTime(); + this._broadcastFeatureTimer(); + this.broadcastTimer(); // if we are not updating, send the timers if (this.ontimeCycle !== this.cycleState.onUpdate) { @@ -694,10 +771,11 @@ export class EventTimer extends Timer { try { const d = JSON.parse(data); this.onAir = !!d; - this.broadcastThis('onAir', this.onAir); } catch (error) { this.error('RX', `Failed to parse message ${data}`); } + this.broadcastThis('onAir', this.onAir); + this._broadcastFeatureMessageControl(); }); /*******************************************/ @@ -712,15 +790,13 @@ export class EventTimer extends Timer { }); /*******************************************/ - // timer - socket.on('get-current', () => { - socket.emit('current', this.getCurrentInSeconds()); - }); + // ** TO BE DEPRECATED ** // socket.on('get-timer', () => { socket.emit('timer', this.getTimeObject()); }); + // ** TO BE DEPRECATED IN FAVOR OF DELAY ** // socket.on('increment-timer', (data) => { if (isNaN(parseInt(data, 10))) return; if (data < -5 || data > 5) return; @@ -731,6 +807,8 @@ export class EventTimer extends Timer { // playstate socket.on('set-playstate', (data) => { this.trigger(data); + this._broadcastFeaturePlaybackControl(); + this._broadcastFeatureInfo(); }); socket.on('get-playstate', () => { @@ -755,28 +833,15 @@ export class EventTimer extends Timer { socket.emit('selected-id', this.selectedEventId); }); - socket.on('get-numevents', () => { - socket.emit('numevents', this._eventlist.length); - }); - socket.on('get-next-id', () => { socket.emit('next-id', this.nextEventId); }); - socket.on('get-publicselected-id', () => { - socket.emit('publicselected-id', this.selectedPublicEventId); - }); - - socket.on('get-publicnext-id', () => { - socket.emit('publicnext-id', this.nextPublicEventId); - }); - // title data socket.on('get-titles', () => { socket.emit('titles', this.titles); }); - // title data socket.on('get-publictitles', () => { socket.emit('publictitles', this.titlesPublic); }); @@ -788,51 +853,79 @@ export class EventTimer extends Timer { /*******************************************/ - // Messages - socket.on('get-messages', () => { - this.broadcastThis('messages-timer', this.presenter); - this.broadcastThis('messages-public', this.public); - this.broadcastThis('messages-lower', this.lower); - }); - // Presenter message socket.on('set-timer-message-text', (data) => { this._setTitles('set-timer-text', data); + this._broadcastFeatureMessageControl(); }); socket.on('set-timer-message-visible', (data) => { this._setTitles('set-timer-visible', data); + this._broadcastFeatureMessageControl(); }); - socket.on('get-timer', () => { - this.broadcastThis('messages-timer', this.presenter); - }); /*******************************************/ // Public message socket.on('set-public-message-text', (data) => { this._setTitles('set-public-text', data); + this._broadcastFeatureMessageControl(); }); socket.on('set-public-message-visible', (data) => { this._setTitles('set-public-visible', data); - }); - - socket.on('get-public', () => { - socket.emit('messages-public', this.public); + this._broadcastFeatureMessageControl(); }); /*******************************************/ // Lower third message socket.on('set-lower-message-text', (data) => { this._setTitles('set-lower-text', data); + this._broadcastFeatureMessageControl(); }); socket.on('set-lower-message-visible', (data) => { this._setTitles('set-lower-visible', data); + this._broadcastFeatureMessageControl(); }); - socket.on('get-lower', () => { - socket.emit('messages-lower', this.lower); + /* MOLECULAR ENDPOINTS + * ===================== + * 1. EVENT LIST + * 2. MESSAGE CONTROL + * 3. PLAYBACK CONTROL + * 4. INFO + * 5. CUESHEET + * 6. TIMER OBJECT + * */ + + // 1. EVENT LIST + socket.on('get-ontime-feat-eventlist', () => { + this._broadcastFeatureEventList(); + }); + + // 2. MESSAGE CONTROL + socket.on('get-ontime-feat-messagecontrol', () => { + this._broadcastFeatureMessageControl(); + }); + + // 3. PLAYBACK CONTROL + socket.on('get-ontime-feat-playbackcontrol', () => { + this._broadcastFeaturePlaybackControl(); + }); + + // 4. INFO + socket.on('get-ontime-feat-info', () => { + this._broadcastFeatureInfo(); + }); + + // 5. CUE SHEET + socket.on('get-ontime-feat-cuesheet', () => { + this._broadcastFeatureCuesheet(); + }); + + // 6. TIMER + socket.on('get-ontime-timer', () => { + this._broadcastFeatureTimer(); }); }); } @@ -1394,6 +1487,7 @@ export class EventTimer extends Timer { // call super super.stop(); + this._resetSelection(); // update lifecycle: onPause this.ontimeCycle = this.cycleState.onStop; diff --git a/server/src/package.json b/server/src/package.json index 27f78b441..8afe05309 100644 --- a/server/src/package.json +++ b/server/src/package.json @@ -1,7 +1,7 @@ { "name": "ontime-server", "type": "module", - "version": "1.2.1", + "version": "1.3.0", "dependencies": { "body-parser": "^1.20.0", "dotenv": "^16.0.1",