mirror of
https://github.com/cpvalente/ontime.git
synced 2026-08-19 22:24:11 +00:00
V2 ws store (#310)
* Update TimerService.ts * refactor: message service publishes to store * refactor: several type improvements * V2 ws store wss (#309) * refactor: shared logging types * refactor: simplify message service consumption * refactor: create discrete logging system * refactor: move socket.io > websocket
This commit is contained in:
@@ -18,9 +18,10 @@
|
|||||||
"axios": "^1.2.0",
|
"axios": "^1.2.0",
|
||||||
"color": "^4.2.3",
|
"color": "^4.2.3",
|
||||||
"csv-stringify": "^6.2.3",
|
"csv-stringify": "^6.2.3",
|
||||||
|
"deepmerge": "^4.3.0",
|
||||||
"framer-motion": "^8.0.2",
|
"framer-motion": "^8.0.2",
|
||||||
"jotai": "^1.10.0",
|
"jotai": "^1.10.0",
|
||||||
"luxon": "^3.1.0",
|
"luxon": "^3.3.0",
|
||||||
"react": "^18.2.0",
|
"react": "^18.2.0",
|
||||||
"react-beautiful-dnd": "^13.1.1",
|
"react-beautiful-dnd": "^13.1.1",
|
||||||
"react-dom": "^18.2.0",
|
"react-dom": "^18.2.0",
|
||||||
@@ -29,9 +30,10 @@
|
|||||||
"react-qr-code": "^2.0.11",
|
"react-qr-code": "^2.0.11",
|
||||||
"react-router-dom": "^6.3.0",
|
"react-router-dom": "^6.3.0",
|
||||||
"react-table": "^7.7.0",
|
"react-table": "^7.7.0",
|
||||||
"socket.io-client": "^4.5.4",
|
"react-use-websocket": "^4.3.1",
|
||||||
"typeface-open-sans": "^1.1.13",
|
"typeface-open-sans": "^1.1.13",
|
||||||
"web-vitals": "^3.1.1"
|
"web-vitals": "^3.1.1",
|
||||||
|
"zustand": "^4.3.6"
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"addversion": "node -p \"'export const ONTIME_VERSION = ' + JSON.stringify(require('../../package.json').version) + ';'\" > src/ONTIME_VERSION.js",
|
"addversion": "node -p \"'export const ONTIME_VERSION = ' + JSON.stringify(require('../../package.json').version) + ';'\" > src/ONTIME_VERSION.js",
|
||||||
@@ -63,6 +65,7 @@
|
|||||||
"@testing-library/react": "^13.1.1",
|
"@testing-library/react": "^13.1.1",
|
||||||
"@testing-library/user-event": "^14.1.1",
|
"@testing-library/user-event": "^14.1.1",
|
||||||
"@types/color": "^3.0.3",
|
"@types/color": "^3.0.3",
|
||||||
|
"@types/luxon": "^3.2.0",
|
||||||
"@types/prop-types": "^15.7.5",
|
"@types/prop-types": "^15.7.5",
|
||||||
"@types/react": "^18.0.26",
|
"@types/react": "^18.0.26",
|
||||||
"@types/react-beautiful-dnd": "^13.1.3",
|
"@types/react-beautiful-dnd": "^13.1.3",
|
||||||
|
|||||||
+27
-27
@@ -6,9 +6,9 @@ import { ReactQueryDevtools } from '@tanstack/react-query-devtools';
|
|||||||
|
|
||||||
import ErrorBoundary from './common/components/error-boundary/ErrorBoundary';
|
import ErrorBoundary from './common/components/error-boundary/ErrorBoundary';
|
||||||
import { AppContextProvider } from './common/context/AppContext';
|
import { AppContextProvider } from './common/context/AppContext';
|
||||||
import { LoggingProvider } from './common/context/LoggingContext';
|
|
||||||
import useElectronEvent from './common/hooks/useElectronEvent';
|
import useElectronEvent from './common/hooks/useElectronEvent';
|
||||||
import { ontimeQueryClient } from './common/queryClient';
|
import { ontimeQueryClient } from './common/queryClient';
|
||||||
|
import { connectSocket } from './common/utils/socket';
|
||||||
import theme from './theme/theme';
|
import theme from './theme/theme';
|
||||||
import AppRouter from './AppRouter';
|
import AppRouter from './AppRouter';
|
||||||
|
|
||||||
@@ -16,20 +16,22 @@ import AppRouter from './AppRouter';
|
|||||||
// @ts-expect-error no types from font import
|
// @ts-expect-error no types from font import
|
||||||
import('typeface-open-sans');
|
import('typeface-open-sans');
|
||||||
|
|
||||||
|
connectSocket();
|
||||||
|
|
||||||
function App() {
|
function App() {
|
||||||
const { isElectron, sendToElectron } = useElectronEvent();
|
const { isElectron, sendToElectron } = useElectronEvent();
|
||||||
|
|
||||||
const handleKeyPress = (event:KeyboardEvent) => {
|
const handleKeyPress = (event: KeyboardEvent) => {
|
||||||
// handle held key
|
// handle held key
|
||||||
if (event.repeat) return;
|
if (event.repeat) return;
|
||||||
// check if the alt key is pressed
|
// check if the alt key is pressed
|
||||||
if (event.altKey) {
|
if (event.altKey) {
|
||||||
if (event.code === 'KeyT') {
|
if (event.code === 'KeyT') {
|
||||||
// ask to see debug
|
// ask to see debug
|
||||||
sendToElectron('set-window', 'show-dev');
|
sendToElectron('set-window', 'show-dev');
|
||||||
}
|
|
||||||
}
|
}
|
||||||
};
|
}
|
||||||
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (isElectron) {
|
if (isElectron) {
|
||||||
@@ -44,22 +46,20 @@ function App() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<ChakraProvider resetCSS theme={theme}>
|
<ChakraProvider resetCSS theme={theme}>
|
||||||
<LoggingProvider>
|
<QueryClientProvider client={ontimeQueryClient}>
|
||||||
<QueryClientProvider client={ontimeQueryClient}>
|
<AppContextProvider>
|
||||||
<AppContextProvider>
|
<BrowserRouter>
|
||||||
<BrowserRouter>
|
<div className='App'>
|
||||||
<div className='App'>
|
<ErrorBoundary>
|
||||||
<ErrorBoundary>
|
<Suspense fallback={null}>
|
||||||
<Suspense fallback={null}>
|
<AppRouter />
|
||||||
<AppRouter />
|
</Suspense>
|
||||||
</Suspense>
|
</ErrorBoundary>
|
||||||
</ErrorBoundary>
|
<ReactQueryDevtools initialIsOpen={false} />
|
||||||
<ReactQueryDevtools initialIsOpen={false} />
|
</div>
|
||||||
</div>
|
</BrowserRouter>
|
||||||
</BrowserRouter>
|
</AppContextProvider>
|
||||||
</AppContextProvider>
|
</QueryClientProvider>
|
||||||
</QueryClientProvider>
|
|
||||||
</LoggingProvider>
|
|
||||||
</ChakraProvider>
|
</ChakraProvider>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { lazy, useEffect } from 'react';
|
|||||||
import { Navigate, Route, Routes, useLocation, useNavigate } from 'react-router-dom';
|
import { Navigate, Route, Routes, useLocation, useNavigate } from 'react-router-dom';
|
||||||
|
|
||||||
import useAliases from './common/hooks-query/useAliases';
|
import useAliases from './common/hooks-query/useAliases';
|
||||||
import withSocket from './features/viewers/ViewWrapper';
|
import withData from './features/viewers/ViewWrapper';
|
||||||
|
|
||||||
const Editor = lazy(() => import('./features/editors/ProtectedEditor'));
|
const Editor = lazy(() => import('./features/editors/ProtectedEditor'));
|
||||||
const Table = lazy(() => import('./features/table/ProtectedTable'));
|
const Table = lazy(() => import('./features/table/ProtectedTable'));
|
||||||
@@ -17,14 +17,14 @@ const Public = lazy(() => import('./features/viewers/public/Public'));
|
|||||||
const Lower = lazy(() => import('./features/viewers/lower-thirds/LowerWrapper'));
|
const Lower = lazy(() => import('./features/viewers/lower-thirds/LowerWrapper'));
|
||||||
const StudioClock = lazy(() => import('./features/viewers/studio/StudioClock'));
|
const StudioClock = lazy(() => import('./features/viewers/studio/StudioClock'));
|
||||||
|
|
||||||
const STimer = withSocket(TimerView);
|
const STimer = withData(TimerView);
|
||||||
const SMinimalTimer = withSocket(MinimalTimerView);
|
const SMinimalTimer = withData(MinimalTimerView);
|
||||||
const SClock = withSocket(ClockView);
|
const SClock = withData(ClockView);
|
||||||
const SCountdown = withSocket(Countdown);
|
const SCountdown = withData(Countdown);
|
||||||
const SBackstage = withSocket(Backstage);
|
const SBackstage = withData(Backstage);
|
||||||
const SPublic = withSocket(Public);
|
const SPublic = withData(Public);
|
||||||
const SLowerThird = withSocket(Lower);
|
const SLowerThird = withData(Lower);
|
||||||
const SStudio = withSocket(StudioClock);
|
const SStudio = withData(StudioClock);
|
||||||
|
|
||||||
const FeatureWrapper = lazy(() => import('./features/FeatureWrapper'));
|
const FeatureWrapper = lazy(() => import('./features/FeatureWrapper'));
|
||||||
const RundownPanel = lazy(() => import('./features/rundown/RundownExport'));
|
const RundownPanel = lazy(() => import('./features/rundown/RundownExport'));
|
||||||
|
|||||||
@@ -10,14 +10,10 @@ export const APP_INFO = ['appinfo'];
|
|||||||
export const OSC_SETTINGS = ['oscSettings'];
|
export const OSC_SETTINGS = ['oscSettings'];
|
||||||
export const APP_SETTINGS = ['appSettings'];
|
export const APP_SETTINGS = ['appSettings'];
|
||||||
export const VIEW_SETTINGS = ['viewSettings'];
|
export const VIEW_SETTINGS = ['viewSettings'];
|
||||||
|
export const RUNTIME = ['runtimeStore'];
|
||||||
|
|
||||||
// websocket stuff
|
// external stuff
|
||||||
export const FEAT_CUESHEET = 'feat-cuesheet';
|
export const githubURL = 'https://api.github.com/repos/cpvalente/ontime/releases/latest';
|
||||||
export const FEAT_INFO = 'feat-info';
|
|
||||||
export const FEAT_MESSAGECONTROL = 'feat-messagecontrol';
|
|
||||||
export const FEAT_PLAYBACKCONTROL = 'feat-playbackcontrol';
|
|
||||||
export const FEAT_RUNDOWN = 'feat-rundown';
|
|
||||||
export const TIMER = 'timer';
|
|
||||||
|
|
||||||
// external stuff
|
// external stuff
|
||||||
export const githubURL = 'https://api.github.com/repos/cpvalente/ontime/releases/latest';
|
export const githubURL = 'https://api.github.com/repos/cpvalente/ontime/releases/latest';
|
||||||
@@ -29,6 +25,8 @@ export const githubURL = 'https://api.github.com/repos/cpvalente/ontime/releases
|
|||||||
export const calculateServer = () => (import.meta.env.DEV ? `http://localhost:${STATIC_PORT}` : window.location.origin);
|
export const calculateServer = () => (import.meta.env.DEV ? `http://localhost:${STATIC_PORT}` : window.location.origin);
|
||||||
|
|
||||||
export const serverURL = calculateServer();
|
export const serverURL = calculateServer();
|
||||||
|
export const websocketUrl = `ws://${window.location.hostname}:${STATIC_PORT}/ws`;
|
||||||
|
|
||||||
export const eventURL = `${serverURL}/eventdata`;
|
export const eventURL = `${serverURL}/eventdata`;
|
||||||
export const rundownURL = `${serverURL}/eventlist`;
|
export const rundownURL = `${serverURL}/eventlist`;
|
||||||
export const ontimeURL = `${serverURL}/ontime`;
|
export const ontimeURL = `${serverURL}/ontime`;
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { useCallback, useContext, useEffect, useRef, useState } from 'react';
|
import { useCallback, useEffect, useRef, useState } from 'react';
|
||||||
import {
|
import {
|
||||||
AlertDialog,
|
AlertDialog,
|
||||||
AlertDialogBody,
|
AlertDialogBody,
|
||||||
@@ -12,8 +12,8 @@ import {
|
|||||||
} from '@chakra-ui/react';
|
} from '@chakra-ui/react';
|
||||||
import { FiPower } from '@react-icons/all-files/fi/FiPower';
|
import { FiPower } from '@react-icons/all-files/fi/FiPower';
|
||||||
|
|
||||||
import { LoggingContext } from '../../context/LoggingContext';
|
|
||||||
import { Size } from '../../models/Util.type';
|
import { Size } from '../../models/Util.type';
|
||||||
|
import { useEmitLog } from '../../stores/logger';
|
||||||
|
|
||||||
interface QuitIconBtnProps {
|
interface QuitIconBtnProps {
|
||||||
clickHandler: () => void;
|
clickHandler: () => void;
|
||||||
@@ -39,7 +39,7 @@ const quitBtnStyle = {
|
|||||||
export default function QuitIconBtn(props: QuitIconBtnProps) {
|
export default function QuitIconBtn(props: QuitIconBtnProps) {
|
||||||
const { clickHandler, size = 'lg', ...rest } = props;
|
const { clickHandler, size = 'lg', ...rest } = props;
|
||||||
const [isOpen, setIsOpen] = useState(false);
|
const [isOpen, setIsOpen] = useState(false);
|
||||||
const { emitInfo } = useContext(LoggingContext);
|
const { emitInfo } = useEmitLog();
|
||||||
const onClose = () => setIsOpen(false);
|
const onClose = () => setIsOpen(false);
|
||||||
const cancelRef = useRef<HTMLButtonElement | null>(null);
|
const cancelRef = useRef<HTMLButtonElement | null>(null);
|
||||||
|
|
||||||
|
|||||||
@@ -2,12 +2,9 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import * as Sentry from '@sentry/react';
|
import * as Sentry from '@sentry/react';
|
||||||
|
|
||||||
import { LoggingContext } from '../../context/LoggingContext';
|
|
||||||
|
|
||||||
import style from './ErrorBoundary.module.scss';
|
import style from './ErrorBoundary.module.scss';
|
||||||
|
|
||||||
class ErrorBoundary extends React.Component {
|
class ErrorBoundary extends React.Component {
|
||||||
static contextType = LoggingContext;
|
|
||||||
reportContent = '';
|
reportContent = '';
|
||||||
|
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
import { KeyboardEvent, useCallback, useContext, useEffect, useRef, useState } from 'react';
|
import { KeyboardEvent, useCallback, useEffect, useRef, useState } from 'react';
|
||||||
import { Button, Input, InputGroup, InputLeftElement, Tooltip } from '@chakra-ui/react';
|
import { Button, Input, InputGroup, InputLeftElement, Tooltip } from '@chakra-ui/react';
|
||||||
|
import { millisToString } from 'ontime-utils';
|
||||||
|
|
||||||
import { EventEditorSubmitActions } from '../../../../features/event-editor/EventEditor';
|
import { EventEditorSubmitActions } from '../../../../features/event-editor/EventEditor';
|
||||||
import { tooltipDelayFast } from '../../../../ontimeConfig';
|
import { tooltipDelayFast } from '../../../../ontimeConfig';
|
||||||
import { LoggingContext } from '../../../context/LoggingContext';
|
import { useEmitLog } from '../../../stores/logger';
|
||||||
import { forgivingStringToMillis } from '../../../utils/dateConfig';
|
import { forgivingStringToMillis } from '../../../utils/dateConfig';
|
||||||
import { stringFromMillis } from '../../../utils/time';
|
|
||||||
import { TimeEntryField } from '../../../utils/timesManager';
|
import { TimeEntryField } from '../../../utils/timesManager';
|
||||||
|
|
||||||
import style from './TimeInput.module.scss';
|
import style from './TimeInput.module.scss';
|
||||||
@@ -24,7 +24,7 @@ export default function TimeInput(props: TimeInputProps) {
|
|||||||
const {
|
const {
|
||||||
name, submitHandler, time = 0, delay = 0, placeholder, validationHandler, previousEnd = 0,
|
name, submitHandler, time = 0, delay = 0, placeholder, validationHandler, previousEnd = 0,
|
||||||
} = props;
|
} = props;
|
||||||
const { emitError } = useContext(LoggingContext);
|
const { emitError } = useEmitLog();
|
||||||
const inputRef = useRef<HTMLInputElement | null>(null);
|
const inputRef = useRef<HTMLInputElement | null>(null);
|
||||||
const [value, setValue] = useState('');
|
const [value, setValue] = useState('');
|
||||||
|
|
||||||
@@ -34,7 +34,7 @@ export default function TimeInput(props: TimeInputProps) {
|
|||||||
const resetValue = useCallback(() => {
|
const resetValue = useCallback(() => {
|
||||||
// Todo: check if change is necessary
|
// Todo: check if change is necessary
|
||||||
try {
|
try {
|
||||||
setValue(stringFromMillis(time + delay));
|
setValue(millisToString(time + delay));
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
emitError(`Unable to parse date: ${error}`);
|
emitError(`Unable to parse date: ${error}`);
|
||||||
}
|
}
|
||||||
@@ -97,7 +97,7 @@ export default function TimeInput(props: TimeInputProps) {
|
|||||||
const success = handleSubmit(newValue);
|
const success = handleSubmit(newValue);
|
||||||
if (success) {
|
if (success) {
|
||||||
const ms = forgivingStringToMillis(newValue);
|
const ms = forgivingStringToMillis(newValue);
|
||||||
setValue(stringFromMillis(ms + delay));
|
setValue(millisToString(ms + delay));
|
||||||
} else {
|
} else {
|
||||||
resetValue();
|
resetValue();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { ChangeEvent, useCallback, useContext, useRef, useState } from 'react';
|
import { ChangeEvent, useCallback, useRef, useState } from 'react';
|
||||||
import {
|
import {
|
||||||
Button,
|
Button,
|
||||||
Checkbox,
|
Checkbox,
|
||||||
@@ -21,7 +21,7 @@ import { useQueryClient } from '@tanstack/react-query';
|
|||||||
|
|
||||||
import { RUNDOWN_TABLE } from '../../api/apiConstants';
|
import { RUNDOWN_TABLE } from '../../api/apiConstants';
|
||||||
import { uploadData } from '../../api/ontimeApi';
|
import { uploadData } from '../../api/ontimeApi';
|
||||||
import { LoggingContext } from '../../context/LoggingContext';
|
import { useEmitLog } from '../../stores/logger';
|
||||||
import TooltipActionBtn from '../buttons/TooltipActionBtn';
|
import TooltipActionBtn from '../buttons/TooltipActionBtn';
|
||||||
|
|
||||||
import { validateFile } from './utils';
|
import { validateFile } from './utils';
|
||||||
@@ -35,7 +35,7 @@ interface UploadModalProps {
|
|||||||
|
|
||||||
export default function UploadModal({ onClose, isOpen }: UploadModalProps) {
|
export default function UploadModal({ onClose, isOpen }: UploadModalProps) {
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
const { emitError } = useContext(LoggingContext);
|
const { emitError } = useEmitLog();
|
||||||
const [errors, setErrors] = useState<string[]>([]);
|
const [errors, setErrors] = useState<string[]>([]);
|
||||||
const [file, setFile] = useState<File | null>(null);
|
const [file, setFile] = useState<File | null>(null);
|
||||||
const [progress, setProgress] = useState(0);
|
const [progress, setProgress] = useState(0);
|
||||||
|
|||||||
@@ -1,137 +0,0 @@
|
|||||||
import { createContext, ReactNode, useCallback, useEffect, useState } from 'react';
|
|
||||||
import { generateId } from 'ontime-utils';
|
|
||||||
|
|
||||||
import socket from '../utils/socket';
|
|
||||||
import { nowInMillis, stringFromMillis } from '../utils/time';
|
|
||||||
|
|
||||||
export enum LOG_LEVEL {
|
|
||||||
INFO = 'INFO',
|
|
||||||
WARN = 'WARN',
|
|
||||||
ERROR = 'ERROR',
|
|
||||||
}
|
|
||||||
|
|
||||||
export type Log = {
|
|
||||||
id: string;
|
|
||||||
origin: string;
|
|
||||||
time: string;
|
|
||||||
level: LOG_LEVEL;
|
|
||||||
text: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
interface LoggingProviderState {
|
|
||||||
logData: Log[];
|
|
||||||
emitInfo: (text: string) => void;
|
|
||||||
emitWarning: (text: string) => void;
|
|
||||||
emitError: (text: string) => void;
|
|
||||||
clearLog: () => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
type LoggingProviderProps = {
|
|
||||||
children: ReactNode;
|
|
||||||
};
|
|
||||||
|
|
||||||
const notInitialised = () => {
|
|
||||||
throw new Error('Not initialised');
|
|
||||||
};
|
|
||||||
|
|
||||||
export const LoggingContext = createContext<LoggingProviderState>({
|
|
||||||
logData: [],
|
|
||||||
emitInfo: notInitialised,
|
|
||||||
emitWarning: notInitialised,
|
|
||||||
emitError: notInitialised,
|
|
||||||
clearLog: notInitialised,
|
|
||||||
});
|
|
||||||
|
|
||||||
export const LoggingProvider = ({ children }: LoggingProviderProps) => {
|
|
||||||
const MAX_MESSAGES = 100;
|
|
||||||
const [logData, setLogData] = useState<Log[]>([]);
|
|
||||||
const origin = 'USER';
|
|
||||||
|
|
||||||
// todo: use react-query store
|
|
||||||
// todo: useSubscription or feature
|
|
||||||
// handle incoming messages
|
|
||||||
useEffect(() => {
|
|
||||||
socket.emit('get-logger');
|
|
||||||
|
|
||||||
socket.on('logger', (data: Log) => {
|
|
||||||
setLogData((currentLog) => [data, ...currentLog]);
|
|
||||||
});
|
|
||||||
|
|
||||||
// Clear listener
|
|
||||||
return () => {
|
|
||||||
socket.off('logger');
|
|
||||||
};
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Utility function sends message over socket
|
|
||||||
* @param text
|
|
||||||
* @param level
|
|
||||||
* @private
|
|
||||||
*/
|
|
||||||
const _send = useCallback(
|
|
||||||
(text: string, level: LOG_LEVEL) => {
|
|
||||||
if (socket != null) {
|
|
||||||
const newLogMessage: Log = {
|
|
||||||
id: generateId(),
|
|
||||||
origin,
|
|
||||||
time: stringFromMillis(nowInMillis()),
|
|
||||||
level,
|
|
||||||
text,
|
|
||||||
};
|
|
||||||
setLogData((currentLog) => [newLogMessage, ...currentLog]);
|
|
||||||
socket.emit('logger', newLogMessage);
|
|
||||||
}
|
|
||||||
if (logData.length > MAX_MESSAGES) {
|
|
||||||
setLogData((currentLog) => currentLog.slice(1));
|
|
||||||
}
|
|
||||||
},
|
|
||||||
[logData.length, setLogData],
|
|
||||||
);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Sends a message with level INFO
|
|
||||||
* @param text
|
|
||||||
*/
|
|
||||||
const emitInfo = useCallback(
|
|
||||||
(text: string) => {
|
|
||||||
_send(text, LOG_LEVEL.INFO);
|
|
||||||
},
|
|
||||||
[_send],
|
|
||||||
);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Sends a message with level WARN
|
|
||||||
* @param text
|
|
||||||
*/
|
|
||||||
const emitWarning = useCallback(
|
|
||||||
(text: string) => {
|
|
||||||
_send(text, LOG_LEVEL.WARN);
|
|
||||||
},
|
|
||||||
[_send],
|
|
||||||
);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Sends a message with level ERROR
|
|
||||||
* @param text
|
|
||||||
*/
|
|
||||||
const emitError = useCallback(
|
|
||||||
(text: string) => {
|
|
||||||
_send(text, LOG_LEVEL.ERROR);
|
|
||||||
},
|
|
||||||
[_send],
|
|
||||||
);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Clears running log
|
|
||||||
*/
|
|
||||||
const clearLog = useCallback(() => {
|
|
||||||
setLogData([]);
|
|
||||||
}, [setLogData]);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<LoggingContext.Provider value={{ emitInfo, logData, emitWarning, emitError, clearLog }}>
|
|
||||||
{children}
|
|
||||||
</LoggingContext.Provider>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
import { useCallback, useContext } from 'react';
|
import { useCallback } from 'react';
|
||||||
import { useMutation, useQueryClient } from '@tanstack/react-query';
|
import { useMutation, useQueryClient } from '@tanstack/react-query';
|
||||||
import axios, { AxiosError } from 'axios';
|
import axios, { AxiosError } from 'axios';
|
||||||
import { useAtomValue } from 'jotai';
|
import { useAtomValue } from 'jotai';
|
||||||
@@ -15,14 +15,14 @@ import {
|
|||||||
requestReorderEvent,
|
requestReorderEvent,
|
||||||
} from '../api/eventsApi';
|
} from '../api/eventsApi';
|
||||||
import { defaultPublicAtom, startTimeIsLastEndAtom } from '../atoms/LocalEventSettings';
|
import { defaultPublicAtom, startTimeIsLastEndAtom } from '../atoms/LocalEventSettings';
|
||||||
import { LoggingContext } from '../context/LoggingContext';
|
import { useEmitLog } from '../stores/logger';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @description Set of utilities for events
|
* @description Set of utilities for events
|
||||||
*/
|
*/
|
||||||
export const useEventAction = () => {
|
export const useEventAction = () => {
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
const { emitError } = useContext(LoggingContext);
|
const { emitError } = useEmitLog();
|
||||||
const defaultPublic = useAtomValue(defaultPublicAtom);
|
const defaultPublic = useAtomValue(defaultPublicAtom);
|
||||||
const startTimeIsLastEnd = useAtomValue(startTimeIsLastEndAtom);
|
const startTimeIsLastEnd = useAtomValue(startTimeIsLastEndAtom);
|
||||||
|
|
||||||
@@ -39,23 +39,24 @@ export const useEventAction = () => {
|
|||||||
networkMode: 'always',
|
networkMode: 'always',
|
||||||
});
|
});
|
||||||
|
|
||||||
type AddOptions = {
|
type BaseOptions = {
|
||||||
defaultPublic?: boolean;
|
|
||||||
startTimeIsLastEnd?: boolean;
|
|
||||||
lastEventId?: string;
|
|
||||||
after?: string;
|
after?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
type EventOptions = BaseOptions & {
|
||||||
|
defaultPublic?: boolean;
|
||||||
|
lastEventId?: string;
|
||||||
|
startTimeIsLastEnd?: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Adds an event to rundown
|
* Adds an event to rundown
|
||||||
*/
|
*/
|
||||||
const addEvent = useCallback(
|
const addEvent = useCallback(
|
||||||
async (event: Partial<OntimeRundownEntry>, options?: AddOptions) => {
|
async (event: Partial<OntimeRundownEntry>, options?: EventOptions) => {
|
||||||
const newEvent: Partial<OntimeRundownEntry> = { ...event };
|
const newEvent: Partial<OntimeRundownEntry> = { ...event };
|
||||||
|
|
||||||
// ************* CHECK OPTIONS
|
// ************* CHECK OPTIONS specific to events
|
||||||
// there is an option to pass an index of an array to use as start time
|
|
||||||
// only events have options
|
|
||||||
if (newEvent.type === SupportedEvent.Event) {
|
if (newEvent.type === SupportedEvent.Event) {
|
||||||
const applicationOptions = {
|
const applicationOptions = {
|
||||||
defaultPublic: options?.defaultPublic ?? defaultPublic,
|
defaultPublic: options?.defaultPublic ?? defaultPublic,
|
||||||
@@ -81,13 +82,15 @@ export const useEventAction = () => {
|
|||||||
if (applicationOptions.defaultPublic) {
|
if (applicationOptions.defaultPublic) {
|
||||||
newEvent.isPublic = true;
|
newEvent.isPublic = true;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (applicationOptions?.after) {
|
// handle adding options that concern all event type
|
||||||
newEvent.after = applicationOptions.after;
|
if (options?.after) {
|
||||||
}
|
newEvent.after = options.after;
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
// @ts-expect-error -- we know that the object is well formed now
|
||||||
await _addEventMutation.mutateAsync(newEvent);
|
await _addEventMutation.mutateAsync(newEvent);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (!axios.isAxiosError(error)) {
|
if (!axios.isAxiosError(error)) {
|
||||||
|
|||||||
@@ -1,144 +1,99 @@
|
|||||||
import { useQuery } from '@tanstack/react-query';
|
import { RuntimeStore } from 'ontime-types';
|
||||||
import { Playback } from 'ontime-types';
|
|
||||||
|
|
||||||
import {
|
import { deepCompare, useRuntimeStore } from '../stores/runtime';
|
||||||
FEAT_CUESHEET,
|
import { socketSendJson } from '../utils/socket';
|
||||||
FEAT_INFO,
|
|
||||||
FEAT_MESSAGECONTROL,
|
|
||||||
FEAT_PLAYBACKCONTROL,
|
|
||||||
FEAT_RUNDOWN,
|
|
||||||
TIMER,
|
|
||||||
} from '../api/apiConstants';
|
|
||||||
import { ontimeQueryClient as queryClient } from '../queryClient';
|
|
||||||
import socket, { subscribeOnce } from '../utils/socket';
|
|
||||||
|
|
||||||
function createSocketHook<T>(key: string, defaultValue: T | null = null) {
|
export const useRundownEditor = () => {
|
||||||
subscribeOnce<T>(key, (data) => queryClient.setQueryData([key], data));
|
const featureSelector = (state: RuntimeStore) => ({
|
||||||
|
playback: state.playback,
|
||||||
// retrieves data from the cache or null if non-existent
|
selectedEventId: state.loaded.selectedEventId,
|
||||||
// we need the null because useQuery can't receive undefined
|
nextEventId: state.loaded.nextEventId,
|
||||||
const fetcher = () => (queryClient.getQueryData([key]) ?? defaultValue) as T | null;
|
|
||||||
|
|
||||||
return () => useQuery({ queryKey: [key], queryFn: fetcher, placeholderData: defaultValue });
|
|
||||||
}
|
|
||||||
|
|
||||||
interface IRundown {
|
|
||||||
selectedEventId: string | null;
|
|
||||||
nextEventId: string | null;
|
|
||||||
playback: Playback | null;
|
|
||||||
}
|
|
||||||
|
|
||||||
const emptyRundown: IRundown = {
|
|
||||||
selectedEventId: null,
|
|
||||||
nextEventId: null,
|
|
||||||
playback: null,
|
|
||||||
};
|
|
||||||
|
|
||||||
export const useRundownEditor = createSocketHook(FEAT_RUNDOWN, emptyRundown);
|
|
||||||
|
|
||||||
const emptyMessageControl = {
|
|
||||||
messages: {
|
|
||||||
presenter: {
|
|
||||||
text: '',
|
|
||||||
visible: false,
|
|
||||||
},
|
|
||||||
public: {
|
|
||||||
text: '',
|
|
||||||
visible: false,
|
|
||||||
},
|
|
||||||
lower: {
|
|
||||||
text: '',
|
|
||||||
visible: false,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
onAir: false,
|
|
||||||
};
|
|
||||||
|
|
||||||
export const useMessageControl = createSocketHook(FEAT_MESSAGECONTROL, emptyMessageControl);
|
|
||||||
export const setMessage = {
|
|
||||||
presenterText: (payload: string) => socket.emit('set-timer-message-text', payload),
|
|
||||||
presenterVisible: (payload: boolean) => socket.emit('set-timer-message-visible', payload),
|
|
||||||
publicText: (payload: string) => socket.emit('set-public-message-text', payload),
|
|
||||||
publicVisible: (payload: boolean) => socket.emit('set-public-message-visible', payload),
|
|
||||||
lowerText: (payload: string) => socket.emit('set-lower-message-text', payload),
|
|
||||||
lowerVisible: (payload: boolean) => socket.emit('set-lower-message-visible', payload),
|
|
||||||
onAir: (payload: boolean) => socket.emit('set-onAir', payload),
|
|
||||||
};
|
|
||||||
|
|
||||||
export const emptyPlaybackControl = {
|
|
||||||
playback: 'stop',
|
|
||||||
selectedEventId: null,
|
|
||||||
numEvents: 0,
|
|
||||||
};
|
|
||||||
export const usePlaybackControl = createSocketHook(FEAT_PLAYBACKCONTROL, emptyPlaybackControl);
|
|
||||||
export const resetPlayback = () => {
|
|
||||||
const cacheData = queryClient.getQueryData([FEAT_PLAYBACKCONTROL]) as Record<string, unknown>;
|
|
||||||
queryClient.setQueryData([FEAT_PLAYBACKCONTROL], {
|
|
||||||
...cacheData,
|
|
||||||
playback: 'stop',
|
|
||||||
selectedEventId: null,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
return useRuntimeStore(featureSelector, deepCompare);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const useMessageControl = () => {
|
||||||
|
const featureSelector = (state: RuntimeStore) => ({
|
||||||
|
timerMessage: state.timerMessage,
|
||||||
|
publicMessage: state.publicMessage,
|
||||||
|
lowerMessage: state.lowerMessage,
|
||||||
|
onAir: state.onAir,
|
||||||
|
});
|
||||||
|
|
||||||
|
return useRuntimeStore(featureSelector, deepCompare);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const setMessage = {
|
||||||
|
presenterText: (payload: string) => socketSendJson('set-timer-message-text', payload),
|
||||||
|
presenterVisible: (payload: boolean) => socketSendJson('set-timer-message-visible', payload),
|
||||||
|
publicText: (payload: string) => socketSendJson('set-public-message-text', payload),
|
||||||
|
publicVisible: (payload: boolean) => socketSendJson('set-public-message-visible', payload),
|
||||||
|
lowerText: (payload: string) => socketSendJson('set-lower-message-text', payload),
|
||||||
|
lowerVisible: (payload: boolean) => socketSendJson('set-lower-message-visible', payload),
|
||||||
|
onAir: (payload: boolean) => socketSendJson('set-onAir', payload),
|
||||||
|
};
|
||||||
|
|
||||||
|
export const usePlaybackControl = () => {
|
||||||
|
const featureSelector = (state: RuntimeStore) => ({
|
||||||
|
playback: state.playback,
|
||||||
|
numEvents: state.loaded.numEvents,
|
||||||
|
});
|
||||||
|
|
||||||
|
return useRuntimeStore(featureSelector, deepCompare);
|
||||||
|
};
|
||||||
|
|
||||||
export const setPlayback = {
|
export const setPlayback = {
|
||||||
start: () => socket.emit('set-start'),
|
start: () => socketSendJson('start'),
|
||||||
pause: () => socket.emit('set-pause'),
|
pause: () => socketSendJson('pause'),
|
||||||
roll: () => socket.emit('set-roll'),
|
roll: () => socketSendJson('roll'),
|
||||||
previous: () => {
|
previous: () => {
|
||||||
socket.emit('set-previous');
|
socketSendJson('previous');
|
||||||
},
|
},
|
||||||
next: () => {
|
next: () => {
|
||||||
socket.emit('set-next');
|
socketSendJson('next');
|
||||||
},
|
},
|
||||||
stop: () => {
|
stop: () => {
|
||||||
socket.emit('set-stop');
|
socketSendJson('stop');
|
||||||
},
|
},
|
||||||
reload: () => {
|
reload: () => {
|
||||||
socket.emit('set-reload');
|
socketSendJson('reload');
|
||||||
},
|
},
|
||||||
delay: (amount: number) => {
|
delay: (amount: number) => {
|
||||||
socket.emit('set-delay', amount);
|
socketSendJson('delay', amount);
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
export const emptyInfo = {
|
export const useInfoPanel = () => {
|
||||||
titles: {
|
const featureSelector = (state: RuntimeStore) => ({
|
||||||
titleNow: '',
|
titles: state.titles,
|
||||||
subtitleNow: '',
|
playback: state.playback,
|
||||||
presenterNow: '',
|
selectedEventIndex: state.loaded.selectedEventIndex,
|
||||||
noteNow: '',
|
numEvents: state.loaded.numEvents,
|
||||||
titleNext: '',
|
});
|
||||||
subtitleNext: '',
|
|
||||||
presenterNext: '',
|
return useRuntimeStore(featureSelector, deepCompare);
|
||||||
noteNext: '',
|
|
||||||
},
|
|
||||||
playback: 'stop',
|
|
||||||
selectedEventId: null,
|
|
||||||
selectedEventIndex: null,
|
|
||||||
numEvents: 0,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const useInfoPanel = createSocketHook(FEAT_INFO, emptyInfo);
|
export const useCuesheet = () => {
|
||||||
|
const featureSelector = (state: RuntimeStore) => ({
|
||||||
|
selectedEventIndex: state.loaded.selectedEventId,
|
||||||
|
titleNow: state.titles.titleNow,
|
||||||
|
});
|
||||||
|
|
||||||
export const emptyCuesheet = {
|
return useRuntimeStore(featureSelector, deepCompare);
|
||||||
selectedEventId: null,
|
|
||||||
titleNow: '',
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const useCuesheet = createSocketHook(FEAT_CUESHEET, emptyCuesheet);
|
|
||||||
|
|
||||||
export const setEventPlayback = {
|
export const setEventPlayback = {
|
||||||
loadEvent: (eventId: string) => socket.emit('set-loadid', eventId),
|
loadEvent: (eventId: string) => socketSendJson('loadid', eventId),
|
||||||
startEvent: (eventId: string) => socket.emit('set-startid', eventId),
|
startEvent: (eventId: string) => socketSendJson('startid', eventId),
|
||||||
pause: () => socket.emit('set-pause'),
|
pause: () => socketSendJson('pause'),
|
||||||
};
|
};
|
||||||
|
|
||||||
const emptyTimer = {
|
export const useTimer = () => {
|
||||||
clock: 0,
|
const featureSelector = (state: RuntimeStore) => ({
|
||||||
current: 0,
|
timer: state.timer,
|
||||||
secondaryTimer: null,
|
});
|
||||||
duration: null,
|
|
||||||
startedAt: null,
|
|
||||||
expectedFinish: null,
|
|
||||||
};
|
|
||||||
|
|
||||||
export const useTimer = createSocketHook(TIMER, emptyTimer);
|
return useRuntimeStore(featureSelector, deepCompare);
|
||||||
|
};
|
||||||
|
|||||||
@@ -1,22 +0,0 @@
|
|||||||
import { useEffect, useState } from 'react';
|
|
||||||
|
|
||||||
import socket from '../utils/socket';
|
|
||||||
|
|
||||||
export default function useSubscription<T>(topic: string, initialState: T, requestString?: string) {
|
|
||||||
const [state, setState] = useState<T>(initialState);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (requestString) {
|
|
||||||
socket.emit(requestString);
|
|
||||||
} else {
|
|
||||||
socket.emit(`get-${topic}`);
|
|
||||||
}
|
|
||||||
socket.on(topic, setState);
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
socket.off(topic);
|
|
||||||
};
|
|
||||||
}, [requestString, topic]);
|
|
||||||
|
|
||||||
return [state, setState] as const;
|
|
||||||
};
|
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
export default function createStore<T>(initialState: T) {
|
||||||
|
let currentState = initialState;
|
||||||
|
const listeners = new Set<(state: T) => void>();
|
||||||
|
|
||||||
|
return {
|
||||||
|
get: () => currentState,
|
||||||
|
set: (newState: T) => {
|
||||||
|
currentState = newState;
|
||||||
|
listeners.forEach((listener) => listener(currentState));
|
||||||
|
},
|
||||||
|
subscribe: (listener: (state: T) => void) => {
|
||||||
|
listeners.add(listener);
|
||||||
|
return () => listeners.delete(listener);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -0,0 +1,85 @@
|
|||||||
|
import { useCallback } from 'react';
|
||||||
|
import { Log, LogLevel } from 'ontime-types';
|
||||||
|
import { generateId, millisToString } from 'ontime-utils';
|
||||||
|
import { useStore } from 'zustand';
|
||||||
|
import { createStore } from 'zustand/vanilla';
|
||||||
|
|
||||||
|
import { socketSendJson } from '../utils/socket';
|
||||||
|
import { nowInMillis } from '../utils/time';
|
||||||
|
|
||||||
|
type LogStore = {
|
||||||
|
logs: Log[];
|
||||||
|
};
|
||||||
|
|
||||||
|
export const logger = createStore<LogStore>(() => ({
|
||||||
|
logs: [],
|
||||||
|
}));
|
||||||
|
|
||||||
|
export const useLogData = () => useStore(logger);
|
||||||
|
|
||||||
|
export const addLog = (log: Log) =>
|
||||||
|
logger.setState((state) => ({
|
||||||
|
logs: [...state.logs, log],
|
||||||
|
}));
|
||||||
|
|
||||||
|
export const clearLogs = () => logger.setState({ logs: [] });
|
||||||
|
|
||||||
|
export function useEmitLog() {
|
||||||
|
/**
|
||||||
|
* Utility function sends message over socket
|
||||||
|
* @param text
|
||||||
|
* @param level
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
const _emit = useCallback((text: string, level: LogLevel) => {
|
||||||
|
const log = {
|
||||||
|
id: generateId(),
|
||||||
|
origin: 'CLIENT',
|
||||||
|
time: millisToString(nowInMillis()),
|
||||||
|
level,
|
||||||
|
text,
|
||||||
|
};
|
||||||
|
|
||||||
|
addLog(log);
|
||||||
|
socketSendJson('ontime-log', log);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sends a message with level INFO
|
||||||
|
* @param text
|
||||||
|
*/
|
||||||
|
const emitInfo = useCallback(
|
||||||
|
(text: string) => {
|
||||||
|
_emit(text, LogLevel.Info);
|
||||||
|
},
|
||||||
|
[_emit],
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sends a message with level WARN
|
||||||
|
* @param text
|
||||||
|
*/
|
||||||
|
const emitWarning = useCallback(
|
||||||
|
(text: string) => {
|
||||||
|
_emit(text, LogLevel.Warn);
|
||||||
|
},
|
||||||
|
[_emit],
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sends a message with level ERROR
|
||||||
|
* @param text
|
||||||
|
*/
|
||||||
|
const emitError = useCallback(
|
||||||
|
(text: string) => {
|
||||||
|
_emit(text, LogLevel.Error);
|
||||||
|
},
|
||||||
|
[_emit],
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
emitInfo,
|
||||||
|
emitWarning,
|
||||||
|
emitError,
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -0,0 +1,73 @@
|
|||||||
|
import isEqual from 'react-fast-compare';
|
||||||
|
import { Playback, RuntimeStore } from 'ontime-types';
|
||||||
|
import { useStore } from 'zustand';
|
||||||
|
import { createStore } from 'zustand/vanilla';
|
||||||
|
|
||||||
|
export const runtimeStorePlaceholder = {
|
||||||
|
timer: {
|
||||||
|
clock: 0,
|
||||||
|
current: null,
|
||||||
|
elapsed: null,
|
||||||
|
expectedFinish: null,
|
||||||
|
addedTime: 0,
|
||||||
|
startedAt: null,
|
||||||
|
finishedAt: null,
|
||||||
|
secondaryTimer: null,
|
||||||
|
selectedEventId: null,
|
||||||
|
duration: null,
|
||||||
|
timerType: null,
|
||||||
|
},
|
||||||
|
playback: Playback.Stop,
|
||||||
|
timerMessage: {
|
||||||
|
text: '',
|
||||||
|
visible: false,
|
||||||
|
},
|
||||||
|
publicMessage: {
|
||||||
|
text: '',
|
||||||
|
visible: false,
|
||||||
|
},
|
||||||
|
lowerMessage: {
|
||||||
|
text: '',
|
||||||
|
visible: false,
|
||||||
|
},
|
||||||
|
onAir: false,
|
||||||
|
loaded: {
|
||||||
|
numEvents: 0,
|
||||||
|
selectedEventIndex: null,
|
||||||
|
selectedEventId: null,
|
||||||
|
selectedPublicEventId: null,
|
||||||
|
nextEventId: null,
|
||||||
|
nextPublicEventId: null,
|
||||||
|
},
|
||||||
|
titles: {
|
||||||
|
titleNow: null,
|
||||||
|
subtitleNow: null,
|
||||||
|
presenterNow: null,
|
||||||
|
noteNow: null,
|
||||||
|
titleNext: null,
|
||||||
|
subtitleNext: null,
|
||||||
|
presenterNext: null,
|
||||||
|
noteNext: null,
|
||||||
|
},
|
||||||
|
titlesPublic: {
|
||||||
|
titleNow: null,
|
||||||
|
subtitleNow: null,
|
||||||
|
presenterNow: null,
|
||||||
|
noteNow: null,
|
||||||
|
titleNext: null,
|
||||||
|
subtitleNext: null,
|
||||||
|
presenterNext: null,
|
||||||
|
noteNext: null,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export const runtime = createStore<RuntimeStore>(() => ({
|
||||||
|
...runtimeStorePlaceholder,
|
||||||
|
}));
|
||||||
|
|
||||||
|
export const deepCompare = <T>(a: T, b: T) => isEqual(a, b);
|
||||||
|
|
||||||
|
export const useRuntimeStore = <T>(
|
||||||
|
selector: (state: RuntimeStore) => T,
|
||||||
|
equalityFn?: (a: unknown, b: unknown) => boolean,
|
||||||
|
) => useStore(runtime, selector, equalityFn);
|
||||||
@@ -6,7 +6,6 @@ import {
|
|||||||
millisToSeconds,
|
millisToSeconds,
|
||||||
timeStringToMillis,
|
timeStringToMillis,
|
||||||
} from '../dateConfig';
|
} from '../dateConfig';
|
||||||
import { stringFromMillis } from '../time';
|
|
||||||
|
|
||||||
describe('test string from formatDisplay function', () => {
|
describe('test string from formatDisplay function', () => {
|
||||||
it('test with null values', () => {
|
it('test with null values', () => {
|
||||||
@@ -58,7 +57,7 @@ describe('test string from formatDisplay function', () => {
|
|||||||
describe('test formatDisplay handles partial secs', () => {
|
describe('test formatDisplay handles partial secs', () => {
|
||||||
it('test with 1795829', () => {
|
it('test with 1795829', () => {
|
||||||
const t = { val: 1795829, result: '00:29:55' };
|
const t = { val: 1795829, result: '00:29:55' };
|
||||||
expect(stringFromMillis(t.val)).toBe(t.result);
|
expect(formatDisplay(t.val)).toBe(t.result);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -1,17 +1,138 @@
|
|||||||
import { serverURL } from '../api/apiConstants';
|
import { Log } from 'ontime-types';
|
||||||
import { io } from 'socket.io-client';
|
|
||||||
|
|
||||||
const socket = io(serverURL, { transports: ['websocket'] });
|
import { RUNTIME, websocketUrl } from '../api/apiConstants';
|
||||||
const subscriptions = new Set();
|
import { ontimeQueryClient } from '../queryClient';
|
||||||
|
import { addLog } from '../stores/logger';
|
||||||
|
import { runtime } from '../stores/runtime';
|
||||||
|
|
||||||
export function subscribeOnce<T>(key: string, callback: (data: T) => void, requestString?: string) {
|
export let websocket: WebSocket | null = null;
|
||||||
if (subscriptions.has(key)) {
|
let reconnectTimeout: NodeJS.Timeout | null = null;
|
||||||
return;
|
const reconnectInterval = 1000;
|
||||||
|
let shouldReconnect = true;
|
||||||
|
|
||||||
|
export const connectSocket = () => {
|
||||||
|
websocket = new WebSocket(websocketUrl);
|
||||||
|
|
||||||
|
websocket.onopen = () => {
|
||||||
|
clearTimeout(reconnectTimeout as NodeJS.Timeout);
|
||||||
|
};
|
||||||
|
|
||||||
|
websocket.onclose = () => {
|
||||||
|
console.warn('WebSocket disconnected');
|
||||||
|
if (shouldReconnect) {
|
||||||
|
reconnectTimeout = setTimeout(() => {
|
||||||
|
console.warn('WebSocket: attempting reconnect');
|
||||||
|
if (websocket && websocket.readyState === WebSocket.CLOSED) {
|
||||||
|
connectSocket();
|
||||||
|
}
|
||||||
|
}, reconnectInterval);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
websocket.onerror = (error) => {
|
||||||
|
console.error('WebSocket error:', error);
|
||||||
|
};
|
||||||
|
|
||||||
|
websocket.onmessage = (event) => {
|
||||||
|
try {
|
||||||
|
const data = JSON.parse(event.data);
|
||||||
|
|
||||||
|
const { type, payload } = data;
|
||||||
|
|
||||||
|
if (!type) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: implement partial store updates
|
||||||
|
switch (type) {
|
||||||
|
case 'ontime-log': {
|
||||||
|
addLog(payload as Log);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 'ontime': {
|
||||||
|
runtime.setState(payload);
|
||||||
|
if (import.meta.env.DEV) {
|
||||||
|
ontimeQueryClient.setQueryData(RUNTIME, data.payload);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 'ontime-playback': {
|
||||||
|
const state = runtime.getState();
|
||||||
|
state.playback = payload;
|
||||||
|
runtime.setState(state);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 'ontime-timer': {
|
||||||
|
const state = runtime.getState();
|
||||||
|
state.timer = payload;
|
||||||
|
runtime.setState(state);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 'ontime-loaded': {
|
||||||
|
const state = runtime.getState();
|
||||||
|
state.loaded = payload;
|
||||||
|
runtime.setState(state);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 'ontime-titles': {
|
||||||
|
const state = runtime.getState();
|
||||||
|
state.titles = payload;
|
||||||
|
runtime.setState(state);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 'ontime-titlesPublic': {
|
||||||
|
const state = runtime.getState();
|
||||||
|
state.titlesPublic = payload;
|
||||||
|
runtime.setState(state);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 'ontime-timerMessage': {
|
||||||
|
const state = runtime.getState();
|
||||||
|
state.timerMessage = payload;
|
||||||
|
runtime.setState(state);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 'ontime-publicMessage': {
|
||||||
|
const state = runtime.getState();
|
||||||
|
state.publicMessage = payload;
|
||||||
|
runtime.setState(state);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 'ontime-lowerMessage': {
|
||||||
|
const state = runtime.getState();
|
||||||
|
state.lowerMessage = payload;
|
||||||
|
runtime.setState(state);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 'ontime-onAir': {
|
||||||
|
const state = runtime.getState();
|
||||||
|
state.onAir = payload;
|
||||||
|
runtime.setState(state);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (_) {
|
||||||
|
// ignore unhandled
|
||||||
|
}
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export const disconnectSocket = () => {
|
||||||
|
shouldReconnect = false;
|
||||||
|
websocket?.close();
|
||||||
|
};
|
||||||
|
|
||||||
|
export const socketSend = (message: any) => {
|
||||||
|
if (websocket && websocket.readyState === WebSocket.OPEN) {
|
||||||
|
websocket.send(message);
|
||||||
}
|
}
|
||||||
subscriptions.add(key);
|
};
|
||||||
|
|
||||||
requestString ? socket.emit(requestString) : socket.emit(`get-${key}`);
|
export const socketSendJson = (type: string, payload?: any) => {
|
||||||
socket.on(key, callback);
|
socketSend(
|
||||||
}
|
JSON.stringify({
|
||||||
|
type,
|
||||||
export default socket;
|
payload,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|||||||
@@ -1,86 +0,0 @@
|
|||||||
import { DateTime } from 'luxon';
|
|
||||||
|
|
||||||
import { APP_SETTINGS } from '../api/apiConstants';
|
|
||||||
import { ontimeQueryClient } from '../queryClient';
|
|
||||||
|
|
||||||
import { mth, mtm, mts } from './timeConstants';
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 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 | null} ms - time in milliseconds
|
|
||||||
* @param {boolean} showSeconds - weather to show the seconds
|
|
||||||
* @param {string} delim - character between HH MM SS
|
|
||||||
* @param {string} ifNull - what to return if value is null
|
|
||||||
* @returns {string} String representing time 00:12:02
|
|
||||||
*/
|
|
||||||
export const stringFromMillis = (ms, showSeconds = true, delim = ':', ifNull = '...') => {
|
|
||||||
if (ms == null || isNaN(ms)) return ifNull;
|
|
||||||
const isNegative = ms < 0 ? '-' : '';
|
|
||||||
const millis = Math.abs(ms);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @description ensures value is double digit
|
|
||||||
* @param value
|
|
||||||
* @return {string|*}
|
|
||||||
*/
|
|
||||||
const showWith0 = (value) => (value < 10 ? `0${value}` : value);
|
|
||||||
const hours = showWith0(Math.floor(((millis / mth) % 60) % 24));
|
|
||||||
const minutes = showWith0(Math.floor((millis / mtm) % 60));
|
|
||||||
const seconds = showWith0(Math.floor((millis / mts) % 60));
|
|
||||||
|
|
||||||
return showSeconds
|
|
||||||
? `${isNegative}${
|
|
||||||
parseInt(hours, 10) ? `${hours}${delim}` : `00${delim}`
|
|
||||||
}${minutes}${delim}${seconds}`
|
|
||||||
: `${isNegative}${parseInt(hours, 10) ? `${hours}` : '00'}${delim}${minutes}`;
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @description Resolves format from url and store
|
|
||||||
* @return {string|undefined}
|
|
||||||
*/
|
|
||||||
export const resolveTimeFormat = () => {
|
|
||||||
const params = new URL(document.location).searchParams;
|
|
||||||
const urlOptions = params.get('format');
|
|
||||||
const settings = ontimeQueryClient.getQueryData(APP_SETTINGS);
|
|
||||||
|
|
||||||
return urlOptions || settings?.timeFormat;
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
/**
|
|
||||||
* @description utility function to format a date in 12 or 24 hour format
|
|
||||||
* @param {number} milliseconds
|
|
||||||
* @param {object} [options]
|
|
||||||
* @param {boolean} [options.showSeconds]
|
|
||||||
* @param {string} [options.format]
|
|
||||||
* @param {function} resolver
|
|
||||||
* @return {string}
|
|
||||||
*/
|
|
||||||
export const formatTime = (milliseconds, options, resolver = resolveTimeFormat) => {
|
|
||||||
if (milliseconds === null) {
|
|
||||||
return '...';
|
|
||||||
}
|
|
||||||
const timeFormat = resolver();
|
|
||||||
const { showSeconds = false, format: formatString = 'hh:mm a' } = options || {};
|
|
||||||
return timeFormat === '12'
|
|
||||||
? DateTime.fromMillis(milliseconds).toUTC().toFormat(formatString)
|
|
||||||
: stringFromMillis(milliseconds, showSeconds);
|
|
||||||
};
|
|
||||||
@@ -0,0 +1,60 @@
|
|||||||
|
import { DateTime } from 'luxon';
|
||||||
|
import { Settings } from 'ontime-types';
|
||||||
|
import { millisToString } from 'ontime-utils';
|
||||||
|
|
||||||
|
import { APP_SETTINGS } from '../api/apiConstants';
|
||||||
|
import { ontimeQueryClient } from '../queryClient';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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 Resolves format from url and store
|
||||||
|
* @return {string|undefined}
|
||||||
|
*/
|
||||||
|
export const resolveTimeFormat = () => {
|
||||||
|
const params = new URL(document.location.href).searchParams;
|
||||||
|
const urlOptions = params.get('format');
|
||||||
|
const settings: Settings | undefined = ontimeQueryClient.getQueryData(APP_SETTINGS);
|
||||||
|
|
||||||
|
return urlOptions || settings?.timeFormat;
|
||||||
|
};
|
||||||
|
|
||||||
|
type FormatOptions = {
|
||||||
|
showSeconds?: boolean;
|
||||||
|
format?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
/**
|
||||||
|
* @description utility function to format a date in 12 or 24 hour format
|
||||||
|
* @param {number | null} milliseconds
|
||||||
|
* @param {object} [options]
|
||||||
|
* @param {boolean} [options.showSeconds]
|
||||||
|
* @param {string} [options.format]
|
||||||
|
* @param {function} resolver
|
||||||
|
* @return {string}
|
||||||
|
*/
|
||||||
|
export const formatTime = (milliseconds: number | null, options: FormatOptions, resolver = resolveTimeFormat) => {
|
||||||
|
if (milliseconds === null) {
|
||||||
|
return '...';
|
||||||
|
}
|
||||||
|
const timeFormat = resolver();
|
||||||
|
const { showSeconds = false, format: formatString = 'hh:mm a' } = options || {};
|
||||||
|
return timeFormat === '12'
|
||||||
|
? DateTime.fromMillis(milliseconds).toUTC().toFormat(formatString)
|
||||||
|
: millisToString(milliseconds, showSeconds);
|
||||||
|
};
|
||||||
@@ -9,40 +9,40 @@ import InputRow from './InputRow';
|
|||||||
import style from './MessageControl.module.scss';
|
import style from './MessageControl.module.scss';
|
||||||
|
|
||||||
export default function MessageControl() {
|
export default function MessageControl() {
|
||||||
const { data } = useMessageControl();
|
const data = useMessageControl();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={style.messageContainer}>
|
<div className={style.messageContainer}>
|
||||||
<InputRow
|
<InputRow
|
||||||
label='Timer screen message'
|
label='Timer screen message'
|
||||||
placeholder='Shown in stage timer'
|
placeholder='Shown in stage timer'
|
||||||
text={data?.messages.presenter.text || ''}
|
text={data.timerMessage.text || ''}
|
||||||
visible={data?.messages.presenter.visible || false}
|
visible={data.timerMessage.visible || false}
|
||||||
changeHandler={(newValue) => setMessage.presenterText(newValue)}
|
changeHandler={(newValue) => setMessage.presenterText(newValue)}
|
||||||
actionHandler={() => setMessage.presenterVisible(!data?.messages.presenter.visible)}
|
actionHandler={() => setMessage.presenterVisible(!data.timerMessage.visible)}
|
||||||
/>
|
/>
|
||||||
<InputRow
|
<InputRow
|
||||||
label='Public / Backstage screen message'
|
label='Public / Backstage screen message'
|
||||||
placeholder='Shown in public and backstage screens'
|
placeholder='Shown in public and backstage screens'
|
||||||
text={data?.messages.public.text || ''}
|
text={data.publicMessage.text || ''}
|
||||||
visible={data?.messages.public.visible || false}
|
visible={data.publicMessage.visible || false}
|
||||||
changeHandler={(newValue) => setMessage.publicText(newValue)}
|
changeHandler={(newValue) => setMessage.publicText(newValue)}
|
||||||
actionHandler={() => setMessage.publicVisible(!data?.messages.public.visible)}
|
actionHandler={() => setMessage.publicVisible(!data.publicMessage.visible)}
|
||||||
/>
|
/>
|
||||||
<InputRow
|
<InputRow
|
||||||
label='Lower third message'
|
label='Lower third message'
|
||||||
placeholder='Shown in lower third'
|
placeholder='Shown in lower third'
|
||||||
text={data?.messages.lower.text || ''}
|
text={data.lowerMessage.text || ''}
|
||||||
visible={data?.messages.lower.visible || false}
|
visible={data.lowerMessage.visible || false}
|
||||||
changeHandler={(newValue) => setMessage.lowerText(newValue)}
|
changeHandler={(newValue) => setMessage.lowerText(newValue)}
|
||||||
actionHandler={() => setMessage.lowerVisible(!data?.messages.lower.visible)}
|
actionHandler={() => setMessage.lowerVisible(!data.lowerMessage.visible)}
|
||||||
/>
|
/>
|
||||||
<div className={style.onAirSection}>
|
<div className={style.onAirSection}>
|
||||||
<label className={style.label}>Toggle On Air state</label>
|
<label className={style.label}>Toggle On Air state</label>
|
||||||
<Button
|
<Button
|
||||||
variant={data?.onAir ? 'ontime-filled' : 'ontime-subtle'}
|
variant={data.onAir ? 'ontime-filled' : 'ontime-subtle'}
|
||||||
leftIcon={data?.onAir ? <IoMicSharp size='24px' /> : <IoMicOffOutline size='24px' />}
|
leftIcon={data.onAir ? <IoMicSharp size='24px' /> : <IoMicOffOutline size='24px' />}
|
||||||
onClick={() => setMessage.onAir(!data?.onAir)}
|
onClick={() => setMessage.onAir(!data.onAir)}
|
||||||
>
|
>
|
||||||
{data?.onAir ? 'Ontime is On Air' : 'Ontime is Off Air'}
|
{data?.onAir ? 'Ontime is On Air' : 'Ontime is Off Air'}
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
@@ -5,24 +5,15 @@ import Transport from './Transport';
|
|||||||
|
|
||||||
interface PlaybackButtonsProps {
|
interface PlaybackButtonsProps {
|
||||||
playback: Playback;
|
playback: Playback;
|
||||||
selectedId: string | null;
|
|
||||||
noEvents: boolean;
|
noEvents: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function PlaybackButtons(props: PlaybackButtonsProps) {
|
export default function PlaybackButtons(props: PlaybackButtonsProps) {
|
||||||
const { playback, selectedId, noEvents } = props;
|
const { playback, noEvents } = props;
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<PlaybackDisplay
|
<PlaybackDisplay playback={playback} noEvents={noEvents} />
|
||||||
playback={playback}
|
<Transport playback={playback} noEvents={noEvents} />
|
||||||
selectedId={selectedId}
|
|
||||||
noEvents={noEvents}
|
|
||||||
/>
|
|
||||||
<Transport
|
|
||||||
playback={playback}
|
|
||||||
selectedId={selectedId}
|
|
||||||
noEvents={noEvents}
|
|
||||||
/>
|
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -8,19 +8,12 @@ import PlaybackTimer from './PlaybackTimer';
|
|||||||
import style from './PlaybackControl.module.scss';
|
import style from './PlaybackControl.module.scss';
|
||||||
|
|
||||||
export default function PlaybackControl() {
|
export default function PlaybackControl() {
|
||||||
const { data } = usePlaybackControl();
|
const data = usePlaybackControl();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={style.mainContainer}>
|
<div className={style.mainContainer}>
|
||||||
<PlaybackTimer
|
<PlaybackTimer playback={data.playback as Playback} />
|
||||||
playback={data.playback as Playback}
|
<PlaybackButtons playback={data.playback} noEvents={data.numEvents < 1} />
|
||||||
selectedId={data.selectedEventId}
|
|
||||||
/>
|
|
||||||
<PlaybackButtons
|
|
||||||
playback={data.playback}
|
|
||||||
selectedId={data.selectedEventId}
|
|
||||||
noEvents={data.numEvents < 1}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,23 +11,23 @@ import style from './PlaybackControl.module.scss';
|
|||||||
|
|
||||||
interface PlaybackProps {
|
interface PlaybackProps {
|
||||||
playback: Playback;
|
playback: Playback;
|
||||||
selectedId: string | null;
|
|
||||||
noEvents: boolean;
|
noEvents: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function PlaybackDisplay(props: PlaybackProps) {
|
export default function PlaybackDisplay(props: PlaybackProps) {
|
||||||
const { playback, selectedId, noEvents } = props;
|
const { playback, noEvents } = props;
|
||||||
const isRolling = playback === 'roll';
|
const isRolling = playback === Playback.Roll;
|
||||||
const isPlaying = playback === 'play';
|
const isPlaying = playback === Playback.Play;
|
||||||
const isPaused = playback === 'pause';
|
const isPaused = playback === Playback.Pause;
|
||||||
const isArmed = playback === 'armed';
|
const isArmed = playback === Playback.Armed;
|
||||||
|
const isStopped = playback === Playback.Stop;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={style.playbackContainer}>
|
<div className={style.playbackContainer}>
|
||||||
<TapButton
|
<TapButton
|
||||||
onClick={() => setPlayback.start()}
|
onClick={() => setPlayback.start()}
|
||||||
disabled={!selectedId || isRolling}
|
disabled={isStopped || isRolling}
|
||||||
theme='play'
|
theme={Playback.Play}
|
||||||
active={isPlaying}
|
active={isPlaying}
|
||||||
>
|
>
|
||||||
<IoPlay />
|
<IoPlay />
|
||||||
@@ -35,8 +35,8 @@ export default function PlaybackDisplay(props: PlaybackProps) {
|
|||||||
|
|
||||||
<TapButton
|
<TapButton
|
||||||
onClick={() => setPlayback.pause()}
|
onClick={() => setPlayback.pause()}
|
||||||
disabled={!selectedId || isRolling || isArmed}
|
disabled={isStopped || isRolling || isArmed}
|
||||||
theme='pause'
|
theme={Playback.Pause}
|
||||||
active={isPaused}
|
active={isPaused}
|
||||||
>
|
>
|
||||||
<IoPause />
|
<IoPause />
|
||||||
@@ -44,8 +44,8 @@ export default function PlaybackDisplay(props: PlaybackProps) {
|
|||||||
|
|
||||||
<TapButton
|
<TapButton
|
||||||
onClick={() => setPlayback.roll()}
|
onClick={() => setPlayback.roll()}
|
||||||
disabled={noEvents}
|
disabled={!isStopped || noEvents}
|
||||||
theme='roll'
|
theme={Playback.Roll}
|
||||||
active={isRolling}
|
active={isRolling}
|
||||||
>
|
>
|
||||||
<IoTimeOutline />
|
<IoTimeOutline />
|
||||||
|
|||||||
@@ -4,33 +4,33 @@ import { Playback } from 'ontime-types';
|
|||||||
import TimerDisplay from '../../../common/components/timer-display/TimerDisplay';
|
import TimerDisplay from '../../../common/components/timer-display/TimerDisplay';
|
||||||
import { setPlayback, useTimer } from '../../../common/hooks/useSocket';
|
import { setPlayback, useTimer } from '../../../common/hooks/useSocket';
|
||||||
import { millisToMinutes } from '../../../common/utils/dateConfig';
|
import { millisToMinutes } from '../../../common/utils/dateConfig';
|
||||||
import { stringFromMillis } from '../../../common/utils/time';
|
|
||||||
import { tooltipDelayMid } from '../../../ontimeConfig';
|
import { tooltipDelayMid } from '../../../ontimeConfig';
|
||||||
|
|
||||||
import TapButton from './TapButton';
|
import TapButton from './TapButton';
|
||||||
|
|
||||||
import style from './PlaybackControl.module.scss';
|
import style from './PlaybackControl.module.scss';
|
||||||
|
import { millisToString } from 'ontime-utils';
|
||||||
|
|
||||||
interface PlaybackTimerProps {
|
interface PlaybackTimerProps {
|
||||||
playback: Playback;
|
playback: Playback;
|
||||||
selectedId: string | null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function PlaybackTimer(props: PlaybackTimerProps) {
|
export default function PlaybackTimer(props: PlaybackTimerProps) {
|
||||||
const { playback, selectedId } = props;
|
const { playback } = props;
|
||||||
const { data: timerData } = useTimer();
|
const data = useTimer();
|
||||||
|
|
||||||
// TODO: checkout typescript in utilities
|
// TODO: checkout typescript in utilities
|
||||||
const started = stringFromMillis(timerData?.startedAt, true);
|
const started = millisToString(data.timer.startedAt);
|
||||||
const finish = stringFromMillis(timerData.expectedFinish, true);
|
const finish = millisToString(data.timer.expectedFinish);
|
||||||
const isRolling = playback === 'roll';
|
const isRolling = playback === Playback.Roll;
|
||||||
const isWaiting = timerData.secondaryTimer !== null && timerData.secondaryTimer > 0 && timerData.current === null;
|
const isStopped = playback === Playback.Stop;
|
||||||
const disableButtons = selectedId === null || isRolling;
|
const isWaiting = data.timer.secondaryTimer !== null && data.timer.secondaryTimer > 0 && data.timer.current === null;
|
||||||
const isOvertime = timerData.current !== null && timerData.current < 0;
|
const disableButtons = isStopped || isRolling;
|
||||||
const hasAddedTime = Boolean(timerData.addedTime);
|
const isOvertime = data.timer.current !== null && data.timer.current < 0;
|
||||||
|
const hasAddedTime = Boolean(data.timer.addedTime);
|
||||||
|
|
||||||
const rollLabel = isRolling ? 'Roll mode active' : '';
|
const rollLabel = isRolling ? 'Roll mode active' : '';
|
||||||
const addedTimeLabel = hasAddedTime ? `Added ${millisToMinutes(timerData.addedTime)} minutes` : '';
|
const addedTimeLabel = hasAddedTime ? `Added ${millisToMinutes(data.timer.addedTime)} minutes` : '';
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={style.timeContainer}>
|
<div className={style.timeContainer}>
|
||||||
@@ -44,7 +44,7 @@ export default function PlaybackTimer(props: PlaybackTimerProps) {
|
|||||||
</Tooltip>
|
</Tooltip>
|
||||||
</div>
|
</div>
|
||||||
<div className={style.timer}>
|
<div className={style.timer}>
|
||||||
<TimerDisplay time={isWaiting ? timerData.secondaryTimer : timerData.current} />
|
<TimerDisplay time={isWaiting ? data.timer.secondaryTimer : data.timer.current} />
|
||||||
</div>
|
</div>
|
||||||
{isWaiting ? (
|
{isWaiting ? (
|
||||||
<div className={style.roll}>
|
<div className={style.roll}>
|
||||||
|
|||||||
@@ -14,46 +14,33 @@ import style from './PlaybackControl.module.scss';
|
|||||||
|
|
||||||
interface TransportProps {
|
interface TransportProps {
|
||||||
playback: Playback;
|
playback: Playback;
|
||||||
selectedId: string | null;
|
|
||||||
noEvents: boolean;
|
noEvents: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function Transport(props: TransportProps) {
|
export default function Transport(props: TransportProps) {
|
||||||
const { playback, selectedId, noEvents } = props;
|
const { playback, noEvents } = props;
|
||||||
const isRolling = playback === 'roll';
|
const isRolling = playback === Playback.Roll;
|
||||||
|
const isStopped = playback === Playback.Stop;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={style.playbackContainer}>
|
<div className={style.playbackContainer}>
|
||||||
<Tooltip label='Previous event' openDelay={tooltipDelayMid}>
|
<Tooltip label='Previous event' openDelay={tooltipDelayMid}>
|
||||||
<TapButton
|
<TapButton onClick={() => setPlayback.previous()} disabled={isRolling || noEvents}>
|
||||||
onClick={() => setPlayback.previous()}
|
|
||||||
disabled={isRolling || noEvents}
|
|
||||||
>
|
|
||||||
<IoPlaySkipBack />
|
<IoPlaySkipBack />
|
||||||
</TapButton>
|
</TapButton>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
<Tooltip label='Next event' openDelay={tooltipDelayMid}>
|
<Tooltip label='Next event' openDelay={tooltipDelayMid}>
|
||||||
<TapButton
|
<TapButton onClick={() => setPlayback.next()} disabled={isRolling || noEvents}>
|
||||||
onClick={() => setPlayback.next()}
|
|
||||||
disabled={isRolling || noEvents}
|
|
||||||
>
|
|
||||||
<IoPlaySkipForward />
|
<IoPlaySkipForward />
|
||||||
</TapButton>
|
</TapButton>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
<Tooltip label='Reload event' openDelay={tooltipDelayMid}>
|
<Tooltip label='Reload event' openDelay={tooltipDelayMid}>
|
||||||
<TapButton
|
<TapButton onClick={() => setPlayback.reload()} disabled={isStopped || isRolling}>
|
||||||
onClick={() => setPlayback.reload()}
|
|
||||||
disabled={!selectedId || isRolling}
|
|
||||||
>
|
|
||||||
<IoReload className={style.invertX} />
|
<IoReload className={style.invertX} />
|
||||||
</TapButton>
|
</TapButton>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
<Tooltip label='Unload Event' openDelay={tooltipDelayMid}>
|
<Tooltip label='Unload Event' openDelay={tooltipDelayMid}>
|
||||||
<TapButton
|
<TapButton onClick={() => setPlayback.stop()} disabled={isStopped && !isRolling} theme={Playback.Stop}>
|
||||||
onClick={() => setPlayback.stop()}
|
|
||||||
disabled={!selectedId && !isRolling}
|
|
||||||
theme='stop'
|
|
||||||
>
|
|
||||||
<IoStop />
|
<IoStop />
|
||||||
</TapButton>
|
</TapButton>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
|
|||||||
@@ -1,20 +1,20 @@
|
|||||||
import { useCallback, useContext, useEffect, useState } from 'react';
|
import { useCallback, useEffect, useState } from 'react';
|
||||||
import { Button, Select, Switch } from '@chakra-ui/react';
|
import { Button, Select, Switch } from '@chakra-ui/react';
|
||||||
import { IoBan } from '@react-icons/all-files/io5/IoBan';
|
import { IoBan } from '@react-icons/all-files/io5/IoBan';
|
||||||
import { useAtom } from 'jotai';
|
import { useAtom } from 'jotai';
|
||||||
import { OntimeEvent, TimerType } from 'ontime-types';
|
import { OntimeEvent, TimerType } from 'ontime-types';
|
||||||
|
import { millisToString } from 'ontime-utils';
|
||||||
|
|
||||||
import { editorEventId } from '../../common/atoms/LocalEventSettings';
|
import { editorEventId } from '../../common/atoms/LocalEventSettings';
|
||||||
import CopyTag from '../../common/components/copy-tag/CopyTag';
|
import CopyTag from '../../common/components/copy-tag/CopyTag';
|
||||||
import ColourInput from '../../common/components/input/colour-input/ColourInput';
|
import ColourInput from '../../common/components/input/colour-input/ColourInput';
|
||||||
import TextInput from '../../common/components/input/text-input/TextInput';
|
import TextInput from '../../common/components/input/text-input/TextInput';
|
||||||
import TimeInput from '../../common/components/input/time-input/TimeInput';
|
import TimeInput from '../../common/components/input/time-input/TimeInput';
|
||||||
import { LoggingContext } from '../../common/context/LoggingContext';
|
|
||||||
import { useEventAction } from '../../common/hooks/useEventAction';
|
import { useEventAction } from '../../common/hooks/useEventAction';
|
||||||
import useRundown from '../../common/hooks-query/useRundown';
|
import useRundown from '../../common/hooks-query/useRundown';
|
||||||
|
import { useEmitLog } from '../../common/stores/logger';
|
||||||
import { millisToMinutes } from '../../common/utils/dateConfig';
|
import { millisToMinutes } from '../../common/utils/dateConfig';
|
||||||
import getDelayTo from '../../common/utils/getDelayTo';
|
import getDelayTo from '../../common/utils/getDelayTo';
|
||||||
import { stringFromMillis } from '../../common/utils/time';
|
|
||||||
import { calculateDuration, TimeEntryField, validateEntry } from '../../common/utils/timesManager';
|
import { calculateDuration, TimeEntryField, validateEntry } from '../../common/utils/timesManager';
|
||||||
|
|
||||||
import style from './EventEditor.module.scss';
|
import style from './EventEditor.module.scss';
|
||||||
@@ -25,7 +25,7 @@ export type EventEditorSubmitActions = keyof OntimeEvent | 'durationOverride';
|
|||||||
export default function EventEditor() {
|
export default function EventEditor() {
|
||||||
const [openId] = useAtom(editorEventId);
|
const [openId] = useAtom(editorEventId);
|
||||||
const { data } = useRundown();
|
const { data } = useRundown();
|
||||||
const { emitWarning, emitError } = useContext(LoggingContext);
|
const { emitWarning, emitError } = useEmitLog();
|
||||||
const { updateEvent } = useEventAction();
|
const { updateEvent } = useEventAction();
|
||||||
const [event, setEvent] = useState<OntimeEvent | null>(null);
|
const [event, setEvent] = useState<OntimeEvent | null>(null);
|
||||||
const [delay, setDelay] = useState(0);
|
const [delay, setDelay] = useState(0);
|
||||||
@@ -121,8 +121,8 @@ export default function EventEditor() {
|
|||||||
|
|
||||||
const delayed = delay !== 0;
|
const delayed = delay !== 0;
|
||||||
const addedTime = delayed ? `${delay >= 0 ? '+' : '-'} ${millisToMinutes(Math.abs(delay))} minutes` : null;
|
const addedTime = delayed ? `${delay >= 0 ? '+' : '-'} ${millisToMinutes(Math.abs(delay))} minutes` : null;
|
||||||
const newStart = delayed ? `New start ${stringFromMillis(event.timeStart + delay)}` : null;
|
const newStart = delayed ? `New start ${millisToString(event.timeStart + delay)}` : null;
|
||||||
const newEnd = delayed ? `New end ${stringFromMillis(event.timeEnd + delay)}` : null;
|
const newEnd = delayed ? `New end ${millisToString(event.timeEnd + delay)}` : null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={style.eventEditor}>
|
<div className={style.eventEditor}>
|
||||||
|
|||||||
@@ -1,52 +1,21 @@
|
|||||||
import { useState } from 'react';
|
import { PropsWithChildren, useState } from 'react';
|
||||||
|
|
||||||
import CollapseBar from '../../common/components/collapse-bar/CollapseBar';
|
import CollapseBar from '../../common/components/collapse-bar/CollapseBar';
|
||||||
|
|
||||||
import style from './Info.module.scss';
|
import style from './Info.module.scss';
|
||||||
|
|
||||||
type TitleShape = {
|
|
||||||
title: string;
|
|
||||||
presenter: string;
|
|
||||||
subtitle: string;
|
|
||||||
note: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface CollapsableInfoProps {
|
interface CollapsableInfoProps {
|
||||||
title: string;
|
title: string;
|
||||||
data: TitleShape;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function CollapsableInfo(props: CollapsableInfoProps) {
|
export default function CollapsableInfo(props: PropsWithChildren<CollapsableInfoProps>) {
|
||||||
const { title, data } = props;
|
const { title, children } = props;
|
||||||
const [collapsed, setCollapsed] = useState(false);
|
const [collapsed, setCollapsed] = useState(false);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={style.container}>
|
<div className={style.container}>
|
||||||
<CollapseBar
|
<CollapseBar title={title} isCollapsed={collapsed} onClick={() => setCollapsed((prev) => !prev)} />
|
||||||
title={title}
|
{!collapsed && children}
|
||||||
isCollapsed={collapsed}
|
|
||||||
onClick={() => setCollapsed((prev) => !prev)}
|
|
||||||
/>
|
|
||||||
{!collapsed && (
|
|
||||||
<div className={style.labels}>
|
|
||||||
<div>
|
|
||||||
<span className={style.label}>Title:</span>
|
|
||||||
<span className={style.content}>{data.title}</span>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<span className={style.label}>Presenter:</span>
|
|
||||||
<span className={style.content}>{data.presenter}</span>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<span className={style.label}>Subtitle:</span>
|
|
||||||
<span className={style.content}>{data.subtitle}</span>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<span className={style.label}>Note:</span>
|
|
||||||
<span className={style.content}>{data.note}</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,44 +0,0 @@
|
|||||||
import { useInfoPanel } from '../../common/hooks/useSocket';
|
|
||||||
|
|
||||||
import InfoTitle from './CollapsableInfo';
|
|
||||||
import InfoLogger from './InfoLogger';
|
|
||||||
import InfoNif from './InfoNif';
|
|
||||||
|
|
||||||
import style from './Info.module.scss';
|
|
||||||
|
|
||||||
export default function Info() {
|
|
||||||
const { data } = useInfoPanel();
|
|
||||||
|
|
||||||
const titlesNow = {
|
|
||||||
title: data.titles.titleNow,
|
|
||||||
subtitle: data.titles.subtitleNow,
|
|
||||||
presenter: data.titles.presenterNow,
|
|
||||||
note: data.titles.noteNow,
|
|
||||||
};
|
|
||||||
|
|
||||||
const titlesNext = {
|
|
||||||
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 (
|
|
||||||
<>
|
|
||||||
<div className={style.panelHeader}>
|
|
||||||
<span>Ontime running on port 4001</span>
|
|
||||||
<span>{selected}</span>
|
|
||||||
</div>
|
|
||||||
<InfoNif />
|
|
||||||
<InfoTitle title='Playing Now' data={titlesNow} />
|
|
||||||
<InfoTitle title='Playing Next' data={titlesNext} />
|
|
||||||
<InfoLogger />
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,53 @@
|
|||||||
|
import { useInfoPanel } from '../../common/hooks/useSocket';
|
||||||
|
|
||||||
|
import CollapsableInfo from './CollapsableInfo';
|
||||||
|
import InfoLogger from './InfoLogger';
|
||||||
|
import InfoNif from './InfoNif';
|
||||||
|
import InfoTitles from './InfoTitles';
|
||||||
|
|
||||||
|
import style from './Info.module.scss';
|
||||||
|
|
||||||
|
export default function Info() {
|
||||||
|
const data = useInfoPanel();
|
||||||
|
|
||||||
|
const titlesNow = {
|
||||||
|
title: data.titles.titleNow || '',
|
||||||
|
subtitle: data.titles.subtitleNow || '',
|
||||||
|
presenter: data.titles.presenterNow || '',
|
||||||
|
note: data.titles.noteNow || '',
|
||||||
|
};
|
||||||
|
|
||||||
|
const titlesNext = {
|
||||||
|
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 (
|
||||||
|
<>
|
||||||
|
<div className={style.panelHeader}>
|
||||||
|
<span>Ontime running on port 4001</span>
|
||||||
|
<span>{selected}</span>
|
||||||
|
</div>
|
||||||
|
<CollapsableInfo title='Network Info'>
|
||||||
|
<InfoNif />
|
||||||
|
</CollapsableInfo>
|
||||||
|
<CollapsableInfo title='Playing Now'>
|
||||||
|
<InfoTitles data={titlesNow} />
|
||||||
|
</CollapsableInfo>
|
||||||
|
<CollapsableInfo title='Playing Next'>
|
||||||
|
<InfoTitles data={titlesNext} />
|
||||||
|
</CollapsableInfo>
|
||||||
|
<CollapsableInfo title='Log'>
|
||||||
|
<InfoLogger />
|
||||||
|
</CollapsableInfo>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -6,12 +6,7 @@ $info-hover: $section-white;
|
|||||||
|
|
||||||
.infoLoggerContainer {
|
.infoLoggerContainer {
|
||||||
max-height: 80%;
|
max-height: 80%;
|
||||||
margin-top: 32px;
|
height: 100%
|
||||||
|
|
||||||
&.expanded {
|
|
||||||
min-height: 50%;
|
|
||||||
height: 100%
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.log {
|
.log {
|
||||||
@@ -24,6 +19,7 @@ $info-hover: $section-white;
|
|||||||
.logEntry {
|
.logEntry {
|
||||||
display: flex;
|
display: flex;
|
||||||
margin-bottom: 2px;
|
margin-bottom: 2px;
|
||||||
|
|
||||||
&.INFO {
|
&.INFO {
|
||||||
color: $info-gray;
|
color: $info-gray;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,24 +1,22 @@
|
|||||||
import { useCallback, useContext, useEffect, useState } from 'react';
|
import { useCallback, useState } from 'react';
|
||||||
import { Button } from '@chakra-ui/react';
|
import { Button } from '@chakra-ui/react';
|
||||||
|
|
||||||
import CollapseBar from '../../common/components/collapse-bar/CollapseBar';
|
import { clearLogs, useLogData } from '../../common/stores/logger';
|
||||||
import { Log, LoggingContext } from '../../common/context/LoggingContext';
|
|
||||||
|
|
||||||
import style from './InfoLogger.module.scss';
|
import style from './InfoLogger.module.scss';
|
||||||
|
|
||||||
enum LOG_FILTER {
|
enum LogFilter {
|
||||||
USER = 'USER',
|
User = 'USER',
|
||||||
CLIENT = 'CLIENT',
|
Client = 'CLIENT',
|
||||||
SERVER = 'SERVER',
|
Server = 'SERVER',
|
||||||
RX = 'RX',
|
RX = 'RX',
|
||||||
TX = 'TX',
|
TX = 'TX',
|
||||||
PLAYBACK = 'PLAYBACK',
|
Playback = 'PLAYBACK',
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function InfoLogger() {
|
export default function InfoLogger() {
|
||||||
const { logData, clearLog } = useContext(LoggingContext);
|
const { logs: logData } = useLogData();
|
||||||
const [data, setData] = useState<Log[]>([]);
|
|
||||||
const [collapsed, setCollapsed] = useState(false);
|
|
||||||
const [showClient, setShowClient] = useState(true);
|
const [showClient, setShowClient] = useState(true);
|
||||||
const [showServer, setShowServer] = useState(true);
|
const [showServer, setShowServer] = useState(true);
|
||||||
const [showRx, setShowRx] = useState(true);
|
const [showRx, setShowRx] = useState(true);
|
||||||
@@ -26,123 +24,107 @@ export default function InfoLogger() {
|
|||||||
const [showPlayback, setShowPlayback] = useState(true);
|
const [showPlayback, setShowPlayback] = useState(true);
|
||||||
const [showUser, setShowUser] = useState(true);
|
const [showUser, setShowUser] = useState(true);
|
||||||
|
|
||||||
useEffect(() => {
|
const matchers: LogFilter[] = [];
|
||||||
if (!logData) {
|
if (showUser) {
|
||||||
return;
|
matchers.push(LogFilter.User);
|
||||||
}
|
}
|
||||||
|
if (showClient) {
|
||||||
|
matchers.push(LogFilter.Client);
|
||||||
|
}
|
||||||
|
if (showServer) {
|
||||||
|
matchers.push(LogFilter.Server);
|
||||||
|
}
|
||||||
|
if (showRx) {
|
||||||
|
matchers.push(LogFilter.RX);
|
||||||
|
}
|
||||||
|
if (showTx) {
|
||||||
|
matchers.push(LogFilter.TX);
|
||||||
|
}
|
||||||
|
if (showPlayback) {
|
||||||
|
matchers.push(LogFilter.Playback);
|
||||||
|
}
|
||||||
|
|
||||||
const matchers: LOG_FILTER[] = [];
|
const filteredData = logData.filter((entry) => matchers.some((match) => entry.origin === match));
|
||||||
if (showUser) {
|
|
||||||
matchers.push(LOG_FILTER.USER);
|
|
||||||
}
|
|
||||||
if (showClient) {
|
|
||||||
matchers.push(LOG_FILTER.CLIENT);
|
|
||||||
}
|
|
||||||
if (showServer) {
|
|
||||||
matchers.push(LOG_FILTER.SERVER);
|
|
||||||
}
|
|
||||||
if (showRx) {
|
|
||||||
matchers.push(LOG_FILTER.RX);
|
|
||||||
}
|
|
||||||
if (showTx) {
|
|
||||||
matchers.push(LOG_FILTER.TX);
|
|
||||||
}
|
|
||||||
if (showPlayback) {
|
|
||||||
matchers.push(LOG_FILTER.PLAYBACK);
|
|
||||||
}
|
|
||||||
|
|
||||||
const filteredData = logData.filter((entry) => matchers.some((match) => entry.origin === match));
|
const disableOthers = useCallback((toEnable: LogFilter) => {
|
||||||
setData(filteredData);
|
toEnable === LogFilter.User ? setShowUser(true) : setShowUser(false);
|
||||||
}, [logData, showUser, showClient, showServer, showPlayback, showRx, showTx]);
|
toEnable === LogFilter.Client ? setShowClient(true) : setShowClient(false);
|
||||||
|
toEnable === LogFilter.Server ? setShowServer(true) : setShowServer(false);
|
||||||
const disableOthers = useCallback((toEnable: LOG_FILTER) => {
|
toEnable === LogFilter.RX ? setShowRx(true) : setShowRx(false);
|
||||||
toEnable === LOG_FILTER.USER ? setShowUser(true) : setShowUser(false);
|
toEnable === LogFilter.TX ? setShowTx(true) : setShowTx(false);
|
||||||
toEnable === LOG_FILTER.CLIENT ? setShowClient(true) : setShowClient(false);
|
toEnable === LogFilter.Playback ? setShowPlayback(true) : setShowPlayback(false);
|
||||||
toEnable === LOG_FILTER.SERVER ? setShowServer(true) : setShowServer(false);
|
|
||||||
toEnable === LOG_FILTER.RX ? setShowRx(true) : setShowRx(false);
|
|
||||||
toEnable === LOG_FILTER.TX ? setShowTx(true) : setShowTx(false);
|
|
||||||
toEnable === LOG_FILTER.PLAYBACK ? setShowPlayback(true) : setShowPlayback(false);
|
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={`${style.infoLoggerContainer} ${collapsed? '' : style.expanded}`}>
|
<div className={style.infoLoggerContainer}>
|
||||||
<CollapseBar title='Log' isCollapsed={collapsed} onClick={() => setCollapsed((prev) => !prev)} />
|
<div className={style.buttonBar}>
|
||||||
{!collapsed && (
|
<Button
|
||||||
<>
|
variant={showUser ? 'ontime-filled' : 'ontime-subtle'}
|
||||||
<div className={style.buttonBar}>
|
size='xs'
|
||||||
<Button
|
onClick={() => setShowUser((s) => !s)}
|
||||||
variant={showUser ? 'ontime-filled' : 'ontime-subtle'}
|
onAuxClick={() => disableOthers(LogFilter.User)}
|
||||||
size='xs'
|
onContextMenu={(e) => e.preventDefault()}
|
||||||
onClick={() => setShowUser((s) => !s)}
|
>
|
||||||
onAuxClick={() => disableOthers(LOG_FILTER.USER)}
|
{LogFilter.User}
|
||||||
onContextMenu={(e) => e.preventDefault()}
|
</Button>
|
||||||
>
|
<Button
|
||||||
USER
|
variant={showClient ? 'ontime-filled' : 'ontime-subtle'}
|
||||||
</Button>
|
size='xs'
|
||||||
<Button
|
onClick={() => setShowClient((s) => !s)}
|
||||||
variant={showClient ? 'ontime-filled' : 'ontime-subtle'}
|
onAuxClick={() => disableOthers(LogFilter.Client)}
|
||||||
size='xs'
|
onContextMenu={(e) => e.preventDefault()}
|
||||||
onClick={() => setShowClient((s) => !s)}
|
>
|
||||||
onAuxClick={() => disableOthers(LOG_FILTER.CLIENT)}
|
{LogFilter.Client}
|
||||||
onContextMenu={(e) => e.preventDefault()}
|
</Button>
|
||||||
>
|
<Button
|
||||||
CLIENT
|
variant={showServer ? 'ontime-filled' : 'ontime-subtle'}
|
||||||
</Button>
|
size='xs'
|
||||||
<Button
|
onClick={() => setShowServer((s) => !s)}
|
||||||
variant={showServer ? 'ontime-filled' : 'ontime-subtle'}
|
onAuxClick={() => disableOthers(LogFilter.Server)}
|
||||||
size='xs'
|
onContextMenu={(e) => e.preventDefault()}
|
||||||
onClick={() => setShowServer((s) => !s)}
|
>
|
||||||
onAuxClick={() => disableOthers(LOG_FILTER.SERVER)}
|
{LogFilter.Server}
|
||||||
onContextMenu={(e) => e.preventDefault()}
|
</Button>
|
||||||
>
|
<Button
|
||||||
SERVER
|
variant={showPlayback ? 'ontime-filled' : 'ontime-subtle'}
|
||||||
</Button>
|
size='xs'
|
||||||
<Button
|
onClick={() => setShowPlayback((s) => !s)}
|
||||||
variant={showPlayback ? 'ontime-filled' : 'ontime-subtle'}
|
onAuxClick={() => disableOthers(LogFilter.Playback)}
|
||||||
size='xs'
|
onContextMenu={(e) => e.preventDefault()}
|
||||||
onClick={() => setShowPlayback((s) => !s)}
|
>
|
||||||
onAuxClick={() => disableOthers(LOG_FILTER.PLAYBACK)}
|
{LogFilter.Playback}
|
||||||
onContextMenu={(e) => e.preventDefault()}
|
</Button>
|
||||||
>
|
<Button
|
||||||
PLAYBACK
|
variant={showRx ? 'ontime-filled' : 'ontime-subtle'}
|
||||||
</Button>
|
size='xs'
|
||||||
<Button
|
onClick={() => setShowRx((s) => !s)}
|
||||||
variant={showRx ? 'ontime-filled' : 'ontime-subtle'}
|
onAuxClick={() => disableOthers(LogFilter.RX)}
|
||||||
size='xs'
|
onContextMenu={(e) => e.preventDefault()}
|
||||||
onClick={() => setShowRx((s) => !s)}
|
>
|
||||||
onAuxClick={() => disableOthers(LOG_FILTER.RX)}
|
{LogFilter.RX}
|
||||||
onContextMenu={(e) => e.preventDefault()}
|
</Button>
|
||||||
>
|
<Button
|
||||||
RX
|
variant={showTx ? 'ontime-filled' : 'ontime-subtle'}
|
||||||
</Button>
|
size='xs'
|
||||||
<Button
|
onClick={() => setShowTx((s) => !s)}
|
||||||
variant={showTx ? 'ontime-filled' : 'ontime-subtle'}
|
onAuxClick={() => disableOthers(LogFilter.TX)}
|
||||||
size='xs'
|
onContextMenu={(e) => e.preventDefault()}
|
||||||
onClick={() => setShowTx((s) => !s)}
|
>
|
||||||
onAuxClick={() => disableOthers(LOG_FILTER.TX)}
|
{LogFilter.TX}
|
||||||
onContextMenu={(e) => e.preventDefault()}
|
</Button>
|
||||||
>
|
<Button variant='ontime-outlined' size='xs' onClick={clearLogs}>
|
||||||
TX
|
Clear
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
</div>
|
||||||
variant='ontime-outlined'
|
<ul className={style.log}>
|
||||||
size='xs'
|
{filteredData.map((logEntry) => (
|
||||||
onClick={clearLog}
|
<li key={logEntry.id} className={`${style.logEntry} ${style[logEntry.level]} `}>
|
||||||
>
|
<span className={style.time}>{logEntry.time}</span>
|
||||||
Clear
|
<span className={style.origin}>{logEntry.origin}</span>
|
||||||
</Button>
|
<span className={style.msg}>{logEntry.text}</span>
|
||||||
</div>
|
</li>
|
||||||
<ul className={style.log}>
|
))}
|
||||||
{data.map((logEntry) => (
|
</ul>
|
||||||
<li key={logEntry.id} className={`${style.logEntry} ${style[logEntry.level]} `}>
|
|
||||||
<span className={style.time}>{logEntry.time}</span>
|
|
||||||
<span className={style.origin}>{logEntry.origin}</span>
|
|
||||||
<span className={style.msg}>{logEntry.text}</span>
|
|
||||||
</li>
|
|
||||||
))}
|
|
||||||
</ul>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,5 @@
|
|||||||
import { useState } from 'react';
|
|
||||||
import { IoArrowUp } from '@react-icons/all-files/io5/IoArrowUp';
|
import { IoArrowUp } from '@react-icons/all-files/io5/IoArrowUp';
|
||||||
|
|
||||||
import CollapseBar from '../../common/components/collapse-bar/CollapseBar';
|
|
||||||
import useInfo from '../../common/hooks-query/useInfo';
|
import useInfo from '../../common/hooks-query/useInfo';
|
||||||
import { openLink } from '../../common/utils/linkUtils';
|
import { openLink } from '../../common/utils/linkUtils';
|
||||||
|
|
||||||
@@ -9,7 +7,6 @@ import style from './Info.module.scss';
|
|||||||
|
|
||||||
export default function InfoNif() {
|
export default function InfoNif() {
|
||||||
const { data } = useInfo();
|
const { data } = useInfo();
|
||||||
const [collapsed, setCollapsed] = useState(false);
|
|
||||||
|
|
||||||
const handleClick = (address: string) => {
|
const handleClick = (address: string) => {
|
||||||
const baseURL = 'http://__IP__:4001';
|
const baseURL = 'http://__IP__:4001';
|
||||||
@@ -17,18 +14,13 @@ export default function InfoNif() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={style.container}>
|
<div className={style.interfaceList}>
|
||||||
<CollapseBar title='Network Info' isCollapsed={collapsed} onClick={() => setCollapsed((prev) => !prev)} />
|
{data?.networkInterfaces.map((nif) => (
|
||||||
{!collapsed && (
|
<span key={nif.address} onClick={() => handleClick(nif.address)} className={style.interface}>
|
||||||
<div className={style.interfaceList}>
|
{`${nif.name} - ${nif.address}`}
|
||||||
{data?.networkInterfaces.map((nif) => (
|
<IoArrowUp className={style.linkIcon} />
|
||||||
<span key={nif.address} onClick={() => handleClick(nif.address)} className={style.interface}>
|
</span>
|
||||||
{`${nif.name} - ${nif.address}`}
|
))}
|
||||||
<IoArrowUp className={style.linkIcon} />
|
|
||||||
</span>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,36 @@
|
|||||||
|
import style from './Info.module.scss';
|
||||||
|
|
||||||
|
type TitleShape = {
|
||||||
|
title: string;
|
||||||
|
presenter: string;
|
||||||
|
subtitle: string;
|
||||||
|
note: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
interface InfoTitleProps {
|
||||||
|
data: TitleShape;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function InfoTitles(props: InfoTitleProps) {
|
||||||
|
const { data } = props;
|
||||||
|
return (
|
||||||
|
<div className={style.labels}>
|
||||||
|
<div>
|
||||||
|
<span className={style.label}>Title:</span>
|
||||||
|
<span className={style.content}>{data.title}</span>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<span className={style.label}>Presenter:</span>
|
||||||
|
<span className={style.content}>{data.presenter}</span>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<span className={style.label}>Subtitle:</span>
|
||||||
|
<span className={style.content}>{data.subtitle}</span>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<span className={style.label}>Note:</span>
|
||||||
|
<span className={style.content}>{data.note}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,13 +1,14 @@
|
|||||||
/* eslint-disable jsx-a11y/anchor-has-content */
|
/* eslint-disable jsx-a11y/anchor-has-content */
|
||||||
import { useCallback, useContext, useEffect, useState } from 'react';
|
import { useCallback, useEffect, useState } from 'react';
|
||||||
import { Button, IconButton, Input, ModalBody, Tooltip } from '@chakra-ui/react';
|
import { Button, IconButton, Input, ModalBody, Tooltip } from '@chakra-ui/react';
|
||||||
import { IoInformationCircleOutline } from '@react-icons/all-files/io5/IoInformationCircleOutline';
|
import { IoInformationCircleOutline } from '@react-icons/all-files/io5/IoInformationCircleOutline';
|
||||||
import { IoRemove } from '@react-icons/all-files/io5/IoRemove';
|
import { IoRemove } from '@react-icons/all-files/io5/IoRemove';
|
||||||
import { IoSunny } from '@react-icons/all-files/io5/IoSunny';
|
import { IoSunny } from '@react-icons/all-files/io5/IoSunny';
|
||||||
|
|
||||||
|
import { useEmitLog } from '@/common/stores/logger';
|
||||||
|
|
||||||
import { viewerLocations } from '../../appConstants';
|
import { viewerLocations } from '../../appConstants';
|
||||||
import { postAliases } from '../../common/api/ontimeApi';
|
import { postAliases } from '../../common/api/ontimeApi';
|
||||||
import { LoggingContext } from '../../common/context/LoggingContext';
|
|
||||||
import useAliases from '../../common/hooks-query/useAliases';
|
import useAliases from '../../common/hooks-query/useAliases';
|
||||||
import { validateAlias } from '../../common/utils/aliases';
|
import { validateAlias } from '../../common/utils/aliases';
|
||||||
import { handleLinks, host } from '../../common/utils/linkUtils';
|
import { handleLinks, host } from '../../common/utils/linkUtils';
|
||||||
@@ -19,7 +20,7 @@ import style from './Modals.module.scss';
|
|||||||
|
|
||||||
export default function AliasesModal() {
|
export default function AliasesModal() {
|
||||||
const { data, status, refetch } = useAliases();
|
const { data, status, refetch } = useAliases();
|
||||||
const { emitError } = useContext(LoggingContext);
|
const { emitError } = useEmitLog();
|
||||||
const [changed, setChanged] = useState(false);
|
const [changed, setChanged] = useState(false);
|
||||||
const [submitting, setSubmitting] = useState(false);
|
const [submitting, setSubmitting] = useState(false);
|
||||||
const [aliases, setAliases] = useState([]);
|
const [aliases, setAliases] = useState([]);
|
||||||
@@ -111,29 +112,32 @@ export default function AliasesModal() {
|
|||||||
* @param {string} id - object id
|
* @param {string} id - object id
|
||||||
* @param {boolean} isEnabled - whether to enable / disable flag
|
* @param {boolean} isEnabled - whether to enable / disable flag
|
||||||
*/
|
*/
|
||||||
const setEnabled = useCallback((id, isEnabled) => {
|
const setEnabled = useCallback(
|
||||||
const aliasesState = [...aliases];
|
(id, isEnabled) => {
|
||||||
for (const a of aliasesState) {
|
const aliasesState = [...aliases];
|
||||||
if (a.id === id) {
|
for (const a of aliasesState) {
|
||||||
if (isEnabled) {
|
if (a.id === id) {
|
||||||
if (a.alias === '' || a.pathAndParams === '') {
|
if (isEnabled) {
|
||||||
emitError('Alias incomplete');
|
if (a.alias === '' || a.pathAndParams === '') {
|
||||||
break;
|
emitError('Alias incomplete');
|
||||||
}
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
const isRepeated = aliases.some((r) => a.alias === r.alias && r.enabled);
|
const isRepeated = aliases.some((r) => a.alias === r.alias && r.enabled);
|
||||||
if (isRepeated) {
|
if (isRepeated) {
|
||||||
emitError('There is already an alias with this name');
|
emitError('There is already an alias with this name');
|
||||||
break;
|
break;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
a.enabled = isEnabled;
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
a.enabled = isEnabled;
|
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
}
|
setChanged(true);
|
||||||
setChanged(true);
|
setAliases(aliasesState);
|
||||||
setAliases(aliasesState);
|
},
|
||||||
}, [aliases, emitError]);
|
[aliases, emitError],
|
||||||
|
);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Reverts local state equals to server state
|
* Reverts local state equals to server state
|
||||||
@@ -194,16 +198,16 @@ export default function AliasesModal() {
|
|||||||
eg. a lower third url with some custom parameters
|
eg. a lower third url with some custom parameters
|
||||||
<table>
|
<table>
|
||||||
<tbody>
|
<tbody>
|
||||||
<tr>
|
<tr>
|
||||||
<td className={style.labelNote} style={{ width: '30%' }}>
|
<td className={style.labelNote} style={{ width: '30%' }}>
|
||||||
Alias
|
Alias
|
||||||
</td>
|
</td>
|
||||||
<td className={style.labelNote}>Page URL</td>
|
<td className={style.labelNote}>Page URL</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td>mylower</td>
|
<td>mylower</td>
|
||||||
<td>lower?bg=ff2&text=f00&size=0.6&transition=5</td>
|
<td>lower?bg=ff2&text=f00&size=0.6&transition=5</td>
|
||||||
</tr>
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
<br />
|
<br />
|
||||||
@@ -212,16 +216,16 @@ export default function AliasesModal() {
|
|||||||
eg. an unattended screen that you would need to change route from the app
|
eg. an unattended screen that you would need to change route from the app
|
||||||
<table>
|
<table>
|
||||||
<tbody>
|
<tbody>
|
||||||
<tr>
|
<tr>
|
||||||
<td className={style.labelNote} style={{ width: '30%' }}>
|
<td className={style.labelNote} style={{ width: '30%' }}>
|
||||||
Alias
|
Alias
|
||||||
</td>
|
</td>
|
||||||
<td className={style.labelNote}>Page URL</td>
|
<td className={style.labelNote}>Page URL</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td>thirdfloor</td>
|
<td>thirdfloor</td>
|
||||||
<td>public</td>
|
<td>public</td>
|
||||||
</tr>
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
@@ -254,12 +258,7 @@ export default function AliasesModal() {
|
|||||||
onChange={(event) => handleChange(index, 'pathAndParams', event.target.value)}
|
onChange={(event) => handleChange(index, 'pathAndParams', event.target.value)}
|
||||||
/>
|
/>
|
||||||
<Tooltip label={`Test /${alias.pathAndParams}`} openDelay={tooltipDelayFast}>
|
<Tooltip label={`Test /${alias.pathAndParams}`} openDelay={tooltipDelayFast}>
|
||||||
<a
|
<a href='#!' target='_blank' rel='noreferrer' onClick={(e) => handleLinks(e, alias.pathAndParams)} />
|
||||||
href='#!'
|
|
||||||
target='_blank'
|
|
||||||
rel='noreferrer'
|
|
||||||
onClick={(e) => handleLinks(e, alias.pathAndParams)}
|
|
||||||
/>
|
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
<Tooltip label='Enable alias' openDelay={tooltipDelayFast}>
|
<Tooltip label='Enable alias' openDelay={tooltipDelayFast}>
|
||||||
<IconButton
|
<IconButton
|
||||||
@@ -281,12 +280,8 @@ export default function AliasesModal() {
|
|||||||
/>
|
/>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</div>
|
</div>
|
||||||
{alias.aliasError ? (
|
{alias.aliasError ? <div className={style.error}>{`Alias error: ${alias.aliasError}`}</div> : null}
|
||||||
<div className={style.error}>{`Alias error: ${alias.aliasError}`}</div>
|
{alias.urlError ? <div className={style.error}>{`URL error: ${alias.urlError}`}</div> : null}
|
||||||
) : null}
|
|
||||||
{alias.urlError ? (
|
|
||||||
<div className={style.error}>{`URL error: ${alias.urlError}`}</div>
|
|
||||||
) : null}
|
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
|
|
||||||
@@ -296,12 +291,7 @@ export default function AliasesModal() {
|
|||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<SubmitContainer
|
<SubmitContainer revert={revert} submitting={submitting} changed={changed} status={status} />
|
||||||
revert={revert}
|
|
||||||
submitting={submitting}
|
|
||||||
changed={changed}
|
|
||||||
status={status}
|
|
||||||
/>
|
|
||||||
</form>
|
</form>
|
||||||
</ModalBody>
|
</ModalBody>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { useContext, useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
import isEqual from 'react-fast-compare';
|
import isEqual from 'react-fast-compare';
|
||||||
import {
|
import {
|
||||||
Button,
|
Button,
|
||||||
@@ -16,11 +16,12 @@ import { FiEye } from '@react-icons/all-files/fi/FiEye';
|
|||||||
import { FiX } from '@react-icons/all-files/fi/FiX';
|
import { FiX } from '@react-icons/all-files/fi/FiX';
|
||||||
import { useAtom } from 'jotai';
|
import { useAtom } from 'jotai';
|
||||||
|
|
||||||
|
import { useEmitLog } from '@/common/stores/logger';
|
||||||
|
|
||||||
import { version } from '../../../package.json';
|
import { version } from '../../../package.json';
|
||||||
import { getLatestVersion, postSettings } from '../../common/api/ontimeApi';
|
import { getLatestVersion, postSettings } from '../../common/api/ontimeApi';
|
||||||
import { eventSettingsAtom } from '../../common/atoms/LocalEventSettings';
|
import { eventSettingsAtom } from '../../common/atoms/LocalEventSettings';
|
||||||
import TooltipActionBtn from '../../common/components/buttons/TooltipActionBtn';
|
import TooltipActionBtn from '../../common/components/buttons/TooltipActionBtn';
|
||||||
import { LoggingContext } from '../../common/context/LoggingContext';
|
|
||||||
import useSettings from '../../common/hooks-query/useSettings';
|
import useSettings from '../../common/hooks-query/useSettings';
|
||||||
import { ontimePlaceholderSettings } from '../../common/models/OntimeSettings';
|
import { ontimePlaceholderSettings } from '../../common/models/OntimeSettings';
|
||||||
|
|
||||||
@@ -31,7 +32,7 @@ import style from './Modals.module.scss';
|
|||||||
|
|
||||||
export default function AppSettingsModal() {
|
export default function AppSettingsModal() {
|
||||||
const { data, status, refetch } = useSettings();
|
const { data, status, refetch } = useSettings();
|
||||||
const { emitError, emitWarning } = useContext(LoggingContext);
|
const { emitError, emitWarning } = useEmitLog();
|
||||||
const [formData, setFormData] = useState(ontimePlaceholderSettings);
|
const [formData, setFormData] = useState(ontimePlaceholderSettings);
|
||||||
const [changed, setChanged] = useState(false);
|
const [changed, setChanged] = useState(false);
|
||||||
const [submitting, setSubmitting] = useState(false);
|
const [submitting, setSubmitting] = useState(false);
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
import { useCallback, useContext, useEffect, useState } from 'react';
|
import { useCallback, useEffect, useState } from 'react';
|
||||||
import { FormLabel, Input, ModalBody, Textarea } from '@chakra-ui/react';
|
import { FormLabel, Input, ModalBody, Textarea } from '@chakra-ui/react';
|
||||||
|
|
||||||
|
import { useEmitLog } from '@/common/stores/logger';
|
||||||
|
|
||||||
import { postEventData } from '../../common/api/eventDataApi';
|
import { postEventData } from '../../common/api/eventDataApi';
|
||||||
import { LoggingContext } from '../../common/context/LoggingContext';
|
|
||||||
import useEventData from '../../common/hooks-query/useEventData';
|
import useEventData from '../../common/hooks-query/useEventData';
|
||||||
import { eventDataPlaceholder } from '../../common/models/EventData';
|
import { eventDataPlaceholder } from '../../common/models/EventData';
|
||||||
|
|
||||||
@@ -13,7 +14,7 @@ import style from './Modals.module.scss';
|
|||||||
|
|
||||||
export default function SettingsModal() {
|
export default function SettingsModal() {
|
||||||
const { data, status, refetch } = useEventData();
|
const { data, status, refetch } = useEventData();
|
||||||
const { emitError } = useContext(LoggingContext);
|
const { emitError } = useEmitLog();
|
||||||
const [formData, setFormData] = useState(eventDataPlaceholder);
|
const [formData, setFormData] = useState(eventDataPlaceholder);
|
||||||
const [changed, setChanged] = useState(false);
|
const [changed, setChanged] = useState(false);
|
||||||
const [submitting, setSubmitting] = useState(false);
|
const [submitting, setSubmitting] = useState(false);
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
import { useCallback, useContext, useEffect, useState } from 'react';
|
import { useCallback, useEffect, useState } from 'react';
|
||||||
import { Input, ModalBody } from '@chakra-ui/react';
|
import { Input, ModalBody } from '@chakra-ui/react';
|
||||||
import { IoInformationCircleOutline } from '@react-icons/all-files/io5/IoInformationCircleOutline';
|
import { IoInformationCircleOutline } from '@react-icons/all-files/io5/IoInformationCircleOutline';
|
||||||
|
|
||||||
|
import { useEmitLog } from '@/common/stores/logger';
|
||||||
|
|
||||||
import { postUserFields } from '../../common/api/ontimeApi';
|
import { postUserFields } from '../../common/api/ontimeApi';
|
||||||
import { LoggingContext } from '../../common/context/LoggingContext';
|
|
||||||
import useUserFields from '../../common/hooks-query/useUserFields';
|
import useUserFields from '../../common/hooks-query/useUserFields';
|
||||||
import { userFieldsPlaceholder } from '../../common/models/UserFields';
|
import { userFieldsPlaceholder } from '../../common/models/UserFields';
|
||||||
import { handleLinks, host } from '../../common/utils/linkUtils';
|
import { handleLinks, host } from '../../common/utils/linkUtils';
|
||||||
@@ -14,7 +15,7 @@ import style from './Modals.module.scss';
|
|||||||
|
|
||||||
export default function TableOptionsModal() {
|
export default function TableOptionsModal() {
|
||||||
const { data, status, refetch } = useUserFields();
|
const { data, status, refetch } = useUserFields();
|
||||||
const { emitError } = useContext(LoggingContext);
|
const { emitError } = useEmitLog();
|
||||||
const [changed, setChanged] = useState(false);
|
const [changed, setChanged] = useState(false);
|
||||||
const [submitting, setSubmitting] = useState(false);
|
const [submitting, setSubmitting] = useState(false);
|
||||||
const [userFields, setUserFields] = useState(userFieldsPlaceholder);
|
const [userFields, setUserFields] = useState(userFieldsPlaceholder);
|
||||||
|
|||||||
@@ -1,11 +1,12 @@
|
|||||||
import { useCallback, useContext, useEffect, useState } from 'react';
|
import { useCallback, useEffect, useState } from 'react';
|
||||||
import { FormControl, FormLabel, ModalBody } from '@chakra-ui/react';
|
import { FormControl, FormLabel, ModalBody } from '@chakra-ui/react';
|
||||||
import { IoCheckmarkSharp } from '@react-icons/all-files/io5/IoCheckmarkSharp';
|
import { IoCheckmarkSharp } from '@react-icons/all-files/io5/IoCheckmarkSharp';
|
||||||
import { IoInformationCircleOutline } from '@react-icons/all-files/io5/IoInformationCircleOutline';
|
import { IoInformationCircleOutline } from '@react-icons/all-files/io5/IoInformationCircleOutline';
|
||||||
|
|
||||||
|
import { useEmitLog } from '@/common/stores/logger';
|
||||||
|
|
||||||
import { postView } from '../../common/api/ontimeApi';
|
import { postView } from '../../common/api/ontimeApi';
|
||||||
import EnableBtn from '../../common/components/buttons/EnableBtn';
|
import EnableBtn from '../../common/components/buttons/EnableBtn';
|
||||||
import { LoggingContext } from '../../common/context/LoggingContext';
|
|
||||||
import useViewSettings from '../../common/hooks-query/useViewSettings';
|
import useViewSettings from '../../common/hooks-query/useViewSettings';
|
||||||
import { viewsSettingsPlaceholder } from '../../common/models/ViewSettings.type';
|
import { viewsSettingsPlaceholder } from '../../common/models/ViewSettings.type';
|
||||||
import { openLink } from '../../common/utils/linkUtils';
|
import { openLink } from '../../common/utils/linkUtils';
|
||||||
@@ -17,7 +18,7 @@ import style from './Modals.module.scss';
|
|||||||
export default function ViewsSettingsModal() {
|
export default function ViewsSettingsModal() {
|
||||||
const { data, status, refetch } = useViewSettings();
|
const { data, status, refetch } = useViewSettings();
|
||||||
|
|
||||||
const { emitError } = useContext(LoggingContext);
|
const { emitError } = useEmitLog();
|
||||||
const [formData, setFormData] = useState(viewsSettingsPlaceholder);
|
const [formData, setFormData] = useState(viewsSettingsPlaceholder);
|
||||||
const [changed, setChanged] = useState(false);
|
const [changed, setChanged] = useState(false);
|
||||||
const [submitting, setSubmitting] = useState(false);
|
const [submitting, setSubmitting] = useState(false);
|
||||||
|
|||||||
@@ -1,18 +1,17 @@
|
|||||||
import { useContext } from 'react';
|
|
||||||
import { useForm } from 'react-hook-form';
|
import { useForm } from 'react-hook-form';
|
||||||
import { Button, FormControl, Input, ModalBody, ModalFooter, Switch } from '@chakra-ui/react';
|
import { Button, FormControl, Input, ModalBody, ModalFooter, Switch } from '@chakra-ui/react';
|
||||||
|
|
||||||
import { postOSC } from '../../../common/api/ontimeApi';
|
import { postOSC } from '../../../common/api/ontimeApi';
|
||||||
import { LoggingContext } from '../../../common/context/LoggingContext';
|
|
||||||
import useOscSettings from '../../../common/hooks-query/useOscSettings';
|
import useOscSettings from '../../../common/hooks-query/useOscSettings';
|
||||||
import { PlaceholderSettings } from '../../../common/models/OscSettings';
|
import { PlaceholderSettings } from '../../../common/models/OscSettings';
|
||||||
|
import { useEmitLog } from '../../../common/stores/logger';
|
||||||
import { isIPAddress, isOnlyNumbers } from '../../../common/utils/regex';
|
import { isIPAddress, isOnlyNumbers } from '../../../common/utils/regex';
|
||||||
|
|
||||||
import styles from '../Modal.module.scss';
|
import styles from '../Modal.module.scss';
|
||||||
|
|
||||||
export default function OscIntegrationSettings() {
|
export default function OscIntegrationSettings() {
|
||||||
const { data } = useOscSettings();
|
const { data } = useOscSettings();
|
||||||
const { emitError } = useContext(LoggingContext);
|
const { emitError } = useEmitLog();
|
||||||
const {
|
const {
|
||||||
handleSubmit,
|
handleSubmit,
|
||||||
register,
|
register,
|
||||||
|
|||||||
@@ -1,10 +1,11 @@
|
|||||||
import { useCallback, useContext, useEffect, useState } from 'react';
|
import { useCallback, useEffect, useState } from 'react';
|
||||||
import { FormControl, FormLabel, Input, ModalBody } from '@chakra-ui/react';
|
import { FormControl, FormLabel, Input, ModalBody } from '@chakra-ui/react';
|
||||||
import { IoInformationCircleOutline } from '@react-icons/all-files/io5/IoInformationCircleOutline';
|
import { IoInformationCircleOutline } from '@react-icons/all-files/io5/IoInformationCircleOutline';
|
||||||
|
|
||||||
|
import { useEmitLog } from '@/common/stores/logger';
|
||||||
|
|
||||||
import { postOSC } from '../../../common/api/ontimeApi';
|
import { postOSC } from '../../../common/api/ontimeApi';
|
||||||
import EnableBtn from '../../../common/components/buttons/EnableBtn';
|
import EnableBtn from '../../../common/components/buttons/EnableBtn';
|
||||||
import { LoggingContext } from '../../../common/context/LoggingContext';
|
|
||||||
import useOscSettings from '../../../common/hooks-query/useOscSettings';
|
import useOscSettings from '../../../common/hooks-query/useOscSettings';
|
||||||
import { oscPlaceholderSettings } from '../../../common/models/OscSettings';
|
import { oscPlaceholderSettings } from '../../../common/models/OscSettings';
|
||||||
import { inputProps, portInputProps } from '../modalHelper';
|
import { inputProps, portInputProps } from '../modalHelper';
|
||||||
@@ -76,7 +77,7 @@ const oscTriggerEndpoints = [
|
|||||||
|
|
||||||
export default function OscSettingsModal() {
|
export default function OscSettingsModal() {
|
||||||
const { data, status, refetch } = useOscSettings();
|
const { data, status, refetch } = useOscSettings();
|
||||||
const { emitError } = useContext(LoggingContext);
|
const { emitError } = useEmitLog();
|
||||||
const [formData, setFormData] = useState(oscPlaceholderSettings);
|
const [formData, setFormData] = useState(oscPlaceholderSettings);
|
||||||
const [changed, setChanged] = useState(false);
|
const [changed, setChanged] = useState(false);
|
||||||
const [submitting, setSubmitting] = useState(false);
|
const [submitting, setSubmitting] = useState(false);
|
||||||
@@ -122,8 +123,8 @@ export default function OscSettingsModal() {
|
|||||||
} else {
|
} else {
|
||||||
try {
|
try {
|
||||||
await postOSC(formData);
|
await postOSC(formData);
|
||||||
} catch (error){
|
} catch (error) {
|
||||||
emitError(`Error setting OSC: ${error}`)
|
emitError(`Error setting OSC: ${error}`);
|
||||||
} finally {
|
} finally {
|
||||||
await refetch();
|
await refetch();
|
||||||
setChanged(false);
|
setChanged(false);
|
||||||
@@ -131,7 +132,7 @@ export default function OscSettingsModal() {
|
|||||||
}
|
}
|
||||||
setSubmitting(false);
|
setSubmitting(false);
|
||||||
},
|
},
|
||||||
[emitError, formData, refetch]
|
[emitError, formData, refetch],
|
||||||
);
|
);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -154,7 +155,7 @@ export default function OscSettingsModal() {
|
|||||||
setFormData(temp);
|
setFormData(temp);
|
||||||
setChanged(true);
|
setChanged(true);
|
||||||
},
|
},
|
||||||
[formData]
|
[formData],
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -283,12 +284,7 @@ export default function OscSettingsModal() {
|
|||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<SubmitContainer
|
<SubmitContainer revert={revert} submitting={submitting} changed={changed} status={status} />
|
||||||
revert={revert}
|
|
||||||
submitting={submitting}
|
|
||||||
changed={changed}
|
|
||||||
status={status}
|
|
||||||
/>
|
|
||||||
</form>
|
</form>
|
||||||
</ModalBody>
|
</ModalBody>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ interface RundownProps {
|
|||||||
|
|
||||||
export default function Rundown(props: RundownProps) {
|
export default function Rundown(props: RundownProps) {
|
||||||
const { entries } = props;
|
const { entries } = props;
|
||||||
const { data } = useRundownEditor();
|
const data = useRundownEditor();
|
||||||
const { cursor, moveCursorUp, moveCursorDown, moveCursorTo, isCursorLocked } = useContext(CursorContext);
|
const { cursor, moveCursorUp, moveCursorDown, moveCursorTo, isCursorLocked } = useContext(CursorContext);
|
||||||
const startTimeIsLastEnd = useAtomValue(startTimeIsLastEndAtom);
|
const startTimeIsLastEnd = useAtomValue(startTimeIsLastEndAtom);
|
||||||
const defaultPublic = useAtomValue(defaultPublicAtom);
|
const defaultPublic = useAtomValue(defaultPublicAtom);
|
||||||
|
|||||||
@@ -4,8 +4,8 @@ import { OntimeEvent, OntimeRundownEntry, Playback, SupportedEvent } from 'ontim
|
|||||||
|
|
||||||
import { defaultPublicAtom, editorEventId, startTimeIsLastEndAtom } from '../../common/atoms/LocalEventSettings';
|
import { defaultPublicAtom, editorEventId, startTimeIsLastEndAtom } from '../../common/atoms/LocalEventSettings';
|
||||||
import { CursorContext } from '../../common/context/CursorContext';
|
import { CursorContext } from '../../common/context/CursorContext';
|
||||||
import { LoggingContext } from '../../common/context/LoggingContext';
|
|
||||||
import { useEventAction } from '../../common/hooks/useEventAction';
|
import { useEventAction } from '../../common/hooks/useEventAction';
|
||||||
|
import { useEmitLog } from '../../common/stores/logger';
|
||||||
import { cloneEvent } from '../../common/utils/eventsManager';
|
import { cloneEvent } from '../../common/utils/eventsManager';
|
||||||
import { calculateDuration } from '../../common/utils/timesManager';
|
import { calculateDuration } from '../../common/utils/timesManager';
|
||||||
|
|
||||||
@@ -31,7 +31,7 @@ interface RundownEntryProps {
|
|||||||
|
|
||||||
export default function RundownEntry(props: RundownEntryProps) {
|
export default function RundownEntry(props: RundownEntryProps) {
|
||||||
const { index, eventIndex, data, selected, hasCursor, next, delay, previousEnd, previousEventId, playback } = props;
|
const { index, eventIndex, data, selected, hasCursor, next, delay, previousEnd, previousEventId, playback } = props;
|
||||||
const { emitError } = useContext(LoggingContext);
|
const { emitError } = useEmitLog();
|
||||||
const startTimeIsLastEnd = useAtomValue(startTimeIsLastEndAtom);
|
const startTimeIsLastEnd = useAtomValue(startTimeIsLastEndAtom);
|
||||||
const defaultPublic = useAtomValue(defaultPublicAtom);
|
const defaultPublic = useAtomValue(defaultPublicAtom);
|
||||||
const { addEvent, updateEvent, deleteEvent } = useEventAction();
|
const { addEvent, updateEvent, deleteEvent } = useEventAction();
|
||||||
|
|||||||
@@ -111,7 +111,7 @@ export default function EventBlock(props: EventBlockProps) {
|
|||||||
[title, updateEvent, eventId],
|
[title, updateEvent, eventId],
|
||||||
);
|
);
|
||||||
|
|
||||||
const eventIsPlaying = selected && playback === 'play';
|
const eventIsPlaying = selected && playback === Playback.Play;
|
||||||
const playBtnStyles = { _hover: {} };
|
const playBtnStyles = { _hover: {} };
|
||||||
if (!skip && eventIsPlaying) {
|
if (!skip && eventIsPlaying) {
|
||||||
playBtnStyles._hover = { bg: '#c05621' };
|
playBtnStyles._hover = { bg: '#c05621' };
|
||||||
|
|||||||
@@ -1,20 +1,21 @@
|
|||||||
import { useCallback, useContext } from 'react';
|
import { useCallback } from 'react';
|
||||||
|
import { millisToString } from 'ontime-utils';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
|
|
||||||
|
import { useEmitLog } from '@/common/stores/logger';
|
||||||
|
|
||||||
import TimeInput from '../../../../common/components/input/time-input/TimeInput';
|
import TimeInput from '../../../../common/components/input/time-input/TimeInput';
|
||||||
import { LoggingContext } from '../../../../common/context/LoggingContext';
|
|
||||||
import { millisToMinutes } from '../../../../common/utils/dateConfig';
|
import { millisToMinutes } from '../../../../common/utils/dateConfig';
|
||||||
import { stringFromMillis } from '../../../../common/utils/time';
|
|
||||||
import { validateEntry } from '../../../../common/utils/timesManager';
|
import { validateEntry } from '../../../../common/utils/timesManager';
|
||||||
|
|
||||||
import style from '../EventBlock.module.scss';
|
import style from '../EventBlock.module.scss';
|
||||||
|
|
||||||
export default function EventBlockTimers(props) {
|
export default function EventBlockTimers(props) {
|
||||||
const { timeStart, timeEnd, duration, delay, actionHandler, previousEnd } = props;
|
const { timeStart, timeEnd, duration, delay, actionHandler, previousEnd } = props;
|
||||||
const { emitWarning } = useContext(LoggingContext);
|
const { emitWarning } = useEmitLog();
|
||||||
|
|
||||||
const delayTime = `${delay >= 0 ? '+' : '-'} ${millisToMinutes(Math.abs(delay))}`;
|
const delayTime = `${delay >= 0 ? '+' : '-'} ${millisToMinutes(Math.abs(delay))}`;
|
||||||
const newTime = stringFromMillis(timeStart + delay);
|
const newTime = millisToString(timeStart + delay);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @description Validates a time input against its pair
|
* @description Validates a time input against its pair
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
import { useCallback, useContext, useRef } from 'react';
|
import { useCallback, useRef } from 'react';
|
||||||
import { Button, Checkbox, Tooltip } from '@chakra-ui/react';
|
import { Button, Checkbox, Tooltip } from '@chakra-ui/react';
|
||||||
import { useAtomValue } from 'jotai';
|
import { useAtomValue } from 'jotai';
|
||||||
import { SupportedEvent } from 'ontime-types';
|
import { SupportedEvent } from 'ontime-types';
|
||||||
|
|
||||||
import { defaultPublicAtom, startTimeIsLastEndAtom } from '../../../common/atoms/LocalEventSettings';
|
import { defaultPublicAtom, startTimeIsLastEndAtom } from '../../../common/atoms/LocalEventSettings';
|
||||||
import { LoggingContext } from '../../../common/context/LoggingContext';
|
|
||||||
import { useEventAction } from '../../../common/hooks/useEventAction';
|
import { useEventAction } from '../../../common/hooks/useEventAction';
|
||||||
|
import { useEmitLog } from '../../../common/stores/logger';
|
||||||
import { tooltipDelayMid } from '../../../ontimeConfig';
|
import { tooltipDelayMid } from '../../../ontimeConfig';
|
||||||
|
|
||||||
import style from './QuickAddBlock.module.scss';
|
import style from './QuickAddBlock.module.scss';
|
||||||
@@ -21,7 +21,7 @@ interface QuickAddBlockProps {
|
|||||||
export default function QuickAddBlock(props: QuickAddBlockProps) {
|
export default function QuickAddBlock(props: QuickAddBlockProps) {
|
||||||
const { showKbd, eventId, previousEventId, disableAddDelay = true, disableAddBlock } = props;
|
const { showKbd, eventId, previousEventId, disableAddDelay = true, disableAddBlock } = props;
|
||||||
const { addEvent } = useEventAction();
|
const { addEvent } = useEventAction();
|
||||||
const { emitError } = useContext(LoggingContext);
|
const { emitError } = useEmitLog();
|
||||||
const startTimeIsLastEnd = useAtomValue(startTimeIsLastEndAtom);
|
const startTimeIsLastEnd = useAtomValue(startTimeIsLastEndAtom);
|
||||||
const defaultPublic = useAtomValue(defaultPublicAtom);
|
const defaultPublic = useAtomValue(defaultPublicAtom);
|
||||||
const doStartTime = useRef<HTMLInputElement | null>(null);
|
const doStartTime = useRef<HTMLInputElement | null>(null);
|
||||||
|
|||||||
@@ -1,10 +1,9 @@
|
|||||||
import { FiCheck } from '@react-icons/all-files/fi/FiCheck';
|
import { FiCheck } from '@react-icons/all-files/fi/FiCheck';
|
||||||
|
|
||||||
import { stringFromMillis } from '../../common/utils/time.js';
|
|
||||||
|
|
||||||
import EditableCell from './tableElements/EditableCell';
|
import EditableCell from './tableElements/EditableCell';
|
||||||
|
|
||||||
import style from './Table.module.scss';
|
import style from './Table.module.scss';
|
||||||
|
import { millisToString } from 'ontime-utils';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* React - Table column object
|
* React - Table column object
|
||||||
@@ -22,19 +21,19 @@ export const makeColumns = (sizes, userFields) => {
|
|||||||
{
|
{
|
||||||
Header: 'Start',
|
Header: 'Start',
|
||||||
accessor: 'timeStart',
|
accessor: 'timeStart',
|
||||||
Cell: ({ cell: { value, delayed } }) => stringFromMillis(delayed || value),
|
Cell: ({ cell: { value, delayed } }) => millisToString(delayed || value),
|
||||||
width: sizes?.timeStart || 90,
|
width: sizes?.timeStart || 90,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
Header: 'End',
|
Header: 'End',
|
||||||
accessor: 'timeEnd',
|
accessor: 'timeEnd',
|
||||||
Cell: ({ cell: { value, delayed } }) => stringFromMillis(delayed || value),
|
Cell: ({ cell: { value, delayed } }) => millisToString(delayed || value),
|
||||||
width: sizes?.timeEnd || 90,
|
width: sizes?.timeEnd || 90,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
Header: 'Duration',
|
Header: 'Duration',
|
||||||
accessor: 'duration',
|
accessor: 'duration',
|
||||||
Cell: ({ cell: { value } }) => stringFromMillis(value),
|
Cell: ({ cell: { value } }) => millisToString(value),
|
||||||
width: sizes?.duration || 90,
|
width: sizes?.duration || 90,
|
||||||
},
|
},
|
||||||
{ Header: 'Title', accessor: 'title', width: sizes?.title || 400 },
|
{ Header: 'Title', accessor: 'title', width: sizes?.title || 400 },
|
||||||
|
|||||||
+10
-10
@@ -3,14 +3,18 @@ import { IoPause } from '@react-icons/all-files/io5/IoPause';
|
|||||||
import { IoPlay } from '@react-icons/all-files/io5/IoPlay';
|
import { IoPlay } from '@react-icons/all-files/io5/IoPlay';
|
||||||
import { IoStop } from '@react-icons/all-files/io5/IoStop';
|
import { IoStop } from '@react-icons/all-files/io5/IoStop';
|
||||||
import { IoTimeOutline } from '@react-icons/all-files/io5/IoTimeOutline';
|
import { IoTimeOutline } from '@react-icons/all-files/io5/IoTimeOutline';
|
||||||
import PropTypes from 'prop-types';
|
import { Playback } from 'ontime-types';
|
||||||
|
|
||||||
import { tooltipDelayFast } from '../../../ontimeConfig';
|
import { tooltipDelayFast } from '../../../ontimeConfig';
|
||||||
|
|
||||||
export default function PlaybackIcon(props) {
|
interface PlaybackIconProps {
|
||||||
|
state: Playback;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function PlaybackIcon(props: PlaybackIconProps) {
|
||||||
const { state } = props;
|
const { state } = props;
|
||||||
|
|
||||||
if (state === 'stop') {
|
if (state === Playback.Stop) {
|
||||||
return (
|
return (
|
||||||
<Tooltip openDelay={tooltipDelayFast} label='Timer Stopped' shouldWrapChildren>
|
<Tooltip openDelay={tooltipDelayFast} label='Timer Stopped' shouldWrapChildren>
|
||||||
<IoStop />
|
<IoStop />
|
||||||
@@ -18,7 +22,7 @@ export default function PlaybackIcon(props) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (state === 'start') {
|
if (state === Playback.Play) {
|
||||||
return (
|
return (
|
||||||
<Tooltip openDelay={tooltipDelayFast} label='Timer Playing' shouldWrapChildren>
|
<Tooltip openDelay={tooltipDelayFast} label='Timer Playing' shouldWrapChildren>
|
||||||
<IoPlay />
|
<IoPlay />
|
||||||
@@ -26,7 +30,7 @@ export default function PlaybackIcon(props) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (state === 'pause') {
|
if (state === Playback.Pause) {
|
||||||
return (
|
return (
|
||||||
<Tooltip openDelay={tooltipDelayFast} label='Timer Paused' shouldWrapChildren>
|
<Tooltip openDelay={tooltipDelayFast} label='Timer Paused' shouldWrapChildren>
|
||||||
<IoPause />
|
<IoPause />
|
||||||
@@ -34,7 +38,7 @@ export default function PlaybackIcon(props) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (state === 'roll') {
|
if (state === Playback.Roll) {
|
||||||
return (
|
return (
|
||||||
<Tooltip openDelay={tooltipDelayFast} label='Timer Rolling' shouldWrapChildren>
|
<Tooltip openDelay={tooltipDelayFast} label='Timer Rolling' shouldWrapChildren>
|
||||||
<IoTimeOutline />
|
<IoTimeOutline />
|
||||||
@@ -44,7 +48,3 @@ export default function PlaybackIcon(props) {
|
|||||||
|
|
||||||
return '';
|
return '';
|
||||||
}
|
}
|
||||||
|
|
||||||
PlaybackIcon.propTypes = {
|
|
||||||
state: PropTypes.string,
|
|
||||||
};
|
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
import { stringify } from 'csv-stringify/browser/esm/sync';
|
import { stringify } from 'csv-stringify/browser/esm/sync';
|
||||||
|
import { millisToString } from 'ontime-utils';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @description parses a field for export
|
* @description parses a field for export
|
||||||
@@ -6,14 +7,13 @@ import { stringify } from 'csv-stringify/browser/esm/sync';
|
|||||||
* @param {*} data
|
* @param {*} data
|
||||||
* @return {string}
|
* @return {string}
|
||||||
*/
|
*/
|
||||||
import { stringFromMillis } from '../../common/utils/time';
|
|
||||||
|
|
||||||
export const parseField = (field, data) => {
|
export const parseField = (field, data) => {
|
||||||
let val;
|
let val;
|
||||||
switch (field) {
|
switch (field) {
|
||||||
case 'timeStart':
|
case 'timeStart':
|
||||||
case 'timeEnd':
|
case 'timeEnd':
|
||||||
val = stringFromMillis(data);
|
val = millisToString(data);
|
||||||
break;
|
break;
|
||||||
case 'isPublic':
|
case 'isPublic':
|
||||||
val = data ? 'x' : '';
|
val = data ? 'x' : '';
|
||||||
|
|||||||
+23
-61
@@ -1,68 +1,33 @@
|
|||||||
/* eslint-disable react/display-name */
|
import { ReactNode, useMemo } from 'react';
|
||||||
import { useEffect, useMemo, useState } from 'react';
|
import { Playback } from 'ontime-types';
|
||||||
|
|
||||||
import { useMessageControl } from '../../common/hooks/useSocket';
|
|
||||||
import useSubscription from '../../common/hooks/useSubscription';
|
|
||||||
import useEventData from '../../common/hooks-query/useEventData';
|
import useEventData from '../../common/hooks-query/useEventData';
|
||||||
import useRundown from '../../common/hooks-query/useRundown';
|
import useRundown from '../../common/hooks-query/useRundown';
|
||||||
import useViewSettings from '../../common/hooks-query/useViewSettings';
|
import useViewSettings from '../../common/hooks-query/useViewSettings';
|
||||||
import socket from '../../common/utils/socket';
|
import { useRuntimeStore } from '../../common/stores/runtime';
|
||||||
|
|
||||||
const withSocket = (Component) => {
|
const withData = (Component: ReactNode) => {
|
||||||
return (props) => {
|
return (props) => {
|
||||||
|
|
||||||
|
// HTTP API data
|
||||||
const { data: eventsData } = useRundown();
|
const { data: eventsData } = useRundown();
|
||||||
const { data: genData } = useEventData();
|
const { data: genData } = useEventData();
|
||||||
const { data: viewSettings } = useViewSettings();
|
const { data: viewSettings } = useViewSettings();
|
||||||
const { data: messageControl } = useMessageControl();
|
|
||||||
|
|
||||||
const [publicSelectedId, setPublicSelectedId] = useState(null);
|
|
||||||
|
|
||||||
const [timer] = useSubscription('timer', {
|
|
||||||
clock: null,
|
|
||||||
current: null,
|
|
||||||
elapsed: null ,
|
|
||||||
expectedFinish: null,
|
|
||||||
addedTime: 0,
|
|
||||||
startedAt: null,
|
|
||||||
finishedAt: null,
|
|
||||||
secondaryTimer: null,
|
|
||||||
});
|
|
||||||
const [titles] = useSubscription('titles', {
|
|
||||||
titleNow: '',
|
|
||||||
subtitleNow: '',
|
|
||||||
presenterNow: '',
|
|
||||||
titleNext: '',
|
|
||||||
subtitleNext: '',
|
|
||||||
presenterNext: '',
|
|
||||||
});
|
|
||||||
const [publicTitles] = useSubscription('titlesPublic', {
|
|
||||||
titleNow: '',
|
|
||||||
subtitleNow: '',
|
|
||||||
presenterNow: '',
|
|
||||||
titleNext: '',
|
|
||||||
subtitleNext: '',
|
|
||||||
presenterNext: '',
|
|
||||||
});
|
|
||||||
const [selectedId] = useSubscription('selected-id', null);
|
|
||||||
const [nextId] = useSubscription('next-id', null);
|
|
||||||
const [playback] = useSubscription('playback', null);
|
|
||||||
|
|
||||||
// Ask for update on load
|
|
||||||
useEffect(() => {
|
|
||||||
// todo: remove
|
|
||||||
socket.on('publicselected-id', (data) => {
|
|
||||||
setPublicSelectedId(data);
|
|
||||||
});
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
|
|
||||||
const publicEvents = useMemo(() => {
|
const publicEvents = useMemo(() => {
|
||||||
if (Array.isArray(eventsData)) {
|
if (Array.isArray(eventsData)) {
|
||||||
return eventsData.filter((d) => d.type === 'event' && d.title !== '' && d.isPublic);
|
return eventsData.filter((e) => e.type === 'event' && e.title && e.isPublic);
|
||||||
}
|
}
|
||||||
return [];
|
return [];
|
||||||
}, [eventsData]);
|
}, [eventsData]);
|
||||||
|
|
||||||
|
// websocket data
|
||||||
|
const data = useRuntimeStore();
|
||||||
|
const { timer, titles, titlesPublic, publicMessage, timerMessage, lowerMessage, playback, onAir } = data;
|
||||||
|
const publicSelectedId = data.loaded.selectedPublicEventId;
|
||||||
|
const selectedId = data.loaded.selectedEventId;
|
||||||
|
const nextId = data.loaded.nextEventId;
|
||||||
|
|
||||||
/********************************************/
|
/********************************************/
|
||||||
/*** + titleManager ***/
|
/*** + titleManager ***/
|
||||||
/*** WRAP INFORMATION RELATED TO TITLES ***/
|
/*** WRAP INFORMATION RELATED TO TITLES ***/
|
||||||
@@ -85,16 +50,14 @@ const withSocket = (Component) => {
|
|||||||
/********************************************/
|
/********************************************/
|
||||||
// is there a now field?
|
// is there a now field?
|
||||||
let showPublicNow = true;
|
let showPublicNow = true;
|
||||||
if (!publicTitles.titleNow && !publicTitles.subtitleNow && !publicTitles.presenterNow)
|
if (!titlesPublic.titleNow && !titlesPublic.subtitleNow && !titlesPublic.presenterNow) showPublicNow = false;
|
||||||
showPublicNow = false;
|
|
||||||
|
|
||||||
// is there a next field?
|
// is there a next field?
|
||||||
let showPublicNext = true;
|
let showPublicNext = true;
|
||||||
if (!publicTitles.titleNext && !publicTitles.subtitleNext && !publicTitles.presenterNext)
|
if (!titlesPublic.titleNext && !titlesPublic.subtitleNext && !titlesPublic.presenterNext) showPublicNext = false;
|
||||||
showPublicNext = false;
|
|
||||||
|
|
||||||
const publicTitleManager = {
|
const publicTitleManager = {
|
||||||
...publicTitles,
|
...titlesPublic,
|
||||||
showNow: showPublicNow,
|
showNow: showPublicNow,
|
||||||
showNext: showPublicNext,
|
showNext: showPublicNext,
|
||||||
};
|
};
|
||||||
@@ -110,7 +73,7 @@ const withSocket = (Component) => {
|
|||||||
// get clock string
|
// get clock string
|
||||||
const TimeManagerType = {
|
const TimeManagerType = {
|
||||||
...timer,
|
...timer,
|
||||||
finished: playback === 'play' && timer.current < 0 && timer.startedAt,
|
finished: playback === Playback.Play && (timer.current ?? 0) < 0 && timer.startedAt,
|
||||||
playback,
|
playback,
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -119,13 +82,12 @@ const withSocket = (Component) => {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
Component.displayName = 'ComponentWithData';
|
|
||||||
return (
|
return (
|
||||||
<Component
|
<Component
|
||||||
{...props}
|
{...props}
|
||||||
pres={messageControl.messages.presenter}
|
pres={timerMessage}
|
||||||
publ={messageControl.messages.public}
|
publ={publicMessage}
|
||||||
lower={messageControl.messages.lower}
|
lower={lowerMessage}
|
||||||
title={titleManager}
|
title={titleManager}
|
||||||
publicTitle={publicTitleManager}
|
publicTitle={publicTitleManager}
|
||||||
time={TimeManagerType}
|
time={TimeManagerType}
|
||||||
@@ -136,10 +98,10 @@ const withSocket = (Component) => {
|
|||||||
viewSettings={viewSettings}
|
viewSettings={viewSettings}
|
||||||
nextId={nextId}
|
nextId={nextId}
|
||||||
general={genData}
|
general={genData}
|
||||||
onAir={messageControl.onAir}
|
onAir={onAir}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
export default withSocket;
|
export default withData;
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
import { useSearchParams } from 'react-router-dom';
|
import { useSearchParams } from 'react-router-dom';
|
||||||
import { useAtom } from 'jotai';
|
import { useAtom } from 'jotai';
|
||||||
import { OntimeEvent, OntimeRundownEntry, SupportedEvent } from 'ontime-types';
|
import { OntimeEvent, OntimeRundownEntry, Playback, SupportedEvent } from 'ontime-types';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
|
|
||||||
import { overrideStylesURL } from '../../../common/api/apiConstants';
|
import { overrideStylesURL } from '../../../common/api/apiConstants';
|
||||||
@@ -91,7 +91,7 @@ export default function Countdown(props) {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const standby = time.playback !== 'play' && selectedId === follow?.id;
|
const standby = time.playback !== Playback.Play && selectedId === follow?.id;
|
||||||
const isRunningFinished = time.finished && runningMessage === TimerMessage.running;
|
const isRunningFinished = time.finished && runningMessage === TimerMessage.running;
|
||||||
const isSelected = runningMessage === TimerMessage.running;
|
const isSelected = runningMessage === TimerMessage.running;
|
||||||
const delayedTimerStyles = delay > 0 ? 'aux-timers__value--delayed' : '';
|
const delayedTimerStyles = delay > 0 ? 'aux-timers__value--delayed' : '';
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { OntimeEvent } from 'ontime-types';
|
import { OntimeEvent, Playback } from 'ontime-types';
|
||||||
|
|
||||||
import { TimeManagerType } from '../../../common/models/TimeManager.type';
|
import { TimeManagerType } from '../../../common/models/TimeManager.type';
|
||||||
|
|
||||||
@@ -25,7 +25,7 @@ export const fetchTimerData = (time: TimeManagerType, follow: OntimeEvent, selec
|
|||||||
|
|
||||||
if (selectedId === follow.id) {
|
if (selectedId === follow.id) {
|
||||||
// check that is not running
|
// check that is not running
|
||||||
message = time.playback === 'pause' ? TimerMessage.waiting : TimerMessage.running;
|
message = time.playback === Playback.Pause ? TimerMessage.waiting : TimerMessage.running;
|
||||||
timer = time.current ?? 0;
|
timer = time.current ?? 0;
|
||||||
|
|
||||||
} else if (time.clock < follow.timeStart) {
|
} else if (time.clock < follow.timeStart) {
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { useEffect } from 'react';
|
import { useEffect } from 'react';
|
||||||
import { useSearchParams } from 'react-router-dom';
|
import { useSearchParams } from 'react-router-dom';
|
||||||
import { useAtom } from 'jotai';
|
import { useAtom } from 'jotai';
|
||||||
import { EventData, Message, TimerType, ViewSettings } from 'ontime-types';
|
import { EventData, Message, Playback, TimerType, ViewSettings } from 'ontime-types';
|
||||||
|
|
||||||
import { overrideStylesURL } from '../../../common/api/apiConstants';
|
import { overrideStylesURL } from '../../../common/api/apiConstants';
|
||||||
import { mirrorViewersAtom } from '../../../common/atoms/ViewerSettings';
|
import { mirrorViewersAtom } from '../../../common/atoms/ViewerSettings';
|
||||||
@@ -127,7 +127,7 @@ export default function MinimalTimer(props: MinimalTimerProps) {
|
|||||||
userOptions.hideEndMessage = Boolean(hideEndMessage);
|
userOptions.hideEndMessage = Boolean(hideEndMessage);
|
||||||
|
|
||||||
const showOverlay = pres.text !== '' && pres.visible;
|
const showOverlay = pres.text !== '' && pres.visible;
|
||||||
const isPlaying = time.playback !== 'pause';
|
const isPlaying = time.playback !== Playback.Pause;
|
||||||
const isNegative =
|
const isNegative =
|
||||||
(time.current ?? 0) < 0 && time.timerType !== TimerType.Clock && time.timerType !== TimerType.CountUp;
|
(time.current ?? 0) < 0 && time.timerType !== TimerType.Clock && time.timerType !== TimerType.CountUp;
|
||||||
const showEndMessage = time.current < 0 && general.endMessage && !hideEndMessage;
|
const showEndMessage = time.current < 0 && general.endMessage && !hideEndMessage;
|
||||||
|
|||||||
@@ -10,9 +10,10 @@ import useFitText from '../../../common/hooks/useFitText';
|
|||||||
import { useRuntimeStylesheet } from '../../../common/hooks/useRuntimeStylesheet';
|
import { useRuntimeStylesheet } from '../../../common/hooks/useRuntimeStylesheet';
|
||||||
import { formatDisplay } from '../../../common/utils/dateConfig';
|
import { formatDisplay } from '../../../common/utils/dateConfig';
|
||||||
import { formatEventList, getEventsWithDelay, trimEventlist } from '../../../common/utils/eventsManager';
|
import { formatEventList, getEventsWithDelay, trimEventlist } from '../../../common/utils/eventsManager';
|
||||||
import { formatTime, stringFromMillis } from '../../../common/utils/time';
|
import { formatTime } from '../../../common/utils/time';
|
||||||
|
|
||||||
import './StudioClock.scss';
|
import './StudioClock.scss';
|
||||||
|
import { millisToString } from 'ontime-utils';
|
||||||
|
|
||||||
const formatOptions = {
|
const formatOptions = {
|
||||||
showSeconds: false,
|
showSeconds: false,
|
||||||
@@ -68,7 +69,7 @@ export default function StudioClock(props) {
|
|||||||
}, [backstageEvents, nextId, selectedId]);
|
}, [backstageEvents, nextId, selectedId]);
|
||||||
|
|
||||||
const clock = formatTime(time.clock, formatOptions);
|
const clock = formatTime(time.clock, formatOptions);
|
||||||
const [, , secondsNow] = stringFromMillis(time.clock).split(':');
|
const [, , secondsNow] = millisToString(time.clock).split(':');
|
||||||
const isNegative = (time.current ?? 0) < 0;
|
const isNegative = (time.current ?? 0) < 0;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { useEffect } from 'react';
|
import { useEffect } from 'react';
|
||||||
import { AnimatePresence, motion } from 'framer-motion';
|
import { AnimatePresence, motion } from 'framer-motion';
|
||||||
import { useAtom } from 'jotai';
|
import { useAtom } from 'jotai';
|
||||||
import { TimerType } from 'ontime-types';
|
import { Playback, TimerType } from 'ontime-types';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
|
|
||||||
import { overrideStylesURL } from '../../../common/api/apiConstants';
|
import { overrideStylesURL } from '../../../common/api/apiConstants';
|
||||||
@@ -61,12 +61,12 @@ export default function Timer(props) {
|
|||||||
|
|
||||||
const clock = formatTime(time.clock, formatOptions);
|
const clock = formatTime(time.clock, formatOptions);
|
||||||
const showOverlay = pres.text !== '' && pres.visible;
|
const showOverlay = pres.text !== '' && pres.visible;
|
||||||
const isPlaying = time.playback !== 'pause';
|
const isPlaying = time.playback !== Playback.Pause;
|
||||||
const isNegative =
|
const isNegative =
|
||||||
(time.current ?? 0) < 0 && time.timerType !== TimerType.Clock && time.timerType !== TimerType.CountUp;
|
(time.current ?? 0) < 0 && time.timerType !== TimerType.Clock && time.timerType !== TimerType.CountUp;
|
||||||
|
|
||||||
const showEndMessage = time.current < 0 && general.endMessage;
|
const showEndMessage = time.current < 0 && general.endMessage;
|
||||||
const showProgress = time.playback !== 'stop';
|
const showProgress = time.playback !== Playback.Stop;
|
||||||
const showFinished = time.finished && (time.timerType !== TimerType.Clock || showEndMessage);
|
const showFinished = time.finished && (time.timerType !== TimerType.Clock || showEndMessage);
|
||||||
const showClock = time.timerType !== TimerType.Clock;
|
const showClock = time.timerType !== TimerType.Clock;
|
||||||
const baseClasses = `stage-timer ${isMirrored ? 'mirror' : ''}`;
|
const baseClasses = `stage-timer ${isMirrored ? 'mirror' : ''}`;
|
||||||
|
|||||||
@@ -21,12 +21,13 @@
|
|||||||
"ontime-utils": "workspace:*",
|
"ontime-utils": "workspace:*",
|
||||||
"passport": "^0.6.0",
|
"passport": "^0.6.0",
|
||||||
"passport-local": "~1.0.0",
|
"passport-local": "~1.0.0",
|
||||||
"socket.io": "^4.5.4"
|
"ws": "^8.12.1"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/express": "^4.17.17",
|
"@types/express": "^4.17.17",
|
||||||
"@types/node": "^16.11.7",
|
"@types/node": "^16.11.7",
|
||||||
"@types/node-osc": "^6.0.0",
|
"@types/node-osc": "^6.0.0",
|
||||||
|
"@types/websocket": "^1.0.5",
|
||||||
"@typescript-eslint/eslint-plugin": "^5.48.1",
|
"@typescript-eslint/eslint-plugin": "^5.48.1",
|
||||||
"@typescript-eslint/parser": "^5.48.1",
|
"@typescript-eslint/parser": "^5.48.1",
|
||||||
"esbuild": "^0.17.5",
|
"esbuild": "^0.17.5",
|
||||||
|
|||||||
@@ -0,0 +1,3 @@
|
|||||||
|
export interface IAdapter {
|
||||||
|
shutdown: () => void;
|
||||||
|
}
|
||||||
@@ -0,0 +1,54 @@
|
|||||||
|
import { Server } from 'node-osc';
|
||||||
|
import { OSCSettings } from 'ontime-types';
|
||||||
|
|
||||||
|
import { IAdapter } from './IAdapter.js';
|
||||||
|
import { dispatchFromAdapter } from '../controllers/integrationController.js';
|
||||||
|
import { logger } from '../classes/Logger.js';
|
||||||
|
|
||||||
|
export class OscServer implements IAdapter {
|
||||||
|
private osc: Server;
|
||||||
|
|
||||||
|
constructor(config: OSCSettings) {
|
||||||
|
this.osc = new Server(config.portIn, '0.0.0.0');
|
||||||
|
|
||||||
|
this.osc.on('error', console.error);
|
||||||
|
|
||||||
|
this.osc.on('message', (msg) => {
|
||||||
|
// message should look like /ontime/{path} {args} where
|
||||||
|
// ontime: fixed message for app
|
||||||
|
// path: command to be called
|
||||||
|
// args: extra data, only used on some API entries (delay, goto)
|
||||||
|
|
||||||
|
// split message
|
||||||
|
const [, address, path] = msg[0].split('/');
|
||||||
|
const args = msg[1];
|
||||||
|
|
||||||
|
// get first part before (ontime)
|
||||||
|
if (address !== 'ontime') {
|
||||||
|
logger.error('RX', `OSC IN: OSC messages to ontime must start with /ontime/, received: ${msg}`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// get second part (command)
|
||||||
|
if (!path) {
|
||||||
|
logger.error('RX', 'OSC IN: No path found');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const reply = dispatchFromAdapter(path, args, 'osc');
|
||||||
|
if (reply) {
|
||||||
|
const { topic, payload } = reply;
|
||||||
|
this.osc.emit(topic, payload);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
logger.error('RX', `OSC IN: ${error}`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
shutdown() {
|
||||||
|
console.log('Shutting down OSC Server');
|
||||||
|
this.osc?.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,124 @@
|
|||||||
|
/**
|
||||||
|
* DESIGN BY CONTRACT
|
||||||
|
* ===================
|
||||||
|
* All websocket calls are expected to follow the defined format,
|
||||||
|
* otherwise they will be ignored by Ontime server
|
||||||
|
*
|
||||||
|
* Messages should be in JSON format with two top level objects
|
||||||
|
* {
|
||||||
|
* type: ...
|
||||||
|
* payload: ...
|
||||||
|
* }
|
||||||
|
*
|
||||||
|
* Type: describes the action to be performed as enumerated in the API design
|
||||||
|
* Payload: adds necessary payload for the request to be completed
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { WebSocket, WebSocketServer } from 'ws';
|
||||||
|
|
||||||
|
import getRandomName from '../utils/getRandomName.js';
|
||||||
|
import { IAdapter } from './IAdapter.js';
|
||||||
|
import { eventStore } from '../stores/EventStore.js';
|
||||||
|
import { dispatchFromAdapter } from '../controllers/integrationController.js';
|
||||||
|
import { logger } from '../classes/Logger.js';
|
||||||
|
|
||||||
|
let instance;
|
||||||
|
|
||||||
|
export class SocketServer implements IAdapter {
|
||||||
|
private readonly MAX_PAYLOAD = 1024 * 256; // 256Kb
|
||||||
|
|
||||||
|
private wss: WebSocketServer | null;
|
||||||
|
private clientIds: Set<string>;
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
if (instance) {
|
||||||
|
throw new Error('There can be only one');
|
||||||
|
}
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-this-alias -- this logic is used to ensure singleton
|
||||||
|
instance = this;
|
||||||
|
this.clientIds = new Set<string>();
|
||||||
|
this.wss = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
init(server) {
|
||||||
|
this.wss = new WebSocketServer({ path: '/ws', server });
|
||||||
|
|
||||||
|
this.wss.on('connection', (ws) => {
|
||||||
|
const clientId = getRandomName();
|
||||||
|
this.clientIds.add(clientId);
|
||||||
|
logger.info('RX', `${this.wss.clients.size} Connections with new: ${clientId}`);
|
||||||
|
|
||||||
|
// send store payload on connect
|
||||||
|
ws.send(
|
||||||
|
JSON.stringify({
|
||||||
|
type: 'ontime',
|
||||||
|
payload: eventStore.poll(),
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
ws.on('error', console.error);
|
||||||
|
|
||||||
|
ws.on('close', () => {
|
||||||
|
logger.info('RX', `${this.wss.clients.size} Connections with disconnected: ${clientId}`);
|
||||||
|
this.clientIds.delete(clientId);
|
||||||
|
});
|
||||||
|
|
||||||
|
ws.on('message', (data) => {
|
||||||
|
if (data.length > this.MAX_PAYLOAD) {
|
||||||
|
ws.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: protocol specific stuff should be handled here
|
||||||
|
// eg: rename-client
|
||||||
|
// socket.on('rename-client', (newName) => {
|
||||||
|
// if (newName) {
|
||||||
|
// const previousName = this._clientNames[socket.id];
|
||||||
|
// this._clientNames[socket.id] = newName;
|
||||||
|
// this.info('CLIENT', `Client ${previousName} renamed to ${newName}`);
|
||||||
|
// }
|
||||||
|
// });
|
||||||
|
|
||||||
|
try {
|
||||||
|
const message = JSON.parse(data);
|
||||||
|
const { type, payload } = message;
|
||||||
|
|
||||||
|
if (type === 'hello') {
|
||||||
|
ws.send('hi');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (type === 'ontime-log') {
|
||||||
|
console.log('attempted adding to log');
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const reply = dispatchFromAdapter(type, payload, 'ws');
|
||||||
|
if (reply) {
|
||||||
|
const { topic, payload } = reply;
|
||||||
|
ws.send(topic, payload);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
logger.error('RX', `WS IN: ${error}`);
|
||||||
|
}
|
||||||
|
} catch (_) {
|
||||||
|
// we ignore unknown
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// message is any serializable value
|
||||||
|
send(message: any) {
|
||||||
|
this.wss?.clients.forEach((client) => {
|
||||||
|
if (client !== this.wss && client.readyState === WebSocket.OPEN) {
|
||||||
|
client.send(JSON.stringify(message));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
shutdown() {
|
||||||
|
this.wss?.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const socket = new SocketServer();
|
||||||
+33
-29
@@ -6,8 +6,7 @@ import cors from 'cors';
|
|||||||
// import utils
|
// import utils
|
||||||
import { join, resolve } from 'path';
|
import { join, resolve } from 'path';
|
||||||
|
|
||||||
import { initiateOSC, shutdownOSCServer } from './controllers/OscController.js';
|
import { initSentry, reportSentryException } from './modules/sentry.js';
|
||||||
import { initSentry } from './modules/sentry.js';
|
|
||||||
import { currentDirectory, environment, isProduction, resolvedPath } from './setup.js';
|
import { currentDirectory, environment, isProduction, resolvedPath } from './setup.js';
|
||||||
import { ONTIME_VERSION } from './ONTIME_VERSION.js';
|
import { ONTIME_VERSION } from './ONTIME_VERSION.js';
|
||||||
import { OSCSettings } from 'ontime-types';
|
import { OSCSettings } from 'ontime-types';
|
||||||
@@ -18,13 +17,18 @@ import { router as eventDataRouter } from './routes/eventDataRouter.js';
|
|||||||
import { router as ontimeRouter } from './routes/ontimeRouter.js';
|
import { router as ontimeRouter } from './routes/ontimeRouter.js';
|
||||||
import { router as playbackRouter } from './routes/playbackRouter.js';
|
import { router as playbackRouter } from './routes/playbackRouter.js';
|
||||||
|
|
||||||
// Services
|
// Import adapters
|
||||||
|
import { OscServer } from './adapters/OscAdapter.js';
|
||||||
|
import { socket } from './adapters/WebsocketAdapter.js';
|
||||||
import { DataProvider } from './classes/data-provider/DataProvider.js';
|
import { DataProvider } from './classes/data-provider/DataProvider.js';
|
||||||
import { socketProvider } from './classes/socket/SocketController.js';
|
|
||||||
import { eventTimer } from './services/TimerService.js';
|
|
||||||
import { dbLoadingProcess } from './modules/loadDb.js';
|
import { dbLoadingProcess } from './modules/loadDb.js';
|
||||||
|
|
||||||
|
// Services
|
||||||
|
import { eventTimer } from './services/TimerService.js';
|
||||||
import { integrationService } from './services/integration-service/IntegrationService.js';
|
import { integrationService } from './services/integration-service/IntegrationService.js';
|
||||||
import { OscIntegration } from './services/integration-service/OscIntegration.js';
|
import { OscIntegration } from './services/integration-service/OscIntegration.js';
|
||||||
|
import { logger } from './classes/Logger.js';
|
||||||
|
import { eventLoader } from './classes/event-loader/EventLoader.js';
|
||||||
|
|
||||||
console.log(`Starting Ontime version ${ONTIME_VERSION}`);
|
console.log(`Starting Ontime version ${ONTIME_VERSION}`);
|
||||||
|
|
||||||
@@ -33,10 +37,7 @@ if (!isProduction) {
|
|||||||
console.log(`Ontime directory at ${currentDirectory} `);
|
console.log(`Ontime directory at ${currentDirectory} `);
|
||||||
}
|
}
|
||||||
|
|
||||||
initSentry(environment);
|
initSentry(isProduction);
|
||||||
|
|
||||||
// import socket provider
|
|
||||||
const socketServer = socketProvider;
|
|
||||||
|
|
||||||
// Create express APP
|
// Create express APP
|
||||||
const app = express();
|
const app = express();
|
||||||
@@ -100,6 +101,9 @@ enum OntimeStartOrder {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let step = OntimeStartOrder.InitDB;
|
let step = OntimeStartOrder.InitDB;
|
||||||
|
let expressServer = null;
|
||||||
|
let oscServer = null;
|
||||||
|
|
||||||
const checkStart = (currentState: OntimeStartOrder) => {
|
const checkStart = (currentState: OntimeStartOrder) => {
|
||||||
if (step !== currentState) {
|
if (step !== currentState) {
|
||||||
step = OntimeStartOrder.Error;
|
step = OntimeStartOrder.Error;
|
||||||
@@ -116,9 +120,6 @@ export const startDb = async () => {
|
|||||||
await dbLoadingProcess;
|
await dbLoadingProcess;
|
||||||
};
|
};
|
||||||
|
|
||||||
// create HTTP server
|
|
||||||
const expressServer = http.createServer(app);
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Starts servers
|
* Starts servers
|
||||||
* @return {Promise<string>}
|
* @return {Promise<string>}
|
||||||
@@ -128,11 +129,13 @@ export const startServer = async () => {
|
|||||||
|
|
||||||
const serverPort = 4001; // hardcoded for now
|
const serverPort = 4001; // hardcoded for now
|
||||||
const returnMessage = `Ontime is listening on port ${serverPort}`;
|
const returnMessage = `Ontime is listening on port ${serverPort}`;
|
||||||
expressServer.listen(serverPort, '0.0.0.0');
|
|
||||||
|
|
||||||
socketServer.initServer(expressServer);
|
expressServer = http.createServer(app);
|
||||||
socketServer.info('SERVER', returnMessage);
|
|
||||||
socketServer.startListener();
|
socket.init(expressServer);
|
||||||
|
eventLoader.init();
|
||||||
|
|
||||||
|
expressServer.listen(serverPort, '0.0.0.0');
|
||||||
|
|
||||||
return returnMessage;
|
return returnMessage;
|
||||||
};
|
};
|
||||||
@@ -149,7 +152,7 @@ export const startOSCServer = async (overrideConfig = null) => {
|
|||||||
const { osc } = DataProvider.getData();
|
const { osc } = DataProvider.getData();
|
||||||
|
|
||||||
if (!osc.enabledIn) {
|
if (!osc.enabledIn) {
|
||||||
socketServer.info('RX', 'OSC Input Disabled');
|
logger.info('RX', 'OSC Input Disabled');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -160,8 +163,8 @@ export const startOSCServer = async (overrideConfig = null) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// Start OSC Server
|
// Start OSC Server
|
||||||
socketServer.info('RX', `Starting OSC Server on port: ${oscSettings.portIn}`);
|
logger.info('RX', `Starting OSC Server on port: ${oscSettings.portIn}`);
|
||||||
initiateOSC(oscSettings);
|
oscServer = new OscServer(oscSettings);
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -178,7 +181,7 @@ export const startIntegrations = async (config?: { osc: OSCSettings }) => {
|
|||||||
|
|
||||||
const oscIntegration = new OscIntegration();
|
const oscIntegration = new OscIntegration();
|
||||||
const { success, message } = oscIntegration.init(osc);
|
const { success, message } = oscIntegration.init(osc);
|
||||||
socketServer.info('RX', message);
|
logger.info('RX', message);
|
||||||
|
|
||||||
if (success) {
|
if (success) {
|
||||||
integrationService.register(oscIntegration);
|
integrationService.register(oscIntegration);
|
||||||
@@ -193,25 +196,26 @@ export const startIntegrations = async (config?: { osc: OSCSettings }) => {
|
|||||||
export const shutdown = async (exitCode = 0) => {
|
export const shutdown = async (exitCode = 0) => {
|
||||||
console.log(`Ontime shutting down with code ${exitCode}`);
|
console.log(`Ontime shutting down with code ${exitCode}`);
|
||||||
|
|
||||||
expressServer.close();
|
expressServer?.close();
|
||||||
shutdownOSCServer();
|
oscServer?.shutdown();
|
||||||
eventTimer.shutdown();
|
eventTimer.shutdown();
|
||||||
socketServer.shutdown();
|
|
||||||
integrationService.shutdown();
|
integrationService.shutdown();
|
||||||
|
logger.shutdown();
|
||||||
|
socket.shutdown();
|
||||||
process.exit(exitCode);
|
process.exit(exitCode);
|
||||||
};
|
};
|
||||||
|
|
||||||
process.on('exit', (code) => console.log(`Ontime exited with code: ${code}`));
|
process.on('exit', (code) => console.log(`Ontime exited with code: ${code}`));
|
||||||
|
|
||||||
process.on('unhandledRejection', async (error, promise) => {
|
process.on('unhandledRejection', async (error) => {
|
||||||
console.error(error, 'Error: unhandled rejection', promise);
|
reportSentryException(error);
|
||||||
socketServer.error('SERVER', 'Error: unhandled rejection');
|
logger.error('SERVER', 'Error: unhandled rejection');
|
||||||
await shutdown(1);
|
await shutdown(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
process.on('uncaughtException', async (error, promise) => {
|
process.on('uncaughtException', async (error) => {
|
||||||
console.error(error, 'Error: uncaught exception', promise);
|
reportSentryException(error);
|
||||||
socketServer.error('SERVER', 'Error: uncaught exception');
|
logger.error('SERVER', 'Error: uncaught exception');
|
||||||
await shutdown(1);
|
await shutdown(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,104 @@
|
|||||||
|
import { Log, LogLevel } from 'ontime-types';
|
||||||
|
import { generateId, millisToString } from 'ontime-utils';
|
||||||
|
|
||||||
|
import { clock } from '../services/Clock.js';
|
||||||
|
import { isProduction } from '../setup.js';
|
||||||
|
import { socket } from '../adapters/WebsocketAdapter.js';
|
||||||
|
|
||||||
|
class Logger {
|
||||||
|
private queue: Log[];
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
this.queue = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Enabling setup logger after init
|
||||||
|
*/
|
||||||
|
init() {
|
||||||
|
this.queue.forEach((log) => {
|
||||||
|
this._push(log);
|
||||||
|
});
|
||||||
|
this.queue = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
private addToQueue(log: Log) {
|
||||||
|
this.queue.push(log);
|
||||||
|
if (this.queue.length > 100) {
|
||||||
|
this.queue.pop();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Internal safe push method, adds log to queue if callback not available
|
||||||
|
* @param log
|
||||||
|
*/
|
||||||
|
private _push(log: Log) {
|
||||||
|
if (!isProduction) {
|
||||||
|
console.log(`[${log.level}] \t ${log.origin} \t ${log.text}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
socket.send({
|
||||||
|
type: 'ontime-log',
|
||||||
|
payload: log,
|
||||||
|
});
|
||||||
|
} catch (_e) {
|
||||||
|
this.addToQueue(log);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Emits logging message
|
||||||
|
* @param level
|
||||||
|
* @param origin
|
||||||
|
* @param text
|
||||||
|
*/
|
||||||
|
emit(level, origin: string, text: string) {
|
||||||
|
const log = {
|
||||||
|
id: generateId(),
|
||||||
|
level,
|
||||||
|
origin,
|
||||||
|
text,
|
||||||
|
time: millisToString(clock.getSystemTime() || 0),
|
||||||
|
};
|
||||||
|
this._push(log);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Utility to emit logging message of type INFO
|
||||||
|
* @param origin
|
||||||
|
* @param text
|
||||||
|
*/
|
||||||
|
info(origin: string, text: string) {
|
||||||
|
this.emit(LogLevel.Info, origin, text);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Utility to emit logging message of type WARN
|
||||||
|
* @param origin
|
||||||
|
* @param text
|
||||||
|
*/
|
||||||
|
warning(origin: string, text: string) {
|
||||||
|
this.emit(LogLevel.Warn, origin, text);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Utility to emit logging message of type ERROR
|
||||||
|
* @param origin
|
||||||
|
* @param text
|
||||||
|
*/
|
||||||
|
error(origin: string, text: string) {
|
||||||
|
this.emit(LogLevel.Error, origin, text);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Shutdown logger
|
||||||
|
*/
|
||||||
|
shutdown() {
|
||||||
|
console.log('Shutting down logger');
|
||||||
|
this.queue = [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const logger = new Logger();
|
||||||
@@ -1,31 +1,17 @@
|
|||||||
|
import { OntimeEvent, TitleBlock, Loaded } from 'ontime-types';
|
||||||
|
|
||||||
import { DataProvider } from '../data-provider/DataProvider.js';
|
import { DataProvider } from '../data-provider/DataProvider.js';
|
||||||
import { getRollTimers } from '../../services/rollUtils.js';
|
import { getRollTimers } from '../../services/rollUtils.js';
|
||||||
import { eventStore } from '../../stores/EventStore.js';
|
import { eventStore } from '../../stores/EventStore.js';
|
||||||
|
|
||||||
let instance;
|
let instance;
|
||||||
|
|
||||||
type TitleBlock = {
|
|
||||||
titleNow: string | null;
|
|
||||||
subtitleNow: string | null;
|
|
||||||
presenterNow: string | null;
|
|
||||||
noteNow: string | null;
|
|
||||||
titleNext: string | null;
|
|
||||||
subtitleNext: string | null;
|
|
||||||
presenterNext: string | null;
|
|
||||||
noteNext: string | null;
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Manages business logic around loading events
|
* Manages business logic around loading events
|
||||||
*/
|
*/
|
||||||
export class EventLoader {
|
export class EventLoader {
|
||||||
loadedEvent: object | null;
|
loadedEvent: OntimeEvent | null;
|
||||||
numEvents: number | null;
|
loaded: Loaded;
|
||||||
selectedEventIndex: number | null;
|
|
||||||
selectedEventId: string | null;
|
|
||||||
selectedPublicEventId: string | null;
|
|
||||||
nextEventId: string | null;
|
|
||||||
nextPublicEventId: string | null;
|
|
||||||
titles: TitleBlock;
|
titles: TitleBlock;
|
||||||
titlesPublic: TitleBlock;
|
titlesPublic: TitleBlock;
|
||||||
|
|
||||||
@@ -36,7 +22,10 @@ export class EventLoader {
|
|||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-this-alias -- this logic is used to ensure singleton
|
// eslint-disable-next-line @typescript-eslint/no-this-alias -- this logic is used to ensure singleton
|
||||||
instance = this;
|
instance = this;
|
||||||
this.reset(false);
|
}
|
||||||
|
|
||||||
|
init() {
|
||||||
|
this.reset();
|
||||||
this.loadedEvent = null;
|
this.loadedEvent = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -122,16 +111,16 @@ export class EventLoader {
|
|||||||
*/
|
*/
|
||||||
findPrevious() {
|
findPrevious() {
|
||||||
const timedEvents = EventLoader.getPlayableEvents();
|
const timedEvents = EventLoader.getPlayableEvents();
|
||||||
if (timedEvents === null || !timedEvents.length || this.selectedEventIndex === 0) {
|
if (timedEvents === null || !timedEvents.length || this.loaded.selectedEventIndex === 0) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
// if there is no event running, go to first
|
// if there is no event running, go to first
|
||||||
if (this.selectedEventIndex === null) {
|
if (this.loaded.selectedEventIndex === null) {
|
||||||
return timedEvents[0];
|
return timedEvents[0];
|
||||||
}
|
}
|
||||||
|
|
||||||
const newIndex = this.selectedEventIndex - 1;
|
const newIndex = this.loaded.selectedEventIndex - 1;
|
||||||
return timedEvents?.[newIndex];
|
return timedEvents?.[newIndex];
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -141,15 +130,15 @@ export class EventLoader {
|
|||||||
*/
|
*/
|
||||||
findNext() {
|
findNext() {
|
||||||
const timedEvents = EventLoader.getPlayableEvents();
|
const timedEvents = EventLoader.getPlayableEvents();
|
||||||
if (timedEvents === null || !timedEvents.length || this.selectedEventIndex === this.numEvents - 1) {
|
if (timedEvents === null || !timedEvents.length || this.loaded.selectedEventIndex === this.loaded.numEvents - 1) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
// if there is no event running, go to first
|
// if there is no event running, go to first
|
||||||
if (this.selectedEventIndex === null) {
|
if (this.loaded.selectedEventIndex === null) {
|
||||||
return timedEvents[0];
|
return timedEvents[0];
|
||||||
}
|
}
|
||||||
const newIndex = this.selectedEventIndex + 1;
|
const newIndex = this.loaded.selectedEventIndex + 1;
|
||||||
return timedEvents?.[newIndex];
|
return timedEvents?.[newIndex];
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -167,9 +156,9 @@ export class EventLoader {
|
|||||||
getRollTimers(timedEvents, timeNow);
|
getRollTimers(timedEvents, timeNow);
|
||||||
|
|
||||||
this.loadedEvent = currentEvent;
|
this.loadedEvent = currentEvent;
|
||||||
this.selectedEventIndex = nowIndex;
|
this.loaded.selectedEventIndex = nowIndex;
|
||||||
this.selectedEventId = currentEvent?.id || null;
|
this.loaded.selectedEventId = currentEvent?.id || null;
|
||||||
this.numEvents = timedEvents.length;
|
this.loaded.numEvents = timedEvents.length;
|
||||||
|
|
||||||
// titles
|
// titles
|
||||||
this._loadThisTitles(currentEvent, 'now-private');
|
this._loadThisTitles(currentEvent, 'now-private');
|
||||||
@@ -187,28 +176,33 @@ export class EventLoader {
|
|||||||
getLoaded() {
|
getLoaded() {
|
||||||
return {
|
return {
|
||||||
loadedEvent: this.loadedEvent,
|
loadedEvent: this.loadedEvent,
|
||||||
selectedEventIndex: this.selectedEventIndex,
|
loaded: this.loaded,
|
||||||
selectedEventId: this.selectedEventId,
|
|
||||||
selectedPublicEventId: this.selectedPublicEventId,
|
|
||||||
nextEventId: this.nextEventId,
|
|
||||||
nextPublicEventId: this.nextPublicEventId,
|
|
||||||
numEvents: this.numEvents,
|
|
||||||
titles: this.titles,
|
titles: this.titles,
|
||||||
titlesPublic: this.titlesPublic,
|
titlesPublic: this.titlesPublic,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Forces event loader to update the event count
|
||||||
|
*/
|
||||||
|
updateNumEvents() {
|
||||||
|
this.loaded.numEvents = EventLoader.getPlayableEvents().length;
|
||||||
|
eventStore.set('loaded', this.loaded);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Resets instance state
|
* Resets instance state
|
||||||
*/
|
*/
|
||||||
reset(emit?: boolean) {
|
reset(emit = true) {
|
||||||
this.loadedEvent = null;
|
this.loadedEvent = null;
|
||||||
this.selectedEventIndex = null;
|
this.loaded = {
|
||||||
this.selectedEventId = null;
|
selectedEventIndex: null,
|
||||||
this.selectedPublicEventId = null;
|
selectedEventId: null,
|
||||||
this.nextEventId = null;
|
selectedPublicEventId: null,
|
||||||
this.nextPublicEventId = null;
|
nextEventId: null,
|
||||||
this.numEvents = null;
|
nextPublicEventId: null,
|
||||||
|
numEvents: EventLoader.getPlayableEvents().length,
|
||||||
|
};
|
||||||
this.titles = {
|
this.titles = {
|
||||||
titleNow: null,
|
titleNow: null,
|
||||||
subtitleNow: null,
|
subtitleNow: null,
|
||||||
@@ -250,9 +244,9 @@ export class EventLoader {
|
|||||||
|
|
||||||
// we know some stuff now
|
// we know some stuff now
|
||||||
this.loadedEvent = event;
|
this.loadedEvent = event;
|
||||||
this.selectedEventIndex = eventIndex;
|
this.loaded.selectedEventIndex = eventIndex;
|
||||||
this.selectedEventId = event.id;
|
this.loaded.selectedEventId = event.id;
|
||||||
this.numEvents = timedEvents.length;
|
this.loaded.numEvents = timedEvents.length;
|
||||||
// this.nextEventId = playableEvents[eventIndex + 1].id;
|
// this.nextEventId = playableEvents[eventIndex + 1].id;
|
||||||
this._loadTitlesNow(event, playableEvents);
|
this._loadTitlesNow(event, playableEvents);
|
||||||
this._loadTitlesNext(playableEvents);
|
this._loadTitlesNext(playableEvents);
|
||||||
@@ -266,6 +260,7 @@ export class EventLoader {
|
|||||||
* Handle side effects from event loading
|
* Handle side effects from event loading
|
||||||
*/
|
*/
|
||||||
private _loadEvent() {
|
private _loadEvent() {
|
||||||
|
eventStore.set('loaded', this.loaded);
|
||||||
eventStore.set('titles', this.titles);
|
eventStore.set('titles', this.titles);
|
||||||
eventStore.set('titlesPublic', this.titlesPublic);
|
eventStore.set('titlesPublic', this.titlesPublic);
|
||||||
}
|
}
|
||||||
@@ -288,13 +283,13 @@ export class EventLoader {
|
|||||||
this.titlesPublic.titleNow = null;
|
this.titlesPublic.titleNow = null;
|
||||||
this.titlesPublic.subtitleNow = null;
|
this.titlesPublic.subtitleNow = null;
|
||||||
this.titlesPublic.presenterNow = null;
|
this.titlesPublic.presenterNow = null;
|
||||||
this.selectedPublicEventId = null;
|
this.loaded.selectedPublicEventId = null;
|
||||||
|
|
||||||
// if there is nothing before, return
|
// if there is nothing before, return
|
||||||
if (this.selectedEventIndex === 0) return;
|
if (this.loaded.selectedEventIndex === 0) return;
|
||||||
|
|
||||||
// iterate backwards to find it
|
// iterate backwards to find it
|
||||||
for (let i = this.selectedEventIndex; i >= 0; i--) {
|
for (let i = this.loaded.selectedEventIndex; i >= 0; i--) {
|
||||||
if (rundown[i].isPublic) {
|
if (rundown[i].isPublic) {
|
||||||
this._loadThisTitles(rundown[i], 'now-public');
|
this._loadThisTitles(rundown[i], 'now-public');
|
||||||
break;
|
break;
|
||||||
@@ -309,27 +304,27 @@ export class EventLoader {
|
|||||||
*/
|
*/
|
||||||
private _loadTitlesNext(rundown) {
|
private _loadTitlesNext(rundown) {
|
||||||
// maybe there is nothing to load
|
// maybe there is nothing to load
|
||||||
if (this.selectedEventIndex === null) return;
|
if (this.loaded.selectedEventIndex === null) return;
|
||||||
|
|
||||||
// assume there is no next event
|
// assume there is no next event
|
||||||
this.titles.titleNext = null;
|
this.titles.titleNext = null;
|
||||||
this.titles.subtitleNext = null;
|
this.titles.subtitleNext = null;
|
||||||
this.titles.presenterNext = null;
|
this.titles.presenterNext = null;
|
||||||
this.titles.noteNext = null;
|
this.titles.noteNext = null;
|
||||||
this.nextEventId = null;
|
this.loaded.nextEventId = null;
|
||||||
|
|
||||||
this.titlesPublic.titleNext = null;
|
this.titlesPublic.titleNext = null;
|
||||||
this.titlesPublic.subtitleNext = null;
|
this.titlesPublic.subtitleNext = null;
|
||||||
this.titlesPublic.presenterNext = null;
|
this.titlesPublic.presenterNext = null;
|
||||||
this.nextPublicEventId = null;
|
this.loaded.nextPublicEventId = null;
|
||||||
|
|
||||||
const numEvents = rundown.length;
|
const numEvents = rundown.length;
|
||||||
|
|
||||||
if (this.selectedEventIndex < numEvents - 1) {
|
if (this.loaded.selectedEventIndex < numEvents - 1) {
|
||||||
let nextPublic = false;
|
let nextPublic = false;
|
||||||
let nextPrivate = false;
|
let nextPrivate = false;
|
||||||
|
|
||||||
for (let i = this.selectedEventIndex + 1; i < numEvents; i++) {
|
for (let i = this.loaded.selectedEventIndex + 1; i < numEvents; i++) {
|
||||||
// if we have not set private
|
// if we have not set private
|
||||||
if (!nextPrivate) {
|
if (!nextPrivate) {
|
||||||
this._loadThisTitles(rundown[i], 'next-private');
|
this._loadThisTitles(rundown[i], 'next-private');
|
||||||
@@ -367,14 +362,14 @@ export class EventLoader {
|
|||||||
this.titlesPublic.subtitleNow = event.subtitle;
|
this.titlesPublic.subtitleNow = event.subtitle;
|
||||||
this.titlesPublic.presenterNow = event.presenter;
|
this.titlesPublic.presenterNow = event.presenter;
|
||||||
this.titlesPublic.noteNow = event.note;
|
this.titlesPublic.noteNow = event.note;
|
||||||
this.selectedPublicEventId = event.id;
|
this.loaded.selectedPublicEventId = event.id;
|
||||||
|
|
||||||
// private
|
// private
|
||||||
this.titles.titleNow = event.title;
|
this.titles.titleNow = event.title;
|
||||||
this.titles.subtitleNow = event.subtitle;
|
this.titles.subtitleNow = event.subtitle;
|
||||||
this.titles.presenterNow = event.presenter;
|
this.titles.presenterNow = event.presenter;
|
||||||
this.titles.noteNow = event.note;
|
this.titles.noteNow = event.note;
|
||||||
this.selectedEventId = event.id;
|
this.loaded.selectedEventId = event.id;
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case 'now-public':
|
case 'now-public':
|
||||||
@@ -382,7 +377,7 @@ export class EventLoader {
|
|||||||
this.titlesPublic.subtitleNow = event.subtitle;
|
this.titlesPublic.subtitleNow = event.subtitle;
|
||||||
this.titlesPublic.presenterNow = event.presenter;
|
this.titlesPublic.presenterNow = event.presenter;
|
||||||
this.titlesPublic.noteNow = event.note;
|
this.titlesPublic.noteNow = event.note;
|
||||||
this.selectedPublicEventId = event.id;
|
this.loaded.selectedPublicEventId = event.id;
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case 'now-private':
|
case 'now-private':
|
||||||
@@ -390,7 +385,7 @@ export class EventLoader {
|
|||||||
this.titles.subtitleNow = event.subtitle;
|
this.titles.subtitleNow = event.subtitle;
|
||||||
this.titles.presenterNow = event.presenter;
|
this.titles.presenterNow = event.presenter;
|
||||||
this.titles.noteNow = event.note;
|
this.titles.noteNow = event.note;
|
||||||
this.selectedEventId = event.id;
|
this.loaded.selectedEventId = event.id;
|
||||||
break;
|
break;
|
||||||
|
|
||||||
// next, load to both public and private
|
// next, load to both public and private
|
||||||
@@ -400,14 +395,14 @@ export class EventLoader {
|
|||||||
this.titlesPublic.subtitleNext = event.subtitle;
|
this.titlesPublic.subtitleNext = event.subtitle;
|
||||||
this.titlesPublic.presenterNext = event.presenter;
|
this.titlesPublic.presenterNext = event.presenter;
|
||||||
this.titlesPublic.noteNext = event.note;
|
this.titlesPublic.noteNext = event.note;
|
||||||
this.nextPublicEventId = event.id;
|
this.loaded.nextPublicEventId = event.id;
|
||||||
|
|
||||||
// private
|
// private
|
||||||
this.titles.titleNext = event.title;
|
this.titles.titleNext = event.title;
|
||||||
this.titles.subtitleNext = event.subtitle;
|
this.titles.subtitleNext = event.subtitle;
|
||||||
this.titles.presenterNext = event.presenter;
|
this.titles.presenterNext = event.presenter;
|
||||||
this.titles.noteNext = event.note;
|
this.titles.noteNext = event.note;
|
||||||
this.nextEventId = event.id;
|
this.loaded.nextEventId = event.id;
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case 'next-public':
|
case 'next-public':
|
||||||
@@ -415,7 +410,7 @@ export class EventLoader {
|
|||||||
this.titlesPublic.subtitleNext = event.subtitle;
|
this.titlesPublic.subtitleNext = event.subtitle;
|
||||||
this.titlesPublic.presenterNext = event.presenter;
|
this.titlesPublic.presenterNext = event.presenter;
|
||||||
this.titlesPublic.noteNext = event.note;
|
this.titlesPublic.noteNext = event.note;
|
||||||
this.nextPublicEventId = event.id;
|
this.loaded.nextPublicEventId = event.id;
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case 'next-private':
|
case 'next-private':
|
||||||
@@ -423,7 +418,7 @@ export class EventLoader {
|
|||||||
this.titles.subtitleNext = event.subtitle;
|
this.titles.subtitleNext = event.subtitle;
|
||||||
this.titles.presenterNext = event.presenter;
|
this.titles.presenterNext = event.presenter;
|
||||||
this.titles.noteNext = event.note;
|
this.titles.noteNext = event.note;
|
||||||
this.nextEventId = event.id;
|
this.loaded.nextEventId = event.id;
|
||||||
break;
|
break;
|
||||||
|
|
||||||
default:
|
default:
|
||||||
|
|||||||
@@ -1,446 +0,0 @@
|
|||||||
import { Server } from 'socket.io';
|
|
||||||
import { generateId } from 'ontime-utils';
|
|
||||||
|
|
||||||
import getRandomName from '../../utils/getRandomName.js';
|
|
||||||
import { stringFromMillis } from '../../utils/time.js';
|
|
||||||
import { messageManager } from '../message-manager/MessageManager.js';
|
|
||||||
import { PlaybackService } from '../../services/PlaybackService.js';
|
|
||||||
|
|
||||||
import { eventTimer } from '../../services/TimerService.js';
|
|
||||||
import { EventLoader, eventLoader } from '../event-loader/EventLoader.js';
|
|
||||||
import { clock } from '../../services/Clock.js';
|
|
||||||
|
|
||||||
class SocketController {
|
|
||||||
constructor() {
|
|
||||||
this.numClients = 0;
|
|
||||||
this.messageStack = [];
|
|
||||||
this._MAX_MESSAGES = 100;
|
|
||||||
this._clientNames = {};
|
|
||||||
this.socket = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
initServer(httpServer) {
|
|
||||||
this.socket = new Server(httpServer, {
|
|
||||||
cors: {
|
|
||||||
origin: '*',
|
|
||||||
methods: 'GET,HEAD,PUT,PATCH,POST,DELETE',
|
|
||||||
preflightContinue: false,
|
|
||||||
optionsSuccessStatus: 204,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
startListener() {
|
|
||||||
this._socketMessageHandler();
|
|
||||||
}
|
|
||||||
|
|
||||||
shutdown() {
|
|
||||||
this.info('SERVER', 'Shutting down ontime');
|
|
||||||
if (this.socket) {
|
|
||||||
this.info('TX', '... Closing socket server');
|
|
||||||
this.socket.close();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Handle socket io connections
|
|
||||||
* @private
|
|
||||||
*/
|
|
||||||
_socketMessageHandler() {
|
|
||||||
this.socket.on('connection', (socket) => {
|
|
||||||
/*******************************/
|
|
||||||
/*** HANDLE NEW CONNECTION ***/
|
|
||||||
/*** --------------------- ***/
|
|
||||||
/*******************************/
|
|
||||||
// keep track of connections
|
|
||||||
this.numClients++;
|
|
||||||
this._clientNames[socket.id] = getRandomName();
|
|
||||||
const message = `${this.numClients} Clients with new connection: ${this._clientNames[socket.id]}`;
|
|
||||||
this.info('CLIENT', message);
|
|
||||||
|
|
||||||
// Todo: review in favour of features
|
|
||||||
// send state
|
|
||||||
socket.emit('timer', eventTimer.timer);
|
|
||||||
socket.emit('playback', eventTimer.playback);
|
|
||||||
socket.emit('selected', {
|
|
||||||
id: eventLoader.selectedEventId,
|
|
||||||
index: eventLoader.selectedEventIndex,
|
|
||||||
total: eventLoader.numEvents,
|
|
||||||
});
|
|
||||||
socket.emit('next-id', eventLoader.nextEventId);
|
|
||||||
socket.emit('publicselected-id', eventLoader.selectedPublicEventId);
|
|
||||||
socket.emit('publicnext-id', eventLoader.nextPublicEventId);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @description handle disconnecting a user
|
|
||||||
*/
|
|
||||||
socket.on('disconnect', () => {
|
|
||||||
this.numClients--;
|
|
||||||
const message = `${this.numClients} Clients with disconnection: ${this._clientNames[socket.id]}`;
|
|
||||||
delete this._clientNames[socket.id];
|
|
||||||
this.info('CLIENT', message);
|
|
||||||
});
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @description utility for renaming a user
|
|
||||||
*/
|
|
||||||
socket.on('rename-client', (newName) => {
|
|
||||||
if (newName) {
|
|
||||||
const previousName = this._clientNames[socket.id];
|
|
||||||
this._clientNames[socket.id] = newName;
|
|
||||||
this.info('CLIENT', `Client ${previousName} renamed to ${newName}`);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
/***************************************/
|
|
||||||
/*** TIMER STATE GETTERS / SETTERS ***/
|
|
||||||
/*** ------- WEBSOCKET API ------- ***/
|
|
||||||
/*** ----------------------------- ***/
|
|
||||||
/***************************************/
|
|
||||||
|
|
||||||
/*******************************************/
|
|
||||||
socket.on('ontime-test', () => {
|
|
||||||
socket.emit('hello', socket.id);
|
|
||||||
});
|
|
||||||
|
|
||||||
socket.on('set-start', () => {
|
|
||||||
PlaybackService.start();
|
|
||||||
});
|
|
||||||
|
|
||||||
socket.on('set-startid', (data) => {
|
|
||||||
if (data) {
|
|
||||||
PlaybackService.startById(data);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
socket.on('set-startindex', (data) => {
|
|
||||||
const eventIndex = Number(data);
|
|
||||||
if (!isNaN(eventIndex)) {
|
|
||||||
PlaybackService.startByIndex(eventIndex);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
socket.on('set-loadid', (data) => {
|
|
||||||
if (data) {
|
|
||||||
PlaybackService.loadById(data);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
socket.on('set-loadindex', (data) => {
|
|
||||||
const eventIndex = Number(data);
|
|
||||||
if (!isNaN(eventIndex)) {
|
|
||||||
PlaybackService.loadByIndex(eventIndex - 1);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
socket.on('set-pause', () => {
|
|
||||||
PlaybackService.pause();
|
|
||||||
});
|
|
||||||
|
|
||||||
socket.on('set-stop', () => {
|
|
||||||
PlaybackService.stop();
|
|
||||||
});
|
|
||||||
|
|
||||||
socket.on('set-reload', () => {
|
|
||||||
PlaybackService.reload();
|
|
||||||
});
|
|
||||||
|
|
||||||
socket.on('set-previous', () => {
|
|
||||||
PlaybackService.loadPrevious();
|
|
||||||
});
|
|
||||||
|
|
||||||
socket.on('set-next', () => {
|
|
||||||
PlaybackService.loadNext();
|
|
||||||
});
|
|
||||||
|
|
||||||
socket.on('set-roll', () => {
|
|
||||||
PlaybackService.roll();
|
|
||||||
});
|
|
||||||
|
|
||||||
socket.on('set-delay', (data) => {
|
|
||||||
const delayTime = Number(data);
|
|
||||||
if (!isNaN(delayTime)) {
|
|
||||||
PlaybackService.setDelay(delayTime);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
/*******************************************/
|
|
||||||
// general playback state, useful for external sync
|
|
||||||
// Todo: add delayed value (will come from rundownService)
|
|
||||||
socket.on('ontime-poll', () => {
|
|
||||||
const timerPoll = eventTimer.timer;
|
|
||||||
const isDelayed = false;
|
|
||||||
const colour = '';
|
|
||||||
socket.emit('ontime-poll', { isDelayed, colour, ...timerPoll });
|
|
||||||
});
|
|
||||||
|
|
||||||
/*******************************************/
|
|
||||||
socket.on('get-playback', () => {
|
|
||||||
socket.emit('playback', eventTimer.playback);
|
|
||||||
});
|
|
||||||
|
|
||||||
socket.on('get-onAir', () => {
|
|
||||||
socket.emit('onAir', messageManager.onAir);
|
|
||||||
});
|
|
||||||
|
|
||||||
/*******************************************/
|
|
||||||
socket.on('get-selected', () => {
|
|
||||||
socket.emit('selected', {
|
|
||||||
id: eventLoader.selectedEventId,
|
|
||||||
index: eventLoader.selectedEventIndex,
|
|
||||||
total: eventLoader.numEvents,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
socket.on('get-titles', () => {
|
|
||||||
socket.emit('titles', eventLoader.titles);
|
|
||||||
});
|
|
||||||
|
|
||||||
socket.on('get-publictitles', () => {
|
|
||||||
socket.emit('publictitles', eventLoader.titlesPublic);
|
|
||||||
});
|
|
||||||
|
|
||||||
/***********************************/
|
|
||||||
/*** MESSAGE GETTERS / SETTERS ***/
|
|
||||||
/*** ------------------------- ***/
|
|
||||||
/***********************************/
|
|
||||||
|
|
||||||
// On Air
|
|
||||||
socket.on('set-onAir', (data) => {
|
|
||||||
if (typeof data === 'boolean') {
|
|
||||||
try {
|
|
||||||
const featureData = messageManager.setOnAir(data);
|
|
||||||
this.info('PLAYBACK', featureData.onAir ? 'Going On Air' : 'Going Off Air');
|
|
||||||
} catch (error) {
|
|
||||||
this.error('RX', `Failed to parse message ${data} : ${error}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Presenter message
|
|
||||||
socket.on('set-timer-message-text', (data) => {
|
|
||||||
if (typeof data !== 'string') {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
messageManager.setTimerText(data);
|
|
||||||
});
|
|
||||||
|
|
||||||
socket.on('set-timer-message-visible', (data) => {
|
|
||||||
if (typeof data !== 'boolean') {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
messageManager.setTimerVisibility(data);
|
|
||||||
});
|
|
||||||
|
|
||||||
/*******************************************/
|
|
||||||
// Public message
|
|
||||||
socket.on('set-public-message-text', (data) => {
|
|
||||||
if (typeof data !== 'string') {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
messageManager.setPublicText(data);
|
|
||||||
});
|
|
||||||
|
|
||||||
socket.on('set-public-message-visible', (data) => {
|
|
||||||
if (typeof data !== 'boolean') {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
messageManager.setPublicVisibility(data);
|
|
||||||
});
|
|
||||||
|
|
||||||
/*******************************************/
|
|
||||||
// Lower third message
|
|
||||||
socket.on('set-lower-message-text', (data) => {
|
|
||||||
if (typeof data !== 'string') {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
messageManager.setLowerText(data);
|
|
||||||
});
|
|
||||||
|
|
||||||
socket.on('set-lower-message-visible', (data) => {
|
|
||||||
if (typeof data !== 'boolean') {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
messageManager.setLowerVisibility(data);
|
|
||||||
});
|
|
||||||
|
|
||||||
/* MOLECULAR ENDPOINTS
|
|
||||||
* =====================
|
|
||||||
* 1. RUNDOWN
|
|
||||||
* 2. MESSAGE CONTROL
|
|
||||||
* 3. PLAYBACK CONTROL
|
|
||||||
* 4. INFO
|
|
||||||
* 5. CUE SHEET
|
|
||||||
* 6. TIMER OBJECT
|
|
||||||
* */
|
|
||||||
|
|
||||||
// 1. RUNDOWN
|
|
||||||
socket.on('get-feat-rundown', () => {
|
|
||||||
this.broadcastFeatureRundown();
|
|
||||||
});
|
|
||||||
|
|
||||||
// 2. MESSAGE CONTROL
|
|
||||||
socket.on('get-feat-messagecontrol', () => {
|
|
||||||
this.broadcastFeatureMessageControl();
|
|
||||||
});
|
|
||||||
|
|
||||||
// 3. PLAYBACK CONTROL
|
|
||||||
socket.on('get-feat-playbackcontrol', () => {
|
|
||||||
this.broadcastFeaturePlaybackControl();
|
|
||||||
});
|
|
||||||
|
|
||||||
// 4. INFO
|
|
||||||
socket.on('get-feat-info', () => {
|
|
||||||
this.broadcastFeatureInfo();
|
|
||||||
});
|
|
||||||
|
|
||||||
// 5. CUE SHEET
|
|
||||||
socket.on('get-feat-cuesheet', () => {
|
|
||||||
this.broadcastFeatureCuesheet();
|
|
||||||
});
|
|
||||||
|
|
||||||
// 6. TIMER
|
|
||||||
socket.on('get-timer', () => {
|
|
||||||
// TODO: Not ideal workaround
|
|
||||||
socket.emit('timer', eventTimer.timer);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
send(topic, payload) {
|
|
||||||
this.socket?.emit(topic, payload);
|
|
||||||
}
|
|
||||||
|
|
||||||
/****************************************************************************/
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Logger logic
|
|
||||||
* -------------
|
|
||||||
*/
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Utility method, sends message and pushes into stack
|
|
||||||
* @param {string} level
|
|
||||||
* @param {string} origin
|
|
||||||
* @param {string} text
|
|
||||||
*/
|
|
||||||
_push(level, origin, text) {
|
|
||||||
const logMessage = {
|
|
||||||
id: generateId(),
|
|
||||||
level,
|
|
||||||
origin,
|
|
||||||
text,
|
|
||||||
time: stringFromMillis(clock.getSystemTime() || 0),
|
|
||||||
};
|
|
||||||
|
|
||||||
this.messageStack.unshift(logMessage);
|
|
||||||
this.socket?.emit('logger', logMessage);
|
|
||||||
|
|
||||||
if (process.env.NODE_ENV !== 'production') {
|
|
||||||
console.log(`[${logMessage.level}] \t ${logMessage.origin} \t ${logMessage.text}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.messageStack.length > this._MAX_MESSAGES) {
|
|
||||||
this.messageStack.pop();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Broadcast data for Event List feature
|
|
||||||
*/
|
|
||||||
broadcastFeatureRundown() {
|
|
||||||
const featureData = {
|
|
||||||
selectedEventId: eventLoader.selectedEventId,
|
|
||||||
nextEventId: eventLoader.nextEventId,
|
|
||||||
playback: eventTimer.playback,
|
|
||||||
};
|
|
||||||
this.send('feat-rundown', featureData);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Broadcast data for Message Control feature
|
|
||||||
*/
|
|
||||||
broadcastFeatureMessageControl() {
|
|
||||||
const featureData = messageManager.getAll();
|
|
||||||
this.send('feat-messagecontrol', featureData);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Broadcast data for Playback Control feature
|
|
||||||
*/
|
|
||||||
broadcastFeaturePlaybackControl() {
|
|
||||||
const featureData = {
|
|
||||||
playback: eventTimer.playback,
|
|
||||||
selectedEventId: eventLoader.selectedEventId,
|
|
||||||
numEvents: EventLoader.getNumEvents(),
|
|
||||||
};
|
|
||||||
this.send('feat-playbackcontrol', featureData);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Broadcast data for Info feature
|
|
||||||
*/
|
|
||||||
broadcastFeatureInfo() {
|
|
||||||
const featureData = {
|
|
||||||
titles: eventLoader.titles,
|
|
||||||
playback: eventTimer.playback,
|
|
||||||
selectedEventId: eventLoader.selectedEventId,
|
|
||||||
selectedEventIndex: eventLoader.selectedEventIndex,
|
|
||||||
numEvents: EventLoader.getNumEvents(),
|
|
||||||
};
|
|
||||||
this.send('feat-info', featureData);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Broadcast data for Cuesheet feature
|
|
||||||
*/
|
|
||||||
broadcastFeatureCuesheet() {
|
|
||||||
const featureData = {
|
|
||||||
playback: eventTimer.playback,
|
|
||||||
selectedEventId: eventLoader.selectedEventId,
|
|
||||||
selectedEventIndex: eventLoader.selectedEventIndex,
|
|
||||||
numEvents: EventLoader.getNumEvents(),
|
|
||||||
titleNow: eventLoader.titles.titleNow,
|
|
||||||
};
|
|
||||||
this.send('feat-cuesheet', featureData);
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: ouch, services should update the store
|
|
||||||
// make middleware to maintain the features OR remove the feature endpoints
|
|
||||||
broadcastState() {
|
|
||||||
this.broadcastFeatureRundown();
|
|
||||||
this.broadcastFeatureMessageControl();
|
|
||||||
this.broadcastFeaturePlaybackControl();
|
|
||||||
this.broadcastFeatureInfo();
|
|
||||||
this.broadcastFeatureCuesheet();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export const socketProvider = new SocketController();
|
|
||||||
@@ -1,159 +0,0 @@
|
|||||||
import { Server } from 'node-osc';
|
|
||||||
import { OSCSettings } from 'ontime-types';
|
|
||||||
|
|
||||||
import { PlaybackService } from '../services/PlaybackService.js';
|
|
||||||
import { messageManager } from '../classes/message-manager/MessageManager.js';
|
|
||||||
import { socketProvider } from '../classes/socket/SocketController.js';
|
|
||||||
|
|
||||||
let oscServer = null;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @description utility function to shut down osc server
|
|
||||||
*/
|
|
||||||
export const shutdownOSCServer = () => {
|
|
||||||
if (oscServer != null) oscServer.close();
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Initialises OSC server
|
|
||||||
*/
|
|
||||||
export const initiateOSC = (config: OSCSettings) => {
|
|
||||||
oscServer = new Server(config.portIn, '0.0.0.0');
|
|
||||||
|
|
||||||
oscServer.on('error', console.error);
|
|
||||||
|
|
||||||
oscServer.on('message', function (msg) {
|
|
||||||
// message should look like /ontime/{path} {args} where
|
|
||||||
// ontime: fixed message for app
|
|
||||||
// path: command to be called
|
|
||||||
// args: extra data, only used on some API entries (delay, goto)
|
|
||||||
|
|
||||||
// split message
|
|
||||||
const [, address, path] = msg[0].split('/');
|
|
||||||
const args = msg[1];
|
|
||||||
|
|
||||||
// get first part before (ontime)
|
|
||||||
if (address !== 'ontime') {
|
|
||||||
console.error('RX', `OSC IN: Message address ${address} not recognised`, msg);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// get second part (command)
|
|
||||||
if (!path) {
|
|
||||||
console.error('RX', 'OSC IN: No path found');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
switch (path.toLowerCase()) {
|
|
||||||
case 'onair': {
|
|
||||||
messageManager.setOnAir(true);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case 'offair': {
|
|
||||||
messageManager.setOnAir(false);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case 'play': {
|
|
||||||
PlaybackService.start();
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case 'start': {
|
|
||||||
try {
|
|
||||||
const eventIndex = Number(args);
|
|
||||||
if (isNaN(eventIndex)) {
|
|
||||||
socketProvider.error('RX', `OSC IN: event index not recognised ${args}`);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
PlaybackService.startByIndex(eventIndex);
|
|
||||||
} catch (error) {
|
|
||||||
console.log('Error loading event: ', error);
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case 'startid': {
|
|
||||||
if (!args) {
|
|
||||||
socketProvider.error('RX', `OSC IN: No ID in request`);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
PlaybackService.loadById(args);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case 'pause': {
|
|
||||||
PlaybackService.pause();
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case 'prev': {
|
|
||||||
PlaybackService.loadPrevious();
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case 'next': {
|
|
||||||
PlaybackService.loadNext();
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case 'unload':
|
|
||||||
case 'stop': {
|
|
||||||
PlaybackService.stop();
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case 'reload': {
|
|
||||||
PlaybackService.reload();
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case 'roll': {
|
|
||||||
PlaybackService.roll();
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case 'delay': {
|
|
||||||
try {
|
|
||||||
const delayTime = Number(args);
|
|
||||||
if (isNaN(delayTime)) {
|
|
||||||
socketProvider.error('RX', `OSC IN: delay time not recognised ${args}`);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
PlaybackService.setDelay(delayTime);
|
|
||||||
} catch (error) {
|
|
||||||
console.log('Error adding delay: ', error);
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case 'goto':
|
|
||||||
case 'load': {
|
|
||||||
try {
|
|
||||||
const eventIndex = Number(args);
|
|
||||||
if (isNaN(eventIndex) || eventIndex <= 0) {
|
|
||||||
socketProvider.error('RX', `OSC IN: event index not recognised or out of range ${eventIndex}`);
|
|
||||||
} else {
|
|
||||||
PlaybackService.loadByIndex(eventIndex - 1);
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
socketProvider.error('RX', `OSC IN: error calling goto ${error}`);
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case 'gotoid':
|
|
||||||
case 'loadid': {
|
|
||||||
if (!args) {
|
|
||||||
socketProvider.error('RX', `OSC IN: event ID not recognised: ${args}}`);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
PlaybackService.loadById(args.toString().toLowerCase());
|
|
||||||
} catch (error) {
|
|
||||||
socketProvider.error('RX', `OSC IN: error calling goto ${error}`);
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
case 'get-playback': {
|
|
||||||
const playback = global.timer.state;
|
|
||||||
global.timer.sendOsc('playback', playback);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
default: {
|
|
||||||
socketProvider.warning('RX', `OSC IN: unhandled message ${path}`);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
};
|
|
||||||
@@ -0,0 +1,188 @@
|
|||||||
|
import { messageService } from '../services/message-service/MessageService.js';
|
||||||
|
import { PlaybackService } from '../services/PlaybackService.js';
|
||||||
|
import { eventStore } from '../stores/EventStore.js';
|
||||||
|
|
||||||
|
export function dispatchFromAdapter(type: string, payload: unknown, source?: 'osc' | 'ws') {
|
||||||
|
switch (type.toLowerCase()) {
|
||||||
|
case 'test-ontime': {
|
||||||
|
return { topic: 'hello' };
|
||||||
|
}
|
||||||
|
|
||||||
|
case 'ontime-poll': {
|
||||||
|
return {
|
||||||
|
topic: 'poll',
|
||||||
|
payload: eventStore.poll(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
case 'set-onair': {
|
||||||
|
if (typeof payload !== 'undefined') {
|
||||||
|
messageService.setOnAir(Boolean(payload));
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
case 'onair': {
|
||||||
|
messageService.setOnAir(true);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
case 'offair': {
|
||||||
|
messageService.setOnAir(false);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
case 'set-timer-message-text': {
|
||||||
|
if (typeof payload !== 'string') {
|
||||||
|
throw new Error(`Unable to parse payload: ${payload}`);
|
||||||
|
}
|
||||||
|
messageService.setTimerText(payload);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 'set-timer-message-visible': {
|
||||||
|
if (typeof payload === 'undefined') {
|
||||||
|
throw new Error(`Unable to parse payload: ${payload}`);
|
||||||
|
}
|
||||||
|
messageService.setTimerVisibility(Boolean(payload));
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
case 'set-public-message-text': {
|
||||||
|
if (typeof payload !== 'string') {
|
||||||
|
throw new Error(`Unable to parse payload: ${payload}`);
|
||||||
|
}
|
||||||
|
messageService.setPublicText(payload);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 'set-public-message-visible': {
|
||||||
|
if (typeof payload === 'undefined') {
|
||||||
|
throw new Error(`Unable to parse payload: ${payload}`);
|
||||||
|
}
|
||||||
|
messageService.setPublicVisibility(Boolean(payload));
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
case 'set-lower-message-text': {
|
||||||
|
if (typeof payload !== 'string') {
|
||||||
|
throw new Error(`Unable to parse payload: ${payload}`);
|
||||||
|
}
|
||||||
|
messageService.setLowerText(payload);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 'set-lower-message-visible': {
|
||||||
|
if (typeof payload === 'undefined') {
|
||||||
|
throw new Error(`Unable to parse payload: ${payload}`);
|
||||||
|
}
|
||||||
|
messageService.setLowerVisibility(Boolean(payload));
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
case 'start': {
|
||||||
|
PlaybackService.start();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
case 'startindex': {
|
||||||
|
const eventIndex = Number(payload);
|
||||||
|
if (isNaN(eventIndex) || eventIndex <= 0) {
|
||||||
|
throw new Error(`Event index not recognised or out of range ${eventIndex}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Indexes in frontend are 1 based
|
||||||
|
PlaybackService.startByIndex(eventIndex - 1);
|
||||||
|
} catch (error) {
|
||||||
|
throw new Error(`Error loading event:: ${error}`);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
case 'startid': {
|
||||||
|
if (!payload) {
|
||||||
|
throw new Error(`Event ID not recognised: ${payload}`);
|
||||||
|
}
|
||||||
|
PlaybackService.startById(payload);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 'pause': {
|
||||||
|
PlaybackService.pause();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 'previous': {
|
||||||
|
PlaybackService.loadPrevious();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 'next': {
|
||||||
|
PlaybackService.loadNext();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 'unload':
|
||||||
|
case 'stop': {
|
||||||
|
PlaybackService.stop();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 'reload': {
|
||||||
|
PlaybackService.reload();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 'roll': {
|
||||||
|
PlaybackService.roll();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 'delay': {
|
||||||
|
const delayTime = Number(payload);
|
||||||
|
if (isNaN(delayTime)) {
|
||||||
|
throw new Error(`Delay time not recognised ${payload}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
PlaybackService.setDelay(delayTime);
|
||||||
|
} catch (error) {
|
||||||
|
throw new Error(`Could not add delay: ${error}`);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 'gotoindex':
|
||||||
|
case 'loadindex': {
|
||||||
|
const eventIndex = Number(payload);
|
||||||
|
if (isNaN(eventIndex) || eventIndex <= 0) {
|
||||||
|
throw new Error(`Event index not recognised or out of range ${eventIndex}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Indexes in frontend are 1 based
|
||||||
|
PlaybackService.loadByIndex(eventIndex - 1);
|
||||||
|
} catch (error) {
|
||||||
|
throw new Error(`Event index not recognised or out of range ${error}`);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 'gotoid':
|
||||||
|
case 'loadid': {
|
||||||
|
if (!payload) {
|
||||||
|
throw new Error(`Event ID not recognised: ${payload}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
PlaybackService.loadById(payload.toString().toLowerCase());
|
||||||
|
} catch (error) {
|
||||||
|
throw new Error(`OSC IN: error calling goto ${error}`);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
case 'get-playback': {
|
||||||
|
const playback = eventStore.get('playback');
|
||||||
|
return { topic: 'playback', payload: playback };
|
||||||
|
}
|
||||||
|
|
||||||
|
case 'get-timer': {
|
||||||
|
const timer = eventStore.get('timer');
|
||||||
|
return { topic: 'timer', payload: timer };
|
||||||
|
}
|
||||||
|
|
||||||
|
default: {
|
||||||
|
throw new Error(`Unhandled message ${type}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,11 +1,10 @@
|
|||||||
// Create controller for GET request to '/playback'
|
|
||||||
// Returns ACK message
|
|
||||||
import { PlaybackService } from '../services/PlaybackService.js';
|
import { PlaybackService } from '../services/PlaybackService.js';
|
||||||
|
import { eventStore } from '../stores/EventStore.js';
|
||||||
|
|
||||||
// Create controller for POST request to '/playback'
|
// Create controller for POST request to '/playback'
|
||||||
// Returns playback state
|
// Returns playback state
|
||||||
export const pbGet = async (req, res) => {
|
export const pbGet = async (req, res) => {
|
||||||
res.send({ playback: global.timer.state });
|
res.send({ playback: eventStore.get('playback') });
|
||||||
};
|
};
|
||||||
|
|
||||||
// Create controller for POST request to '/playback/start'
|
// Create controller for POST request to '/playback/start'
|
||||||
|
|||||||
@@ -2,8 +2,8 @@ import * as Sentry from '@sentry/node';
|
|||||||
|
|
||||||
let shouldReport;
|
let shouldReport;
|
||||||
|
|
||||||
export function initSentry(environment) {
|
export function initSentry(doReport) {
|
||||||
shouldReport = environment === 'production';
|
shouldReport = doReport;
|
||||||
Sentry.init({
|
Sentry.init({
|
||||||
dsn: 'https://ceb6abdce7374857bb50b65636cbaed1@o4504288369836032.ingest.sentry.io/4504288555565056',
|
dsn: 'https://ceb6abdce7374857bb50b65636cbaed1@o4504288369836032.ingest.sentry.io/4504288555565056',
|
||||||
tracesSampleRate: 1.0,
|
tracesSampleRate: 1.0,
|
||||||
|
|||||||
+24
-25
@@ -1,11 +1,10 @@
|
|||||||
/**
|
import { Playback } from 'ontime-types';
|
||||||
* starts loaded timer
|
|
||||||
*/
|
|
||||||
import { socketProvider } from '../classes/socket/SocketController.js';
|
|
||||||
import { eventLoader, EventLoader } from '../classes/event-loader/EventLoader.js';
|
import { eventLoader, EventLoader } from '../classes/event-loader/EventLoader.js';
|
||||||
import { eventStore } from '../stores/EventStore.js';
|
import { eventStore } from '../stores/EventStore.js';
|
||||||
import { eventTimer } from './TimerService.js';
|
import { eventTimer } from './TimerService.js';
|
||||||
import { clock } from './Clock.js';
|
import { clock } from './Clock.js';
|
||||||
|
import { logger } from '../classes/Logger.js';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Service manages playback status of app
|
* Service manages playback status of app
|
||||||
@@ -20,9 +19,9 @@ export class PlaybackService {
|
|||||||
static loadEvent(event) {
|
static loadEvent(event) {
|
||||||
let success = false;
|
let success = false;
|
||||||
if (!event) {
|
if (!event) {
|
||||||
socketProvider.error('PLAYBACK', 'No event found');
|
logger.error('PLAYBACK', 'No event found');
|
||||||
} else if (event.skip) {
|
} else if (event.skip) {
|
||||||
socketProvider.warning('PLAYBACK', `Refused playback of skipped event ID ${event.id}`);
|
logger.warning('PLAYBACK', `Refused playback of skipped event ID ${event.id}`);
|
||||||
} else {
|
} else {
|
||||||
eventLoader.loadEvent(event);
|
eventLoader.loadEvent(event);
|
||||||
eventTimer.load(event);
|
eventTimer.load(event);
|
||||||
@@ -41,7 +40,7 @@ export class PlaybackService {
|
|||||||
const event = EventLoader.getEventWithId(eventId);
|
const event = EventLoader.getEventWithId(eventId);
|
||||||
const success = PlaybackService.loadEvent(event);
|
const success = PlaybackService.loadEvent(event);
|
||||||
if (success) {
|
if (success) {
|
||||||
socketProvider.info('PLAYBACK', `Loaded event with ID ${event.id}`);
|
logger.info('PLAYBACK', `Loaded event with ID ${event.id}`);
|
||||||
PlaybackService.start();
|
PlaybackService.start();
|
||||||
}
|
}
|
||||||
return success;
|
return success;
|
||||||
@@ -56,7 +55,7 @@ export class PlaybackService {
|
|||||||
const event = EventLoader.getEventAtIndex(eventIndex);
|
const event = EventLoader.getEventAtIndex(eventIndex);
|
||||||
const success = PlaybackService.loadEvent(event);
|
const success = PlaybackService.loadEvent(event);
|
||||||
if (success) {
|
if (success) {
|
||||||
socketProvider.info('PLAYBACK', `Loaded event with ID ${event.id}`);
|
logger.info('PLAYBACK', `Loaded event with ID ${event.id}`);
|
||||||
PlaybackService.start();
|
PlaybackService.start();
|
||||||
}
|
}
|
||||||
return success;
|
return success;
|
||||||
@@ -71,7 +70,7 @@ export class PlaybackService {
|
|||||||
const event = EventLoader.getEventWithId(eventId);
|
const event = EventLoader.getEventWithId(eventId);
|
||||||
const success = PlaybackService.loadEvent(event);
|
const success = PlaybackService.loadEvent(event);
|
||||||
if (success) {
|
if (success) {
|
||||||
socketProvider.info('PLAYBACK', `Loaded event with ID ${event.id}`);
|
logger.info('PLAYBACK', `Loaded event with ID ${event.id}`);
|
||||||
}
|
}
|
||||||
return success;
|
return success;
|
||||||
}
|
}
|
||||||
@@ -85,7 +84,7 @@ export class PlaybackService {
|
|||||||
const event = EventLoader.getEventAtIndex(eventIndex);
|
const event = EventLoader.getEventAtIndex(eventIndex);
|
||||||
const success = PlaybackService.loadEvent(event);
|
const success = PlaybackService.loadEvent(event);
|
||||||
if (success) {
|
if (success) {
|
||||||
socketProvider.info('PLAYBACK', `Loaded event with ID ${event.id}`);
|
logger.info('PLAYBACK', `Loaded event with ID ${event.id}`);
|
||||||
}
|
}
|
||||||
return success;
|
return success;
|
||||||
}
|
}
|
||||||
@@ -98,7 +97,7 @@ export class PlaybackService {
|
|||||||
if (previousEvent) {
|
if (previousEvent) {
|
||||||
const success = PlaybackService.loadEvent(previousEvent);
|
const success = PlaybackService.loadEvent(previousEvent);
|
||||||
if (success) {
|
if (success) {
|
||||||
socketProvider.info('PLAYBACK', `Loaded event with ID ${previousEvent.id}`);
|
logger.info('PLAYBACK', `Loaded event with ID ${previousEvent.id}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -111,7 +110,7 @@ export class PlaybackService {
|
|||||||
if (nextEvent) {
|
if (nextEvent) {
|
||||||
const success = PlaybackService.loadEvent(nextEvent);
|
const success = PlaybackService.loadEvent(nextEvent);
|
||||||
if (success) {
|
if (success) {
|
||||||
socketProvider.info('PLAYBACK', `Loaded event with ID ${nextEvent.id}`);
|
logger.info('PLAYBACK', `Loaded event with ID ${nextEvent.id}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -120,10 +119,10 @@ export class PlaybackService {
|
|||||||
* Starts playback on selected event
|
* Starts playback on selected event
|
||||||
*/
|
*/
|
||||||
static start() {
|
static start() {
|
||||||
if (eventLoader.selectedEventId) {
|
if (eventTimer.playback === Playback.Armed || eventTimer.playback === Playback.Pause) {
|
||||||
eventTimer.start();
|
eventTimer.start();
|
||||||
const newState = eventTimer.playback;
|
const newState = eventTimer.playback;
|
||||||
socketProvider.info('PLAYBACK', `Play Mode ${newState.toUpperCase()}`);
|
logger.info('PLAYBACK', `Play Mode ${newState.toUpperCase()}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -131,10 +130,10 @@ export class PlaybackService {
|
|||||||
* Pauses playback on selected event
|
* Pauses playback on selected event
|
||||||
*/
|
*/
|
||||||
static pause() {
|
static pause() {
|
||||||
if (eventLoader.selectedEventId) {
|
if (eventTimer.playback === Playback.Play) {
|
||||||
eventTimer.pause();
|
eventTimer.pause();
|
||||||
const newState = eventTimer.playback;
|
const newState = eventTimer.playback;
|
||||||
socketProvider.info('PLAYBACK', `Play Mode ${newState.toUpperCase()}`);
|
logger.info('PLAYBACK', `Play Mode ${newState.toUpperCase()}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -142,11 +141,11 @@ export class PlaybackService {
|
|||||||
* Stops timer and unloads any events
|
* Stops timer and unloads any events
|
||||||
*/
|
*/
|
||||||
static stop() {
|
static stop() {
|
||||||
if (eventLoader.selectedEventId || eventTimer.playback === 'roll') {
|
if (eventTimer.playback !== Playback.Stop) {
|
||||||
eventLoader.reset();
|
eventLoader.reset();
|
||||||
eventTimer.stop();
|
eventTimer.stop();
|
||||||
const newState = eventTimer.playback;
|
const newState = eventTimer.playback;
|
||||||
socketProvider.info('PLAYBACK', `Play Mode ${newState.toUpperCase()}`);
|
logger.info('PLAYBACK', `Play Mode ${newState.toUpperCase()}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -154,8 +153,8 @@ export class PlaybackService {
|
|||||||
* Reloads current event
|
* Reloads current event
|
||||||
*/
|
*/
|
||||||
static reload() {
|
static reload() {
|
||||||
if (eventLoader.selectedEventId) {
|
if (eventTimer.loadedTimerId) {
|
||||||
this.loadById(eventLoader.selectedEventId);
|
this.loadById(eventTimer.loadedTimerId);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -168,14 +167,14 @@ export class PlaybackService {
|
|||||||
|
|
||||||
// nothing to play
|
// nothing to play
|
||||||
if (rollTimers === null) {
|
if (rollTimers === null) {
|
||||||
socketProvider.error('SERVER', 'Roll: no events found');
|
logger.warning('SERVER', 'Roll: no events found');
|
||||||
PlaybackService.stop();
|
PlaybackService.stop();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const { currentEvent, nextEvent, timers } = rollTimers;
|
const { currentEvent, nextEvent, timers } = rollTimers;
|
||||||
if (!currentEvent && !nextEvent) {
|
if (!currentEvent && !nextEvent) {
|
||||||
socketProvider.error('SERVER', 'Roll: no events found');
|
logger.warning('SERVER', 'Roll: no events found');
|
||||||
PlaybackService.stop();
|
PlaybackService.stop();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -183,7 +182,7 @@ export class PlaybackService {
|
|||||||
eventTimer.roll(currentEvent, nextEvent, timers);
|
eventTimer.roll(currentEvent, nextEvent, timers);
|
||||||
|
|
||||||
const newState = eventTimer.playback;
|
const newState = eventTimer.playback;
|
||||||
socketProvider.info('PLAYBACK', `Play Mode ${newState.toUpperCase()}`);
|
logger.info('PLAYBACK', `Play Mode ${newState.toUpperCase()}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -192,10 +191,10 @@ export class PlaybackService {
|
|||||||
* @param {number} delayTime time in minutes
|
* @param {number} delayTime time in minutes
|
||||||
*/
|
*/
|
||||||
static setDelay(delayTime) {
|
static setDelay(delayTime) {
|
||||||
if (eventLoader.selectedEventId) {
|
if (eventTimer.loadedTimerId) {
|
||||||
const delayInMs = delayTime * 1000 * 60;
|
const delayInMs = delayTime * 1000 * 60;
|
||||||
eventTimer.delay(delayInMs);
|
eventTimer.delay(delayInMs);
|
||||||
socketProvider.info('PLAYBACK', `Added ${delayTime} min delay`);
|
logger.info('PLAYBACK', `Added ${delayTime} min delay`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -5,16 +5,15 @@ import { block as blockDef, delay as delayDef, event as eventDef } from '../mode
|
|||||||
import { MAX_EVENTS } from '../settings.js';
|
import { MAX_EVENTS } from '../settings.js';
|
||||||
import { EventLoader, eventLoader } from '../classes/event-loader/EventLoader.js';
|
import { EventLoader, eventLoader } from '../classes/event-loader/EventLoader.js';
|
||||||
import { eventTimer } from './TimerService.js';
|
import { eventTimer } from './TimerService.js';
|
||||||
import { eventStore } from '../stores/EventStore.js';
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Checks if a list of IDs is in the current selection
|
* Checks if a list of IDs is in the current selection
|
||||||
*/
|
*/
|
||||||
const affectedLoaded = (affectedIds: string[]) => {
|
const affectedLoaded = (affectedIds: string[]) => {
|
||||||
const now = eventLoader.selectedEventId;
|
const now = eventLoader.loaded.selectedEventId;
|
||||||
const nowPublic = eventLoader.selectedPublicEventId;
|
const nowPublic = eventLoader.loaded.selectedPublicEventId;
|
||||||
const next = eventLoader.nextEventId;
|
const next = eventLoader.loaded.nextEventId;
|
||||||
const nextPublic = eventLoader.nextPublicEventId;
|
const nextPublic = eventLoader.loaded.nextPublicEventId;
|
||||||
return (
|
return (
|
||||||
affectedIds.includes(now) ||
|
affectedIds.includes(now) ||
|
||||||
affectedIds.includes(nowPublic) ||
|
affectedIds.includes(nowPublic) ||
|
||||||
@@ -28,8 +27,8 @@ const affectedLoaded = (affectedIds: string[]) => {
|
|||||||
*/
|
*/
|
||||||
const isNewNext = () => {
|
const isNewNext = () => {
|
||||||
const timedEvents = EventLoader.getTimedEvents();
|
const timedEvents = EventLoader.getTimedEvents();
|
||||||
const now = eventLoader.selectedEventId;
|
const now = eventLoader.loaded.selectedEventId;
|
||||||
const next = eventLoader.nextEventId;
|
const next = eventLoader.loaded.nextEventId;
|
||||||
|
|
||||||
// check whether the index of now and next are consecutive
|
// check whether the index of now and next are consecutive
|
||||||
const indexNow = timedEvents.findIndex((event) => event.id === now);
|
const indexNow = timedEvents.findIndex((event) => event.id === now);
|
||||||
@@ -39,8 +38,8 @@ const isNewNext = () => {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
// iterate through timed events and see if there are public events between nowPublic and nextPublic
|
// iterate through timed events and see if there are public events between nowPublic and nextPublic
|
||||||
const nowPublic = eventLoader.selectedPublicEventId;
|
const nowPublic = eventLoader.loaded.selectedPublicEventId;
|
||||||
const nextPublic = eventLoader.nextPublicEventId;
|
const nextPublic = eventLoader.loaded.nextPublicEventId;
|
||||||
|
|
||||||
let foundNew = false;
|
let foundNew = false;
|
||||||
let isAfter = false;
|
let isAfter = false;
|
||||||
@@ -67,7 +66,7 @@ const isNewNext = () => {
|
|||||||
* Updates timer object
|
* Updates timer object
|
||||||
*/
|
*/
|
||||||
export function updateTimer(affectedIds?: string[]) {
|
export function updateTimer(affectedIds?: string[]) {
|
||||||
const runningEventId = eventLoader.selectedEventId;
|
const runningEventId = eventLoader.loaded.selectedEventId;
|
||||||
|
|
||||||
if (runningEventId === null) {
|
if (runningEventId === null) {
|
||||||
return false;
|
return false;
|
||||||
@@ -112,7 +111,7 @@ export function updateTimer(affectedIds?: string[]) {
|
|||||||
* @param {object} eventData
|
* @param {object} eventData
|
||||||
* @return {unknown[]}
|
* @return {unknown[]}
|
||||||
*/
|
*/
|
||||||
export async function addEvent(eventData) {
|
export async function addEvent(eventData: Partial<OntimeEvent> | Partial<OntimeDelay> | Partial<OntimeBlock>) {
|
||||||
const numEvents = DataProvider.getRundownLength();
|
const numEvents = DataProvider.getRundownLength();
|
||||||
if (numEvents > MAX_EVENTS) {
|
if (numEvents > MAX_EVENTS) {
|
||||||
throw new Error(`ERROR: Reached limit number of ${MAX_EVENTS} events`);
|
throw new Error(`ERROR: Reached limit number of ${MAX_EVENTS} events`);
|
||||||
@@ -145,7 +144,7 @@ export async function addEvent(eventData) {
|
|||||||
throw new Error(error);
|
throw new Error(error);
|
||||||
}
|
}
|
||||||
updateTimer([id]);
|
updateTimer([id]);
|
||||||
eventStore.broadcast();
|
updateChangeNumEvents();
|
||||||
return newEvent;
|
return newEvent;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -157,7 +156,6 @@ export async function editEvent(eventData) {
|
|||||||
}
|
}
|
||||||
const newEvent = await DataProvider.updateEventById(eventId, eventData);
|
const newEvent = await DataProvider.updateEventById(eventId, eventData);
|
||||||
updateTimer([eventId]);
|
updateTimer([eventId]);
|
||||||
eventStore.broadcast();
|
|
||||||
return newEvent;
|
return newEvent;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -169,7 +167,7 @@ export async function editEvent(eventData) {
|
|||||||
export async function deleteEvent(eventId) {
|
export async function deleteEvent(eventId) {
|
||||||
await DataProvider.deleteEvent(eventId);
|
await DataProvider.deleteEvent(eventId);
|
||||||
updateTimer([eventId]);
|
updateTimer([eventId]);
|
||||||
eventStore.broadcast();
|
updateChangeNumEvents();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -179,7 +177,7 @@ export async function deleteEvent(eventId) {
|
|||||||
export async function deleteAllEvents() {
|
export async function deleteAllEvents() {
|
||||||
await DataProvider.clearRundown();
|
await DataProvider.clearRundown();
|
||||||
updateTimer();
|
updateTimer();
|
||||||
eventStore.broadcast();
|
updateChangeNumEvents();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -204,7 +202,6 @@ export async function reorderEvent(eventId, from, to) {
|
|||||||
// save rundown
|
// save rundown
|
||||||
await DataProvider.setRundown(rundown);
|
await DataProvider.setRundown(rundown);
|
||||||
updateTimer();
|
updateTimer();
|
||||||
|
|
||||||
return reorderedItem;
|
return reorderedItem;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -260,5 +257,12 @@ export async function applyDelay(eventId) {
|
|||||||
// update rundown
|
// update rundown
|
||||||
await DataProvider.setRundown(rundown);
|
await DataProvider.setRundown(rundown);
|
||||||
updateTimer();
|
updateTimer();
|
||||||
eventStore.broadcast();
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Forces update in the store
|
||||||
|
* Called when we make changes to the rundown object
|
||||||
|
*/
|
||||||
|
function updateChangeNumEvents() {
|
||||||
|
eventLoader.updateNumEvents();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { TimerLifeCycle, TimerType } from 'ontime-types';
|
import { Playback, TimerLifeCycle, TimerState } from 'ontime-types';
|
||||||
|
|
||||||
import { eventStore } from '../stores/EventStore.js';
|
import { eventStore } from '../stores/EventStore.js';
|
||||||
import { PlaybackService } from './PlaybackService.js';
|
import { PlaybackService } from './PlaybackService.js';
|
||||||
@@ -11,27 +11,14 @@ import { clock } from './Clock.js';
|
|||||||
export class TimerService {
|
export class TimerService {
|
||||||
private readonly _interval: NodeJS.Timer;
|
private readonly _interval: NodeJS.Timer;
|
||||||
|
|
||||||
playback: string;
|
playback: Playback;
|
||||||
|
timer: TimerState;
|
||||||
|
|
||||||
loadedTimerId: null;
|
loadedTimerId: null;
|
||||||
private pausedTime: number;
|
private pausedTime: number;
|
||||||
private pausedAt: number | null;
|
private pausedAt: number | null;
|
||||||
private secondaryTarget: number | null;
|
private secondaryTarget: number | null;
|
||||||
|
|
||||||
timer: {
|
|
||||||
clock: number; // realtime clock
|
|
||||||
current: number | null; // running countdown
|
|
||||||
elapsed: number | null; // elapsed time in current timer
|
|
||||||
expectedFinish: number | null;
|
|
||||||
addedTime: number; // time added by user, can be negative
|
|
||||||
startedAt: number | null;
|
|
||||||
finishedAt: number | null; // only if timer has already finished
|
|
||||||
secondaryTimer: number | null; // used for roll mode
|
|
||||||
selectedEventId: string | null;
|
|
||||||
duration: number | null;
|
|
||||||
timerType: TimerType | null;
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @constructor
|
* @constructor
|
||||||
* @param {object} [timerConfig]
|
* @param {object} [timerConfig]
|
||||||
@@ -47,7 +34,7 @@ export class TimerService {
|
|||||||
* @private
|
* @private
|
||||||
*/
|
*/
|
||||||
_clear() {
|
_clear() {
|
||||||
this.playback = 'stop';
|
this.playback = Playback.Stop;
|
||||||
this.timer = {
|
this.timer = {
|
||||||
clock: clock.timeNow(),
|
clock: clock.timeNow(),
|
||||||
current: null,
|
current: null,
|
||||||
@@ -128,7 +115,7 @@ export class TimerService {
|
|||||||
this.loadedTimerId = timer.id;
|
this.loadedTimerId = timer.id;
|
||||||
this.timer.duration = timer.duration;
|
this.timer.duration = timer.duration;
|
||||||
this.timer.current = timer.duration;
|
this.timer.current = timer.duration;
|
||||||
this.playback = 'armed';
|
this.playback = Playback.Armed;
|
||||||
this.timer.timerType = timer.timerType;
|
this.timer.timerType = timer.timerType;
|
||||||
this.pausedTime = 0;
|
this.pausedTime = 0;
|
||||||
this.pausedAt = 0;
|
this.pausedAt = 0;
|
||||||
@@ -151,7 +138,7 @@ export class TimerService {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.playback === 'play') {
|
if (this.playback === Playback.Play) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -166,7 +153,7 @@ export class TimerService {
|
|||||||
this.timer.startedAt = this.timer.clock;
|
this.timer.startedAt = this.timer.clock;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.playback = 'play';
|
this.playback = Playback.Play;
|
||||||
this.timer.expectedFinish = getExpectedFinish(
|
this.timer.expectedFinish = getExpectedFinish(
|
||||||
this.timer.startedAt,
|
this.timer.startedAt,
|
||||||
this.timer.finishedAt,
|
this.timer.finishedAt,
|
||||||
@@ -188,11 +175,11 @@ export class TimerService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pause() {
|
pause() {
|
||||||
if (this.playback !== 'play') {
|
if (this.playback !== Playback.Play) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.playback = 'pause';
|
this.playback = Playback.Pause;
|
||||||
this.timer.clock = clock.timeNow();
|
this.timer.clock = clock.timeNow();
|
||||||
this.pausedAt = this.timer.clock;
|
this.pausedAt = this.timer.clock;
|
||||||
this._onPause();
|
this._onPause();
|
||||||
@@ -205,7 +192,7 @@ export class TimerService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
stop() {
|
stop() {
|
||||||
if (this.playback === 'stop') {
|
if (this.playback === Playback.Stop) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -248,7 +235,7 @@ export class TimerService {
|
|||||||
update() {
|
update() {
|
||||||
this.timer.clock = clock.timeNow();
|
this.timer.clock = clock.timeNow();
|
||||||
|
|
||||||
if (this.playback === 'roll') {
|
if (this.playback === Playback.Roll) {
|
||||||
const tempCurrentTimer = {
|
const tempCurrentTimer = {
|
||||||
selectedEventId: this.loadedTimerId,
|
selectedEventId: this.loadedTimerId,
|
||||||
current: this.timer.current,
|
current: this.timer.current,
|
||||||
@@ -279,11 +266,11 @@ export class TimerService {
|
|||||||
} else {
|
} else {
|
||||||
// we only update timer if a timer has been started
|
// we only update timer if a timer has been started
|
||||||
if (this.timer.startedAt !== null) {
|
if (this.timer.startedAt !== null) {
|
||||||
if (this.playback === 'pause') {
|
if (this.playback === Playback.Pause) {
|
||||||
this.pausedTime = this.timer.clock - this.pausedAt;
|
this.pausedTime = this.timer.clock - this.pausedAt;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.playback === 'play' && this.timer.current <= 0 && this.timer.finishedAt === null) {
|
if (this.playback === Playback.Play && this.timer.current <= 0 && this.timer.finishedAt === null) {
|
||||||
this.timer.finishedAt = this.timer.clock;
|
this.timer.finishedAt = this.timer.clock;
|
||||||
this._onFinish();
|
this._onFinish();
|
||||||
} else {
|
} else {
|
||||||
@@ -338,7 +325,7 @@ export class TimerService {
|
|||||||
this.secondaryTarget = nextEvent.timeStart;
|
this.secondaryTarget = nextEvent.timeStart;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.playback = 'roll';
|
this.playback = Playback.Roll;
|
||||||
this._onRoll();
|
this._onRoll();
|
||||||
this.update();
|
this.update();
|
||||||
}
|
}
|
||||||
|
|||||||
+42
-32
@@ -1,11 +1,13 @@
|
|||||||
import { MessageControl } from 'ontime-types';
|
import { Message } from 'ontime-types';
|
||||||
|
|
||||||
import { eventStore } from '../../stores/EventStore.js';
|
import { eventStore } from '../../stores/EventStore.js';
|
||||||
|
|
||||||
let instance;
|
let instance;
|
||||||
|
|
||||||
class MessageService {
|
class MessageService {
|
||||||
messages: MessageControl;
|
timerMessage: Message;
|
||||||
|
publicMessage: Message;
|
||||||
|
lowerMessage: Message;
|
||||||
onAir: boolean;
|
onAir: boolean;
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
@@ -16,20 +18,21 @@ class MessageService {
|
|||||||
// eslint-disable-next-line @typescript-eslint/no-this-alias -- this logic is used to ensure singleton
|
// eslint-disable-next-line @typescript-eslint/no-this-alias -- this logic is used to ensure singleton
|
||||||
instance = this;
|
instance = this;
|
||||||
|
|
||||||
this.messages = {
|
this.timerMessage = {
|
||||||
presenter: {
|
text: '',
|
||||||
text: '',
|
visible: false,
|
||||||
visible: false,
|
|
||||||
},
|
|
||||||
public: {
|
|
||||||
text: '',
|
|
||||||
visible: false,
|
|
||||||
},
|
|
||||||
lower: {
|
|
||||||
text: '',
|
|
||||||
visible: false,
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
this.publicMessage = {
|
||||||
|
text: '',
|
||||||
|
visible: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
this.lowerMessage = {
|
||||||
|
text: '',
|
||||||
|
visible: false,
|
||||||
|
};
|
||||||
|
|
||||||
this.onAir = false;
|
this.onAir = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -37,8 +40,8 @@ class MessageService {
|
|||||||
* @description sets message on stage timer screen
|
* @description sets message on stage timer screen
|
||||||
*/
|
*/
|
||||||
setTimerText(payload: string) {
|
setTimerText(payload: string) {
|
||||||
this.messages.presenter.text = payload;
|
this.timerMessage.text = payload;
|
||||||
eventStore.set('feat-messagecontrol', { messages: this.messages });
|
eventStore.set('timerMessage', this.timerMessage);
|
||||||
return this.getAll();
|
return this.getAll();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -46,8 +49,8 @@ class MessageService {
|
|||||||
* @description sets message visibility on stage timer screen
|
* @description sets message visibility on stage timer screen
|
||||||
*/
|
*/
|
||||||
setTimerVisibility(status: boolean) {
|
setTimerVisibility(status: boolean) {
|
||||||
this.messages.presenter.visible = status;
|
this.timerMessage.visible = status;
|
||||||
eventStore.set('feat-messagecontrol', { messages: this.messages });
|
eventStore.set('timerMessage', this.timerMessage);
|
||||||
return this.getAll();
|
return this.getAll();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -55,8 +58,8 @@ class MessageService {
|
|||||||
* @description sets message on public screen
|
* @description sets message on public screen
|
||||||
*/
|
*/
|
||||||
setPublicText(payload: string) {
|
setPublicText(payload: string) {
|
||||||
this.messages.public.text = payload;
|
this.publicMessage.text = payload;
|
||||||
eventStore.set('feat-messagecontrol', { messages: this.messages });
|
eventStore.set('publicMessage', this.publicMessage);
|
||||||
return this.getAll();
|
return this.getAll();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -64,8 +67,8 @@ class MessageService {
|
|||||||
* @description sets message visibility on public screen
|
* @description sets message visibility on public screen
|
||||||
*/
|
*/
|
||||||
setPublicVisibility(status: boolean) {
|
setPublicVisibility(status: boolean) {
|
||||||
this.messages.public.visible = status;
|
this.publicMessage.visible = status;
|
||||||
eventStore.set('feat-messagecontrol', { messages: this.messages });
|
eventStore.set('publicMessage', this.publicMessage);
|
||||||
return this.getAll();
|
return this.getAll();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -73,8 +76,8 @@ class MessageService {
|
|||||||
* @description sets message on lower third screen
|
* @description sets message on lower third screen
|
||||||
*/
|
*/
|
||||||
setLowerText(payload: string) {
|
setLowerText(payload: string) {
|
||||||
this.messages.lower.text = payload;
|
this.lowerMessage.text = payload;
|
||||||
eventStore.set('feat-messagecontrol', { messages: this.messages });
|
eventStore.set('lowerMessage', this.lowerMessage);
|
||||||
return this.getAll();
|
return this.getAll();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -82,16 +85,21 @@ class MessageService {
|
|||||||
* @description sets message visibility on lower third screen
|
* @description sets message visibility on lower third screen
|
||||||
*/
|
*/
|
||||||
setLowerVisibility(status: boolean) {
|
setLowerVisibility(status: boolean) {
|
||||||
this.messages.lower.visible = status;
|
this.lowerMessage.visible = status;
|
||||||
eventStore.set('feat-messagecontrol', { messages: this.messages });
|
eventStore.set('lowerMessage', this.lowerMessage);
|
||||||
return this.getAll();
|
return this.getAll();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @description set state of onAir
|
* @description set state of onAir, toggles if parameters are offered
|
||||||
*/
|
*/
|
||||||
setOnAir(status: boolean) {
|
setOnAir(status?: boolean) {
|
||||||
this.onAir = status;
|
if (typeof status === 'undefined') {
|
||||||
|
this.onAir = !this.onAir;
|
||||||
|
} else {
|
||||||
|
this.onAir = status;
|
||||||
|
}
|
||||||
|
eventStore.set('onAir', this.onAir);
|
||||||
return this.getAll();
|
return this.getAll();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -100,10 +108,12 @@ class MessageService {
|
|||||||
*/
|
*/
|
||||||
getAll() {
|
getAll() {
|
||||||
return {
|
return {
|
||||||
messages: this.messages,
|
timerMessage: this.timerMessage,
|
||||||
|
publicMessage: this.publicMessage,
|
||||||
|
lowerMessage: this.lowerMessage,
|
||||||
onAir: this.onAir,
|
onAir: this.onAir,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const messageManager = new MessageService();
|
export const messageService = new MessageService();
|
||||||
@@ -1,23 +0,0 @@
|
|||||||
import { socketProvider } from '../classes/socket/SocketController.js';
|
|
||||||
|
|
||||||
const store = {};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A runtime store that broadcasts its payload
|
|
||||||
*/
|
|
||||||
export const eventStore = {
|
|
||||||
get(key) {
|
|
||||||
return store[key];
|
|
||||||
},
|
|
||||||
set(key, value) {
|
|
||||||
store[key] = value;
|
|
||||||
socketProvider.send(key, value);
|
|
||||||
},
|
|
||||||
poll() {
|
|
||||||
return store;
|
|
||||||
},
|
|
||||||
broadcast() {
|
|
||||||
socketProvider.send(store);
|
|
||||||
socketProvider.broadcastState();
|
|
||||||
},
|
|
||||||
};
|
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
import { RuntimeStore } from 'ontime-types';
|
||||||
|
import { socket } from '../adapters/WebsocketAdapter.js';
|
||||||
|
|
||||||
|
const store: Partial<RuntimeStore> = {};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A runtime store that broadcasts its payload
|
||||||
|
*/
|
||||||
|
export const eventStore = {
|
||||||
|
get<T extends keyof RuntimeStore>(key: T) {
|
||||||
|
return store[key];
|
||||||
|
},
|
||||||
|
set<T extends keyof RuntimeStore>(key: T, value: RuntimeStore[T]) {
|
||||||
|
store[key] = value;
|
||||||
|
// TODO: Partial updates seems to cause issues on the client
|
||||||
|
// socket.send({
|
||||||
|
// type: `ontime-${key}`,
|
||||||
|
// payload: value,
|
||||||
|
// });
|
||||||
|
this.broadcast();
|
||||||
|
},
|
||||||
|
poll() {
|
||||||
|
return store;
|
||||||
|
},
|
||||||
|
broadcast() {
|
||||||
|
socket.send({
|
||||||
|
type: 'ontime',
|
||||||
|
payload: store,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
};
|
||||||
@@ -26,37 +26,6 @@ export const isTimeString = (string) => {
|
|||||||
return regex.test(string);
|
return regex.test(string);
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
|
||||||
* @description Converts milliseconds to string representing time
|
|
||||||
* @param {number} ms - time in milliseconds
|
|
||||||
* @param {boolean} showSeconds - weather to show the seconds
|
|
||||||
* @param {string} delim - character between HH MM SS
|
|
||||||
* @param {string} ifNull - what to return if value is null
|
|
||||||
* @returns {string} String representing time 00:12:02
|
|
||||||
*/
|
|
||||||
|
|
||||||
export const stringFromMillis = (ms, showSeconds = true, delim = ':', ifNull = '...') => {
|
|
||||||
if (ms == null || isNaN(ms)) return ifNull;
|
|
||||||
const isNegative = ms < 0 ? '-' : '';
|
|
||||||
const millis = Math.abs(ms);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @description ensures value is double digit
|
|
||||||
* @param value
|
|
||||||
* @return {string|*}
|
|
||||||
*/
|
|
||||||
const showWith0 = (value) => (value < 10 ? `0${value}` : value);
|
|
||||||
const hours = showWith0(Math.floor(((millis / mth) % 60) % 24));
|
|
||||||
const minutes = showWith0(Math.floor((millis / mtm) % 60));
|
|
||||||
const seconds = showWith0(Math.floor((millis / mts) % 60));
|
|
||||||
|
|
||||||
return showSeconds
|
|
||||||
? `${isNegative}${
|
|
||||||
parseInt(hours, 10) ? `${hours}${delim}` : `00${delim}`
|
|
||||||
}${minutes}${delim}${seconds}`
|
|
||||||
: `${isNegative}${parseInt(hours, 10) ? `${hours}` : '00'}${delim}${minutes}`;
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @description Converts an excel date to milliseconds
|
* @description Converts an excel date to milliseconds
|
||||||
* @argument {string} date - excel string date
|
* @argument {string} date - excel string date
|
||||||
|
|||||||
@@ -0,0 +1,18 @@
|
|||||||
|
export enum LogLevel {
|
||||||
|
Info = 'INFO',
|
||||||
|
Warn = 'WARN',
|
||||||
|
Error = 'ERROR',
|
||||||
|
}
|
||||||
|
|
||||||
|
export type Log = {
|
||||||
|
id: string;
|
||||||
|
origin: string;
|
||||||
|
time: string;
|
||||||
|
level: LogLevel;
|
||||||
|
text: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type LogMessage = {
|
||||||
|
type: 'ontime-log';
|
||||||
|
payload: Log;
|
||||||
|
};
|
||||||
@@ -2,9 +2,3 @@ export type Message = {
|
|||||||
text: string;
|
text: string;
|
||||||
visible: boolean;
|
visible: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type MessageControl = {
|
|
||||||
presenter: Message;
|
|
||||||
public: Message;
|
|
||||||
lower: Message;
|
|
||||||
};
|
|
||||||
|
|||||||
@@ -1 +1,7 @@
|
|||||||
export type Playback = 'roll' | 'play' | 'pause' | 'stop' | 'armed';
|
export enum Playback {
|
||||||
|
Roll = 'roll',
|
||||||
|
Play = 'play',
|
||||||
|
Pause = 'pause',
|
||||||
|
Stop = 'stop',
|
||||||
|
Armed = 'armed',
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,8 @@
|
|||||||
|
export type Loaded = {
|
||||||
|
numEvents: number;
|
||||||
|
selectedEventIndex: number | null;
|
||||||
|
selectedEventId: string | null;
|
||||||
|
selectedPublicEventId: string | null;
|
||||||
|
nextEventId: string | null;
|
||||||
|
nextPublicEventId: string | null;
|
||||||
|
};
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
import { Playback } from './Playback.type.js';
|
||||||
|
import { Message } from './MessageControl.type.js';
|
||||||
|
import { TimerState } from './TimerState.type.js';
|
||||||
|
import { TitleBlock } from './TitleBlock.type.js';
|
||||||
|
import { Loaded } from './Playlist.type.js';
|
||||||
|
|
||||||
|
export type RuntimeStore = {
|
||||||
|
// timer service
|
||||||
|
timer: TimerState;
|
||||||
|
playback: Playback;
|
||||||
|
|
||||||
|
// messages service
|
||||||
|
timerMessage: Message;
|
||||||
|
publicMessage: Message;
|
||||||
|
lowerMessage: Message;
|
||||||
|
onAir: boolean;
|
||||||
|
|
||||||
|
// event loader
|
||||||
|
loaded: Loaded;
|
||||||
|
titles: TitleBlock;
|
||||||
|
titlesPublic: TitleBlock;
|
||||||
|
};
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
import { TimerType } from '../TimerType.type.js';
|
||||||
|
|
||||||
|
export type TimerState = {
|
||||||
|
clock: number; // realtime clock
|
||||||
|
current: number | null; // running countdown
|
||||||
|
elapsed: number | null; // elapsed time in current timer
|
||||||
|
expectedFinish: number | null;
|
||||||
|
addedTime: number; // time added by user, can be negative
|
||||||
|
startedAt: number | null;
|
||||||
|
finishedAt: number | null; // only if timer has already finished
|
||||||
|
secondaryTimer: number | null; // used for roll mode
|
||||||
|
selectedEventId: string | null;
|
||||||
|
duration: number | null;
|
||||||
|
timerType: TimerType | null;
|
||||||
|
};
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
export type TitleBlock = {
|
||||||
|
titleNow: string | null;
|
||||||
|
subtitleNow: string | null;
|
||||||
|
presenterNow: string | null;
|
||||||
|
noteNow: string | null;
|
||||||
|
titleNext: string | null;
|
||||||
|
subtitleNext: string | null;
|
||||||
|
presenterNext: string | null;
|
||||||
|
noteNext: string | null;
|
||||||
|
};
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
import { Alias } from './definitions/core/Alias.type.js';
|
import { Alias } from './definitions/core/Alias.type.js';
|
||||||
import { DatabaseModel } from './definitions/DataModel.type.js';
|
import { DatabaseModel } from './definitions/DataModel.type.js';
|
||||||
import { EventData } from './definitions/core/EventData.type.js';
|
import { EventData } from './definitions/core/EventData.type.js';
|
||||||
import { Message, MessageControl } from './definitions/runtime/MessageControl.type.js';
|
import { Message } from './definitions/runtime/MessageControl.type.js';
|
||||||
import {
|
import {
|
||||||
OntimeBaseEvent,
|
OntimeBaseEvent,
|
||||||
OntimeBlock,
|
OntimeBlock,
|
||||||
@@ -12,9 +12,14 @@ import {
|
|||||||
import { OntimeRundown, OntimeRundownEntry } from './definitions/core/Rundown.type.js';
|
import { OntimeRundown, OntimeRundownEntry } from './definitions/core/Rundown.type.js';
|
||||||
import { OSCSettings, OscSubscription } from './definitions/core/OscSettings.type.js';
|
import { OSCSettings, OscSubscription } from './definitions/core/OscSettings.type.js';
|
||||||
import { Playback } from './definitions/runtime/Playback.type.js';
|
import { Playback } from './definitions/runtime/Playback.type.js';
|
||||||
|
import { Loaded } from './definitions/runtime/Playlist.type.js';
|
||||||
|
import { Log, LogLevel, LogMessage } from './definitions/runtime/Logger.type.js';
|
||||||
|
import { RuntimeStore } from './definitions/runtime/RuntimeStore.type.js';
|
||||||
import { Settings } from './definitions/core/Settings.type.js';
|
import { Settings } from './definitions/core/Settings.type.js';
|
||||||
import { TimerLifeCycle } from './definitions/core/TimerLifecycle.type.js';
|
import { TimerLifeCycle } from './definitions/core/TimerLifecycle.type.js';
|
||||||
|
import { TimerState } from './definitions/runtime/TimerState.type.js';
|
||||||
import { TimerType } from './definitions/TimerType.type.js';
|
import { TimerType } from './definitions/TimerType.type.js';
|
||||||
|
import { TitleBlock } from './definitions/runtime/TitleBlock.type.js';
|
||||||
import { UserFields } from './definitions/core/UserFields.type.js';
|
import { UserFields } from './definitions/core/UserFields.type.js';
|
||||||
import { ViewSettings } from './definitions/core/Views.type.js';
|
import { ViewSettings } from './definitions/core/Views.type.js';
|
||||||
|
|
||||||
@@ -47,10 +52,16 @@ export type { OscSubscription, OSCSettings };
|
|||||||
|
|
||||||
// ---> HTTP
|
// ---> HTTP
|
||||||
|
|
||||||
// SERVER
|
// SERVER RUNTIME
|
||||||
|
export { LogLevel };
|
||||||
|
export type { Log, LogMessage };
|
||||||
|
export { Playback };
|
||||||
export { TimerLifeCycle };
|
export { TimerLifeCycle };
|
||||||
export type { Playback };
|
|
||||||
export type { Message };
|
export type { Message };
|
||||||
export type { MessageControl };
|
export type { Loaded };
|
||||||
|
export type { RuntimeStore };
|
||||||
|
export type { TimerState };
|
||||||
|
export type { TitleBlock };
|
||||||
|
|
||||||
// CLIENT
|
// CLIENT
|
||||||
|
|||||||
@@ -1 +1,2 @@
|
|||||||
|
export { millisToString } from './src/date-utils/millisToString.js';
|
||||||
export { generateId } from './src/generate-id/generateId.js';
|
export { generateId } from './src/generate-id/generateId.js';
|
||||||
|
|||||||
@@ -12,9 +12,11 @@
|
|||||||
"cleanup": "rm -rf .turbo && rm -rf node_modules"
|
"cleanup": "rm -rf .turbo && rm -rf node_modules"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"luxon": "^3.3.0",
|
||||||
"nanoid": "^4.0.0"
|
"nanoid": "^4.0.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@types/luxon": "^3.2.0",
|
||||||
"@typescript-eslint/eslint-plugin": "^5.48.1",
|
"@typescript-eslint/eslint-plugin": "^5.48.1",
|
||||||
"@typescript-eslint/parser": "^5.48.1",
|
"@typescript-eslint/parser": "^5.48.1",
|
||||||
"eslint": "^8.31.0",
|
"eslint": "^8.31.0",
|
||||||
|
|||||||
@@ -0,0 +1,71 @@
|
|||||||
|
import { expect } from 'vitest';
|
||||||
|
|
||||||
|
import { millisToString } from './millisToString';
|
||||||
|
|
||||||
|
describe('millisToString()', () => {
|
||||||
|
it('returns fallback if millis is null', () => {
|
||||||
|
const fallback = 'testFallback';
|
||||||
|
expect(millisToString(null, true, fallback)).toBe(fallback);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns 00:00:00 if 0 is passed', () => {
|
||||||
|
expect(millisToString(0)).toBe('00:00:00');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('shows negative timers', () => {
|
||||||
|
const testScenarios = [
|
||||||
|
{ millis: -300, expected: '-00:00:00' },
|
||||||
|
{ millis: -1000, expected: '-00:00:01' },
|
||||||
|
{ millis: -1500, expected: '-00:00:01' },
|
||||||
|
{ millis: -60000, expected: '-00:01:00' },
|
||||||
|
{ millis: -600000, expected: '-00:10:00' },
|
||||||
|
{ millis: -3600000, expected: '-01:00:00' },
|
||||||
|
{ millis: -36000000, expected: '-10:00:00' },
|
||||||
|
{ millis: -86399000, expected: '-23:59:59' },
|
||||||
|
{ millis: -86400000, expected: '-00:00:00' },
|
||||||
|
{ millis: -86401000, expected: '-00:00:01' },
|
||||||
|
];
|
||||||
|
|
||||||
|
testScenarios.forEach((scenario) => {
|
||||||
|
expect(millisToString(scenario.millis)).toBe(scenario.expected);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test('random properties', () => {
|
||||||
|
const testScenarios = [
|
||||||
|
{ millis: 300, expected: '00:00:00' },
|
||||||
|
{ millis: 1000, expected: '00:00:01' },
|
||||||
|
{ millis: 1500, expected: '00:00:01' },
|
||||||
|
{ millis: 60000, expected: '00:01:00' },
|
||||||
|
{ millis: 600000, expected: '00:10:00' },
|
||||||
|
{ millis: 3600000, expected: '01:00:00' },
|
||||||
|
{ millis: 36000000, expected: '10:00:00' },
|
||||||
|
{ millis: 86399000, expected: '23:59:59' },
|
||||||
|
{ millis: 86400000, expected: '00:00:00' },
|
||||||
|
{ millis: 86401000, expected: '00:00:01' },
|
||||||
|
];
|
||||||
|
|
||||||
|
testScenarios.forEach((scenario) => {
|
||||||
|
expect(millisToString(scenario.millis)).toBe(scenario.expected);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test('random properties without seconds', () => {
|
||||||
|
const testScenarios = [
|
||||||
|
{ millis: 300, expected: '00:00' },
|
||||||
|
{ millis: 1000, expected: '00:00' },
|
||||||
|
{ millis: 1500, expected: '00:00' },
|
||||||
|
{ millis: 60000, expected: '00:01' },
|
||||||
|
{ millis: 600000, expected: '00:10' },
|
||||||
|
{ millis: 3600000, expected: '01:00' },
|
||||||
|
{ millis: 36000000, expected: '10:00' },
|
||||||
|
{ millis: 86399000, expected: '23:59' },
|
||||||
|
{ millis: 86400000, expected: '00:00' },
|
||||||
|
{ millis: 86401000, expected: '00:00' },
|
||||||
|
];
|
||||||
|
|
||||||
|
testScenarios.forEach((scenario) => {
|
||||||
|
expect(millisToString(scenario.millis, false)).toBe(scenario.expected);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
import { DateTime } from 'luxon';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @description Converts milliseconds to string representing time
|
||||||
|
* @param {number | null} millis - time in milliseconds
|
||||||
|
* @param {boolean} showSeconds - weather to show the seconds
|
||||||
|
* @param {string} fallback - what to return if value is null
|
||||||
|
* @returns {string} String representing time 00:12:02
|
||||||
|
*/
|
||||||
|
export function millisToString(millis: number | null, showSeconds = true, fallback = '...') {
|
||||||
|
if (millis === null) {
|
||||||
|
return fallback;
|
||||||
|
}
|
||||||
|
|
||||||
|
const isNegative = millis < 0;
|
||||||
|
|
||||||
|
const format = `HH:mm${showSeconds ? ':ss' : ''}`;
|
||||||
|
return `${isNegative ? '-' : ''}${DateTime.fromMillis(Math.abs(millis)).toUTC().toFormat(format)}`;
|
||||||
|
}
|
||||||
Generated
+65
-118
@@ -47,6 +47,7 @@ importers:
|
|||||||
'@testing-library/react': ^13.1.1
|
'@testing-library/react': ^13.1.1
|
||||||
'@testing-library/user-event': ^14.1.1
|
'@testing-library/user-event': ^14.1.1
|
||||||
'@types/color': ^3.0.3
|
'@types/color': ^3.0.3
|
||||||
|
'@types/luxon': ^3.2.0
|
||||||
'@types/prop-types': ^15.7.5
|
'@types/prop-types': ^15.7.5
|
||||||
'@types/react': ^18.0.26
|
'@types/react': ^18.0.26
|
||||||
'@types/react-beautiful-dnd': ^13.1.3
|
'@types/react-beautiful-dnd': ^13.1.3
|
||||||
@@ -59,6 +60,7 @@ importers:
|
|||||||
axios: ^1.2.0
|
axios: ^1.2.0
|
||||||
color: ^4.2.3
|
color: ^4.2.3
|
||||||
csv-stringify: ^6.2.3
|
csv-stringify: ^6.2.3
|
||||||
|
deepmerge: ^4.3.0
|
||||||
eslint: ^8.31.0
|
eslint: ^8.31.0
|
||||||
eslint-config-prettier: ^8.6.0
|
eslint-config-prettier: ^8.6.0
|
||||||
eslint-plugin-jest: ^27.1.7
|
eslint-plugin-jest: ^27.1.7
|
||||||
@@ -70,7 +72,7 @@ importers:
|
|||||||
framer-motion: ^8.0.2
|
framer-motion: ^8.0.2
|
||||||
jotai: ^1.10.0
|
jotai: ^1.10.0
|
||||||
jsdom: ^21.1.0
|
jsdom: ^21.1.0
|
||||||
luxon: ^3.1.0
|
luxon: ^3.3.0
|
||||||
ontime-types: workspace:*
|
ontime-types: workspace:*
|
||||||
ontime-utils: workspace:*
|
ontime-utils: workspace:*
|
||||||
prettier: ^2.8.3
|
prettier: ^2.8.3
|
||||||
@@ -83,8 +85,8 @@ importers:
|
|||||||
react-qr-code: ^2.0.11
|
react-qr-code: ^2.0.11
|
||||||
react-router-dom: ^6.3.0
|
react-router-dom: ^6.3.0
|
||||||
react-table: ^7.7.0
|
react-table: ^7.7.0
|
||||||
|
react-use-websocket: ^4.3.1
|
||||||
sass: ^1.57.1
|
sass: ^1.57.1
|
||||||
socket.io-client: ^4.5.4
|
|
||||||
stylelint: ^14.16.1
|
stylelint: ^14.16.1
|
||||||
stylelint-config-prettier: ^9.0.4
|
stylelint-config-prettier: ^9.0.4
|
||||||
stylelint-config-standard-scss: ^6.1.0
|
stylelint-config-standard-scss: ^6.1.0
|
||||||
@@ -94,6 +96,7 @@ importers:
|
|||||||
vite-plugin-svgr: ^2.4.0
|
vite-plugin-svgr: ^2.4.0
|
||||||
vite-tsconfig-paths: ^4.0.3
|
vite-tsconfig-paths: ^4.0.3
|
||||||
web-vitals: ^3.1.1
|
web-vitals: ^3.1.1
|
||||||
|
zustand: ^4.3.6
|
||||||
dependencies:
|
dependencies:
|
||||||
'@chakra-ui/react': 2.4.8_loo4skotrnm7icurwgkplqpnwq
|
'@chakra-ui/react': 2.4.8_loo4skotrnm7icurwgkplqpnwq
|
||||||
'@dnd-kit/core': 6.0.7_biqbaboplfbrettd7655fr4n2y
|
'@dnd-kit/core': 6.0.7_biqbaboplfbrettd7655fr4n2y
|
||||||
@@ -110,9 +113,10 @@ importers:
|
|||||||
axios: 1.2.2
|
axios: 1.2.2
|
||||||
color: 4.2.3
|
color: 4.2.3
|
||||||
csv-stringify: 6.2.3
|
csv-stringify: 6.2.3
|
||||||
|
deepmerge: 4.3.0
|
||||||
framer-motion: 8.4.3_biqbaboplfbrettd7655fr4n2y
|
framer-motion: 8.4.3_biqbaboplfbrettd7655fr4n2y
|
||||||
jotai: 1.13.0_react@18.2.0
|
jotai: 1.13.0_react@18.2.0
|
||||||
luxon: 3.2.1
|
luxon: 3.3.0
|
||||||
react: 18.2.0
|
react: 18.2.0
|
||||||
react-beautiful-dnd: 13.1.1_biqbaboplfbrettd7655fr4n2y
|
react-beautiful-dnd: 13.1.1_biqbaboplfbrettd7655fr4n2y
|
||||||
react-dom: 18.2.0_react@18.2.0
|
react-dom: 18.2.0_react@18.2.0
|
||||||
@@ -121,9 +125,10 @@ importers:
|
|||||||
react-qr-code: 2.0.11_react@18.2.0
|
react-qr-code: 2.0.11_react@18.2.0
|
||||||
react-router-dom: 6.6.2_biqbaboplfbrettd7655fr4n2y
|
react-router-dom: 6.6.2_biqbaboplfbrettd7655fr4n2y
|
||||||
react-table: 7.8.0_react@18.2.0
|
react-table: 7.8.0_react@18.2.0
|
||||||
socket.io-client: 4.5.4
|
react-use-websocket: 4.3.1_biqbaboplfbrettd7655fr4n2y
|
||||||
typeface-open-sans: 1.1.13
|
typeface-open-sans: 1.1.13
|
||||||
web-vitals: 3.1.1
|
web-vitals: 3.1.1
|
||||||
|
zustand: 4.3.6_react@18.2.0
|
||||||
devDependencies:
|
devDependencies:
|
||||||
'@sentry/vite-plugin': 0.3.0
|
'@sentry/vite-plugin': 0.3.0
|
||||||
'@tanstack/eslint-plugin-query': 4.21.0
|
'@tanstack/eslint-plugin-query': 4.21.0
|
||||||
@@ -131,6 +136,7 @@ importers:
|
|||||||
'@testing-library/react': 13.4.0_biqbaboplfbrettd7655fr4n2y
|
'@testing-library/react': 13.4.0_biqbaboplfbrettd7655fr4n2y
|
||||||
'@testing-library/user-event': 14.4.3
|
'@testing-library/user-event': 14.4.3
|
||||||
'@types/color': 3.0.3
|
'@types/color': 3.0.3
|
||||||
|
'@types/luxon': 3.2.0
|
||||||
'@types/prop-types': 15.7.5
|
'@types/prop-types': 15.7.5
|
||||||
'@types/react': 18.0.26
|
'@types/react': 18.0.26
|
||||||
'@types/react-beautiful-dnd': 13.1.3
|
'@types/react-beautiful-dnd': 13.1.3
|
||||||
@@ -182,6 +188,7 @@ importers:
|
|||||||
'@types/express': ^4.17.17
|
'@types/express': ^4.17.17
|
||||||
'@types/node': ^16.11.7
|
'@types/node': ^16.11.7
|
||||||
'@types/node-osc': ^6.0.0
|
'@types/node-osc': ^6.0.0
|
||||||
|
'@types/websocket': ^1.0.5
|
||||||
'@typescript-eslint/eslint-plugin': ^5.48.1
|
'@typescript-eslint/eslint-plugin': ^5.48.1
|
||||||
'@typescript-eslint/parser': ^5.48.1
|
'@typescript-eslint/parser': ^5.48.1
|
||||||
body-parser: ^1.20.0
|
body-parser: ^1.20.0
|
||||||
@@ -205,10 +212,10 @@ importers:
|
|||||||
passport-local: ~1.0.0
|
passport-local: ~1.0.0
|
||||||
prettier: ^2.8.3
|
prettier: ^2.8.3
|
||||||
shx: ^0.3.4
|
shx: ^0.3.4
|
||||||
socket.io: ^4.5.4
|
|
||||||
ts-node: ^10.9.1
|
ts-node: ^10.9.1
|
||||||
typescript: ^4.9.4
|
typescript: ^4.9.4
|
||||||
vitest: ^0.27.1
|
vitest: ^0.27.1
|
||||||
|
ws: ^8.12.1
|
||||||
dependencies:
|
dependencies:
|
||||||
'@sentry/node': 7.30.0
|
'@sentry/node': 7.30.0
|
||||||
'@sentry/tracing': 7.30.0
|
'@sentry/tracing': 7.30.0
|
||||||
@@ -226,11 +233,12 @@ importers:
|
|||||||
ontime-utils: link:../../packages/utils
|
ontime-utils: link:../../packages/utils
|
||||||
passport: 0.6.0
|
passport: 0.6.0
|
||||||
passport-local: 1.0.0
|
passport-local: 1.0.0
|
||||||
socket.io: 4.5.4
|
ws: 8.12.1
|
||||||
devDependencies:
|
devDependencies:
|
||||||
'@types/express': 4.17.17
|
'@types/express': 4.17.17
|
||||||
'@types/node': 16.18.11
|
'@types/node': 16.18.11
|
||||||
'@types/node-osc': 6.0.0
|
'@types/node-osc': 6.0.0
|
||||||
|
'@types/websocket': 1.0.5
|
||||||
'@typescript-eslint/eslint-plugin': 5.48.1_3jon24igvnqaqexgwtxk6nkpse
|
'@typescript-eslint/eslint-plugin': 5.48.1_3jon24igvnqaqexgwtxk6nkpse
|
||||||
'@typescript-eslint/parser': 5.48.1_iukboom6ndih5an6iafl45j2fe
|
'@typescript-eslint/parser': 5.48.1_iukboom6ndih5an6iafl45j2fe
|
||||||
esbuild: 0.17.5
|
esbuild: 0.17.5
|
||||||
@@ -258,19 +266,23 @@ importers:
|
|||||||
|
|
||||||
packages/utils:
|
packages/utils:
|
||||||
specifiers:
|
specifiers:
|
||||||
|
'@types/luxon': ^3.2.0
|
||||||
'@typescript-eslint/eslint-plugin': ^5.48.1
|
'@typescript-eslint/eslint-plugin': ^5.48.1
|
||||||
'@typescript-eslint/parser': ^5.48.1
|
'@typescript-eslint/parser': ^5.48.1
|
||||||
eslint: ^8.31.0
|
eslint: ^8.31.0
|
||||||
eslint-config-prettier: ^8.6.0
|
eslint-config-prettier: ^8.6.0
|
||||||
eslint-plugin-prettier: ^4.2.1
|
eslint-plugin-prettier: ^4.2.1
|
||||||
eslint-plugin-simple-import-sort: ^8.0.0
|
eslint-plugin-simple-import-sort: ^8.0.0
|
||||||
|
luxon: ^3.3.0
|
||||||
nanoid: ^4.0.0
|
nanoid: ^4.0.0
|
||||||
prettier: ^2.8.3
|
prettier: ^2.8.3
|
||||||
typescript: ^4.9.4
|
typescript: ^4.9.4
|
||||||
vitest: ^0.27.1
|
vitest: ^0.27.1
|
||||||
dependencies:
|
dependencies:
|
||||||
|
luxon: 3.3.0
|
||||||
nanoid: 4.0.0
|
nanoid: 4.0.0
|
||||||
devDependencies:
|
devDependencies:
|
||||||
|
'@types/luxon': 3.2.0
|
||||||
'@typescript-eslint/eslint-plugin': 5.48.1_3jon24igvnqaqexgwtxk6nkpse
|
'@typescript-eslint/eslint-plugin': 5.48.1_3jon24igvnqaqexgwtxk6nkpse
|
||||||
'@typescript-eslint/parser': 5.48.1_iukboom6ndih5an6iafl45j2fe
|
'@typescript-eslint/parser': 5.48.1_iukboom6ndih5an6iafl45j2fe
|
||||||
eslint: 8.31.0
|
eslint: 8.31.0
|
||||||
@@ -2608,10 +2620,6 @@ packages:
|
|||||||
engines: {node: '>=6'}
|
engines: {node: '>=6'}
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
/@socket.io/component-emitter/3.1.0:
|
|
||||||
resolution: {integrity: sha512-+9jVqKhRSpsc591z5vX+X5Yyw+he/HCB4iQ/RYxw35CEPaY1gnsNE43nf9n9AaYjAQrTiI/mOwKUKdUs9vf7Xg==}
|
|
||||||
dev: false
|
|
||||||
|
|
||||||
/@svgr/babel-plugin-add-jsx-attribute/6.5.1_@babel+core@7.20.12:
|
/@svgr/babel-plugin-add-jsx-attribute/6.5.1_@babel+core@7.20.12:
|
||||||
resolution: {integrity: sha512-9PYGcXrAxitycIjRmZB+Q0JaN07GZIWaTBIGQzfaZv+qr1n8X1XUEJ5rZ/vx6OVD9RRYlrNnXWExQXcmZeD/BQ==}
|
resolution: {integrity: sha512-9PYGcXrAxitycIjRmZB+Q0JaN07GZIWaTBIGQzfaZv+qr1n8X1XUEJ5rZ/vx6OVD9RRYlrNnXWExQXcmZeD/BQ==}
|
||||||
engines: {node: '>=10'}
|
engines: {node: '>=10'}
|
||||||
@@ -2906,16 +2914,6 @@ packages:
|
|||||||
'@types/node': 18.11.18
|
'@types/node': 18.11.18
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
/@types/cookie/0.4.1:
|
|
||||||
resolution: {integrity: sha512-XW/Aa8APYr6jSVVA1y/DEIZX0/GMKLEVekNG727R8cs56ahETkRAy/3DR7+fJyh7oUgGwNQaRfXCun0+KbWY7Q==}
|
|
||||||
dev: false
|
|
||||||
|
|
||||||
/@types/cors/2.8.13:
|
|
||||||
resolution: {integrity: sha512-RG8AStHlUiV5ysZQKq97copd2UmVYw3/pRMLefISZ3S1hK104Cwm7iLQ3fTKx+lsUH2CE8FlLaYeEA2LSeqYUA==}
|
|
||||||
dependencies:
|
|
||||||
'@types/node': 18.11.18
|
|
||||||
dev: false
|
|
||||||
|
|
||||||
/@types/debug/4.1.7:
|
/@types/debug/4.1.7:
|
||||||
resolution: {integrity: sha512-9AonUzyTjXXhEOa0DnqpzZi6VHlqKMswga9EXjpXnnqxwLtdvPPtlO8evrI5D9S6asFRCQ6v+wpiUKbw+vKqyg==}
|
resolution: {integrity: sha512-9AonUzyTjXXhEOa0DnqpzZi6VHlqKMswga9EXjpXnnqxwLtdvPPtlO8evrI5D9S6asFRCQ6v+wpiUKbw+vKqyg==}
|
||||||
dependencies:
|
dependencies:
|
||||||
@@ -3008,6 +3006,10 @@ packages:
|
|||||||
resolution: {integrity: sha512-BdZ5BCCvho3EIXw6wUCXHe7rS53AIDPLE+JzwgT+OsJk53oBfbSmZZ7CX4VaRoN78N+TJpFi9QPlfIVNmJYWxQ==}
|
resolution: {integrity: sha512-BdZ5BCCvho3EIXw6wUCXHe7rS53AIDPLE+JzwgT+OsJk53oBfbSmZZ7CX4VaRoN78N+TJpFi9QPlfIVNmJYWxQ==}
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
|
/@types/luxon/3.2.0:
|
||||||
|
resolution: {integrity: sha512-lGmaGFoaXHuOLXFvuju2bfvZRqxAqkHPx9Y9IQdQABrinJJshJwfNCKV+u7rR3kJbiqfTF/NhOkcxxAFrObyaA==}
|
||||||
|
dev: true
|
||||||
|
|
||||||
/@types/mime/3.0.1:
|
/@types/mime/3.0.1:
|
||||||
resolution: {integrity: sha512-Y4XFY5VJAuw0FgAqPNd6NNoV44jbq9Bz2L7Rh/J6jLTiHBSBJa9fxqQIvkIld4GsoDOcCbvzOUAbLPsSKKg+uA==}
|
resolution: {integrity: sha512-Y4XFY5VJAuw0FgAqPNd6NNoV44jbq9Bz2L7Rh/J6jLTiHBSBJa9fxqQIvkIld4GsoDOcCbvzOUAbLPsSKKg+uA==}
|
||||||
dev: true
|
dev: true
|
||||||
@@ -3035,6 +3037,7 @@ packages:
|
|||||||
|
|
||||||
/@types/node/18.11.18:
|
/@types/node/18.11.18:
|
||||||
resolution: {integrity: sha512-DHQpWGjyQKSHj3ebjFI/wRKcqQcdR+MoFBygntYOZytCqNfkd2ZC4ARDJ2DQqhjH5p85Nnd3jhUJIXrszFX/JA==}
|
resolution: {integrity: sha512-DHQpWGjyQKSHj3ebjFI/wRKcqQcdR+MoFBygntYOZytCqNfkd2ZC4ARDJ2DQqhjH5p85Nnd3jhUJIXrszFX/JA==}
|
||||||
|
dev: true
|
||||||
|
|
||||||
/@types/normalize-package-data/2.4.1:
|
/@types/normalize-package-data/2.4.1:
|
||||||
resolution: {integrity: sha512-Gj7cI7z+98M282Tqmp2K5EIsoouUEzbBJhQQzDE3jSIRk6r9gsz0oUokqIUR4u1R3dMHo0pDHM7sNOHyhulypw==}
|
resolution: {integrity: sha512-Gj7cI7z+98M282Tqmp2K5EIsoouUEzbBJhQQzDE3jSIRk6r9gsz0oUokqIUR4u1R3dMHo0pDHM7sNOHyhulypw==}
|
||||||
@@ -3127,6 +3130,12 @@ packages:
|
|||||||
dev: true
|
dev: true
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
|
/@types/websocket/1.0.5:
|
||||||
|
resolution: {integrity: sha512-NbsqiNX9CnEfC1Z0Vf4mE1SgAJ07JnRYcNex7AJ9zAVzmiGHmjKFEk7O4TJIsgv2B1sLEb6owKFZrACwdYngsQ==}
|
||||||
|
dependencies:
|
||||||
|
'@types/node': 18.11.18
|
||||||
|
dev: true
|
||||||
|
|
||||||
/@types/yargs-parser/21.0.0:
|
/@types/yargs-parser/21.0.0:
|
||||||
resolution: {integrity: sha512-iO9ZQHkZxHn4mSakYV0vFHAVDyEOIJQrV2uZ06HxEPcx+mt8swXoZHIbaaJ2crJYFfErySgktuTZ3BeLz+XmFA==}
|
resolution: {integrity: sha512-iO9ZQHkZxHn4mSakYV0vFHAVDyEOIJQrV2uZ06HxEPcx+mt8swXoZHIbaaJ2crJYFfErySgktuTZ3BeLz+XmFA==}
|
||||||
dev: true
|
dev: true
|
||||||
@@ -3764,11 +3773,6 @@ packages:
|
|||||||
requiresBuild: true
|
requiresBuild: true
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
/base64id/2.0.0:
|
|
||||||
resolution: {integrity: sha512-lGe34o6EHj9y3Kts9R4ZYs/Gr+6N7MCaMlIFA3F1R2O5/m7K06AxfSeO5530PEERE6/WyEg3lsuyw4GHlPZHog==}
|
|
||||||
engines: {node: ^4.5.0 || >= 5.9}
|
|
||||||
dev: false
|
|
||||||
|
|
||||||
/binary-extensions/2.2.0:
|
/binary-extensions/2.2.0:
|
||||||
resolution: {integrity: sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==}
|
resolution: {integrity: sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==}
|
||||||
engines: {node: '>=8'}
|
engines: {node: '>=8'}
|
||||||
@@ -4423,6 +4427,11 @@ packages:
|
|||||||
resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==}
|
resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==}
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
|
/deepmerge/4.3.0:
|
||||||
|
resolution: {integrity: sha512-z2wJZXrmeHdvYJp/Ux55wIjqo81G5Bp4c+oELTW+7ar6SogWHajt5a9gO3s3IDaGSAXjDk0vlQKN3rms8ab3og==}
|
||||||
|
engines: {node: '>=0.10.0'}
|
||||||
|
dev: false
|
||||||
|
|
||||||
/defer-to-connect/1.1.3:
|
/defer-to-connect/1.1.3:
|
||||||
resolution: {integrity: sha512-0ISdNousHvZT2EiFlZeZAHBUvSxmKswVCEf8hW7KWgG4a8MVEu/3Vb6uWYozkjylyCxe0JBIiRB1jV45S70WVQ==}
|
resolution: {integrity: sha512-0ISdNousHvZT2EiFlZeZAHBUvSxmKswVCEf8hW7KWgG4a8MVEu/3Vb6uWYozkjylyCxe0JBIiRB1jV45S70WVQ==}
|
||||||
dev: true
|
dev: true
|
||||||
@@ -4667,45 +4676,6 @@ packages:
|
|||||||
once: 1.4.0
|
once: 1.4.0
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
/engine.io-client/6.2.3:
|
|
||||||
resolution: {integrity: sha512-aXPtgF1JS3RuuKcpSrBtimSjYvrbhKW9froICH4s0F3XQWLxsKNxqzG39nnvQZQnva4CMvUK63T7shevxRyYHw==}
|
|
||||||
dependencies:
|
|
||||||
'@socket.io/component-emitter': 3.1.0
|
|
||||||
debug: 4.3.4
|
|
||||||
engine.io-parser: 5.0.5
|
|
||||||
ws: 8.2.3
|
|
||||||
xmlhttprequest-ssl: 2.0.0
|
|
||||||
transitivePeerDependencies:
|
|
||||||
- bufferutil
|
|
||||||
- supports-color
|
|
||||||
- utf-8-validate
|
|
||||||
dev: false
|
|
||||||
|
|
||||||
/engine.io-parser/5.0.5:
|
|
||||||
resolution: {integrity: sha512-mjEyaa4zhuuRhaSLOdjEb57X0XPP9JEsnXI4E+ivhwT0GgzUogARx4MqoY1jQyB+4Bkz3BUOmzL7t9RMKmlG3g==}
|
|
||||||
engines: {node: '>=10.0.0'}
|
|
||||||
dev: false
|
|
||||||
|
|
||||||
/engine.io/6.2.1:
|
|
||||||
resolution: {integrity: sha512-ECceEFcAaNRybd3lsGQKas3ZlMVjN3cyWwMP25D2i0zWfyiytVbTpRPa34qrr+FHddtpBVOmq4H/DCv1O0lZRA==}
|
|
||||||
engines: {node: '>=10.0.0'}
|
|
||||||
dependencies:
|
|
||||||
'@types/cookie': 0.4.1
|
|
||||||
'@types/cors': 2.8.13
|
|
||||||
'@types/node': 18.11.18
|
|
||||||
accepts: 1.3.8
|
|
||||||
base64id: 2.0.0
|
|
||||||
cookie: 0.4.2
|
|
||||||
cors: 2.8.5
|
|
||||||
debug: 4.3.4
|
|
||||||
engine.io-parser: 5.0.5
|
|
||||||
ws: 8.2.3
|
|
||||||
transitivePeerDependencies:
|
|
||||||
- bufferutil
|
|
||||||
- supports-color
|
|
||||||
- utf-8-validate
|
|
||||||
dev: false
|
|
||||||
|
|
||||||
/entities/4.4.0:
|
/entities/4.4.0:
|
||||||
resolution: {integrity: sha512-oYp7156SP8LkeGD0GF85ad1X9Ai79WtRsZ2gxJqtBuzH+98YUV6jkHEKlZkMbcrjJjIVJNIDP/3WL9wQkoPbWA==}
|
resolution: {integrity: sha512-oYp7156SP8LkeGD0GF85ad1X9Ai79WtRsZ2gxJqtBuzH+98YUV6jkHEKlZkMbcrjJjIVJNIDP/3WL9wQkoPbWA==}
|
||||||
engines: {node: '>=0.12'}
|
engines: {node: '>=0.12'}
|
||||||
@@ -6534,8 +6504,8 @@ packages:
|
|||||||
/lru_map/0.3.3:
|
/lru_map/0.3.3:
|
||||||
resolution: {integrity: sha512-Pn9cox5CsMYngeDbmChANltQl+5pi6XmTrraMSzhPmMBbmgcxmqWry0U3PGapCU1yB4/LqCcom7qhHZiF/jGfQ==}
|
resolution: {integrity: sha512-Pn9cox5CsMYngeDbmChANltQl+5pi6XmTrraMSzhPmMBbmgcxmqWry0U3PGapCU1yB4/LqCcom7qhHZiF/jGfQ==}
|
||||||
|
|
||||||
/luxon/3.2.1:
|
/luxon/3.3.0:
|
||||||
resolution: {integrity: sha512-QrwPArQCNLAKGO/C+ZIilgIuDnEnKx5QYODdDtbFaxzsbZcc/a7WFq7MhsVYgRlwawLtvOUESTlfJ+hc/USqPg==}
|
resolution: {integrity: sha512-An0UCfG/rSiqtAIiBPO0Y9/zAnHUZxAMiCpTd5h2smgsj7GGmcenvrvww2cqNA8/4A5ZrD1gJpHN2mIHZQF+Mg==}
|
||||||
engines: {node: '>=12'}
|
engines: {node: '>=12'}
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
@@ -7613,6 +7583,16 @@ packages:
|
|||||||
react: 18.2.0
|
react: 18.2.0
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
|
/react-use-websocket/4.3.1_biqbaboplfbrettd7655fr4n2y:
|
||||||
|
resolution: {integrity: sha512-zHPLWrgcqydJaak2O5V9hiz4q2dwkwqNQqpgFVmSuPxLZdsZlnDs8DVHy3WtHH+A6ms/8aHIyX7+7ulOcrnR0Q==}
|
||||||
|
peerDependencies:
|
||||||
|
react: '>= 18.0.0'
|
||||||
|
react-dom: '>= 18.0.0'
|
||||||
|
dependencies:
|
||||||
|
react: 18.2.0
|
||||||
|
react-dom: 18.2.0_react@18.2.0
|
||||||
|
dev: false
|
||||||
|
|
||||||
/react/18.2.0:
|
/react/18.2.0:
|
||||||
resolution: {integrity: sha512-/3IjMdb2L9QbBdWiW5e3P2/npwMBaU9mHCSCUzNln0ZCYbcfTsGbTJrU/kGemdH2IWmB2ioZ+zkxtmq6g09fGQ==}
|
resolution: {integrity: sha512-/3IjMdb2L9QbBdWiW5e3P2/npwMBaU9mHCSCUzNln0ZCYbcfTsGbTJrU/kGemdH2IWmB2ioZ+zkxtmq6g09fGQ==}
|
||||||
engines: {node: '>=0.10.0'}
|
engines: {node: '>=0.10.0'}
|
||||||
@@ -8025,50 +8005,6 @@ packages:
|
|||||||
dev: true
|
dev: true
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
/socket.io-adapter/2.4.0:
|
|
||||||
resolution: {integrity: sha512-W4N+o69rkMEGVuk2D/cvca3uYsvGlMwsySWV447y99gUPghxq42BxqLNMndb+a1mm/5/7NeXVQS7RLa2XyXvYg==}
|
|
||||||
dev: false
|
|
||||||
|
|
||||||
/socket.io-client/4.5.4:
|
|
||||||
resolution: {integrity: sha512-ZpKteoA06RzkD32IbqILZ+Cnst4xewU7ZYK12aS1mzHftFFjpoMz69IuhP/nL25pJfao/amoPI527KnuhFm01g==}
|
|
||||||
engines: {node: '>=10.0.0'}
|
|
||||||
dependencies:
|
|
||||||
'@socket.io/component-emitter': 3.1.0
|
|
||||||
debug: 4.3.4
|
|
||||||
engine.io-client: 6.2.3
|
|
||||||
socket.io-parser: 4.2.1
|
|
||||||
transitivePeerDependencies:
|
|
||||||
- bufferutil
|
|
||||||
- supports-color
|
|
||||||
- utf-8-validate
|
|
||||||
dev: false
|
|
||||||
|
|
||||||
/socket.io-parser/4.2.1:
|
|
||||||
resolution: {integrity: sha512-V4GrkLy+HeF1F/en3SpUaM+7XxYXpuMUWLGde1kSSh5nQMN4hLrbPIkD+otwh6q9R6NOQBN4AMaOZ2zVjui82g==}
|
|
||||||
engines: {node: '>=10.0.0'}
|
|
||||||
dependencies:
|
|
||||||
'@socket.io/component-emitter': 3.1.0
|
|
||||||
debug: 4.3.4
|
|
||||||
transitivePeerDependencies:
|
|
||||||
- supports-color
|
|
||||||
dev: false
|
|
||||||
|
|
||||||
/socket.io/4.5.4:
|
|
||||||
resolution: {integrity: sha512-m3GC94iK9MfIEeIBfbhJs5BqFibMtkRk8ZpKwG2QwxV0m/eEhPIV4ara6XCF1LWNAus7z58RodiZlAH71U3EhQ==}
|
|
||||||
engines: {node: '>=10.0.0'}
|
|
||||||
dependencies:
|
|
||||||
accepts: 1.3.8
|
|
||||||
base64id: 2.0.0
|
|
||||||
debug: 4.3.4
|
|
||||||
engine.io: 6.2.1
|
|
||||||
socket.io-adapter: 2.4.0
|
|
||||||
socket.io-parser: 4.2.1
|
|
||||||
transitivePeerDependencies:
|
|
||||||
- bufferutil
|
|
||||||
- supports-color
|
|
||||||
- utf-8-validate
|
|
||||||
dev: false
|
|
||||||
|
|
||||||
/source-map-js/1.0.2:
|
/source-map-js/1.0.2:
|
||||||
resolution: {integrity: sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==}
|
resolution: {integrity: sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==}
|
||||||
engines: {node: '>=0.10.0'}
|
engines: {node: '>=0.10.0'}
|
||||||
@@ -9307,12 +9243,12 @@ packages:
|
|||||||
optional: true
|
optional: true
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
/ws/8.2.3:
|
/ws/8.12.1:
|
||||||
resolution: {integrity: sha512-wBuoj1BDpC6ZQ1B7DWQBYVLphPWkm8i9Y0/3YdHjHKHiohOJ1ws+3OccDWtH+PoC9DZD5WOTrJvNbWvjS6JWaA==}
|
resolution: {integrity: sha512-1qo+M9Ba+xNhPB+YTWUlK6M17brTut5EXbcBaMRN5pH5dFrXz7lzz1ChFSUq3bOUl8yEvSenhHmYUNJxFzdJew==}
|
||||||
engines: {node: '>=10.0.0'}
|
engines: {node: '>=10.0.0'}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
bufferutil: ^4.0.1
|
bufferutil: ^4.0.1
|
||||||
utf-8-validate: ^5.0.2
|
utf-8-validate: '>=5.0.2'
|
||||||
peerDependenciesMeta:
|
peerDependenciesMeta:
|
||||||
bufferutil:
|
bufferutil:
|
||||||
optional: true
|
optional: true
|
||||||
@@ -9349,11 +9285,6 @@ packages:
|
|||||||
resolution: {integrity: sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==}
|
resolution: {integrity: sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==}
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
/xmlhttprequest-ssl/2.0.0:
|
|
||||||
resolution: {integrity: sha512-QKxVRxiRACQcVuQEYFsI1hhkrMlrXHPegbbd1yn9UHOmRxY+si12nQYzri3vbzt8VdTTRviqcKxcyllFas5z2A==}
|
|
||||||
engines: {node: '>=0.4.0'}
|
|
||||||
dev: false
|
|
||||||
|
|
||||||
/xtend/4.0.2:
|
/xtend/4.0.2:
|
||||||
resolution: {integrity: sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==}
|
resolution: {integrity: sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==}
|
||||||
engines: {node: '>=0.4'}
|
engines: {node: '>=0.4'}
|
||||||
@@ -9415,3 +9346,19 @@ packages:
|
|||||||
resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==}
|
resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==}
|
||||||
engines: {node: '>=10'}
|
engines: {node: '>=10'}
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
|
/zustand/4.3.6_react@18.2.0:
|
||||||
|
resolution: {integrity: sha512-6J5zDxjxLE+yukC2XZWf/IyWVKnXT9b9HUv09VJ/bwGCpKNcaTqp7Ws28Xr8jnbvnZcdRaidztAPsXFBIqufiw==}
|
||||||
|
engines: {node: '>=12.7.0'}
|
||||||
|
peerDependencies:
|
||||||
|
immer: '>=9.0'
|
||||||
|
react: '>=16.8'
|
||||||
|
peerDependenciesMeta:
|
||||||
|
immer:
|
||||||
|
optional: true
|
||||||
|
react:
|
||||||
|
optional: true
|
||||||
|
dependencies:
|
||||||
|
react: 18.2.0
|
||||||
|
use-sync-external-store: 1.2.0_react@18.2.0
|
||||||
|
dev: false
|
||||||
|
|||||||
Reference in New Issue
Block a user