mirror of
https://github.com/cpvalente/ontime.git
synced 2026-08-10 01:43:43 +00:00
V2 integration (#293)
* feat: integrations and lifecycles * refactor: prevent issues with start order
This commit is contained in:
@@ -25,6 +25,7 @@
|
||||
"react-beautiful-dnd": "^13.1.1",
|
||||
"react-dom": "^18.2.0",
|
||||
"react-fast-compare": "^3.2.0",
|
||||
"react-hook-form": "^7.43.1",
|
||||
"react-qr-code": "^2.0.11",
|
||||
"react-router-dom": "^6.3.0",
|
||||
"react-table": "^7.7.0",
|
||||
|
||||
@@ -3,7 +3,7 @@ import axios from 'axios';
|
||||
import { URLAliasType } from '../models/Alias.type';
|
||||
import { InfoType } from '../models/Info.types';
|
||||
import { OntimeSettingsType } from '../models/OntimeSettings.type';
|
||||
import { OscSettingsType } from '../models/OscSettings.type';
|
||||
import { OSCSettings } from '../models/OscSettings.type';
|
||||
import { UserFieldsType } from '../models/UserFields.type';
|
||||
import { ViewSettingsType } from '../models/ViewSettings.type';
|
||||
|
||||
@@ -90,7 +90,7 @@ export async function postUserFields(data: UserFieldsType) {
|
||||
* @description HTTP request to retrieve osc settings
|
||||
* @return {Promise}
|
||||
*/
|
||||
export async function getOSC(): Promise<OscSettingsType> {
|
||||
export async function getOSC(): Promise<OSCSettings> {
|
||||
const res = await axios.get(`${ontimeURL}/osc`);
|
||||
return res.data;
|
||||
}
|
||||
@@ -99,7 +99,7 @@ export async function getOSC(): Promise<OscSettingsType> {
|
||||
* @description HTTP request to mutate osc settings
|
||||
* @return {Promise}
|
||||
*/
|
||||
export async function postOSC(data: OscSettingsType) {
|
||||
export async function postOSC(data: OSCSettings) {
|
||||
return axios.post(`${ontimeURL}/osc`, data);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,13 +1,16 @@
|
||||
export type OscSettingsType = {
|
||||
port: string;
|
||||
import { OSCSettings } from 'ontime-types';
|
||||
|
||||
// in the placeholder, we pass strings to satisfy input type
|
||||
export interface PlaceholderSettings extends Omit<OSCSettings, 'portIn' | 'portOut'> {
|
||||
portIn: string;
|
||||
portOut: string;
|
||||
targetIP: string;
|
||||
enabled: boolean;
|
||||
}
|
||||
|
||||
export const oscPlaceholderSettings: OscSettingsType = {
|
||||
port: '',
|
||||
export const oscPlaceholderSettings: PlaceholderSettings = {
|
||||
portIn: '',
|
||||
portOut: '',
|
||||
targetIP: '',
|
||||
enabled: false,
|
||||
enabledIn: false,
|
||||
enabledOut: false,
|
||||
subscriptions: {},
|
||||
};
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
import { isIPAddress, isOnlyNumbers } from '../regex';
|
||||
|
||||
describe('simple tests for regex', () => {
|
||||
test('isOnlyNumbers', () => {
|
||||
const right = ['1231', '1'];
|
||||
const wrong = ['a', 'asdas1asdas', '11as', '1_', '1.1'];
|
||||
|
||||
right.forEach((t) => {
|
||||
expect(isOnlyNumbers.test(t)).toBe(true);
|
||||
});
|
||||
wrong.forEach((t) => {
|
||||
expect(isOnlyNumbers.test(t)).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
test('isIPAddress', () => {
|
||||
const right = ['0.0.0.0', '127.0.0.1'];
|
||||
const wrong = ['0', 'testing', '123.0.1'];
|
||||
|
||||
right.forEach((t) => {
|
||||
expect(isIPAddress.test(t)).toBe(true);
|
||||
});
|
||||
wrong.forEach((t) => {
|
||||
expect(isIPAddress.test(t)).toBe(false);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,2 @@
|
||||
export const isOnlyNumbers = /^\d+$/;
|
||||
export const isIPAddress = /^((25[0-5]|(2[0-4]|1\d|[1-9]|)\d)\.?\b){4}$/;
|
||||
@@ -1,10 +1,11 @@
|
||||
import { lazy, useEffect } from 'react';
|
||||
import { Box, useDisclosure } from '@chakra-ui/react';
|
||||
|
||||
import ErrorBoundary from '../../common/components/error-boundary/ErrorBoundary';
|
||||
import UploadModal from '../../common/components/upload-modal/UploadModal';
|
||||
import ModalManager from '../../features/modals/ModalManager';
|
||||
|
||||
import ModalManager from '../modals/ModalManager';
|
||||
import MenuBar from '../menu/MenuBar';
|
||||
import IntegrationModal from '../modals/integration-modal/IntegrationModal';
|
||||
|
||||
import styles from './Editor.module.scss';
|
||||
|
||||
@@ -15,16 +16,14 @@ const Info = lazy(() => import('../../features/info/InfoExport'));
|
||||
const EventEditor = lazy(() => import('../../features/event-editor/EventEditorExport'));
|
||||
|
||||
export default function Editor() {
|
||||
const {
|
||||
isOpen: isSettingsOpen,
|
||||
onOpen: onSettingsOpen,
|
||||
onClose: onSettingsClose,
|
||||
} = useDisclosure();
|
||||
const { isOpen: isSettingsOpen, onOpen: onSettingsOpen, onClose: onSettingsClose } = useDisclosure();
|
||||
|
||||
const { isOpen: isUploadModalOpen, onOpen: onUploadModalOpen, onClose: onUploadModalClose } = useDisclosure();
|
||||
|
||||
const {
|
||||
isOpen: isUploadModalOpen,
|
||||
onOpen: onUploadModalOpen,
|
||||
onClose: onUploadModalClose,
|
||||
isOpen: isIntegrationModalOpen,
|
||||
onOpen: onIntegrationModalOpen,
|
||||
onClose: onIntegrationModalClose,
|
||||
} = useDisclosure();
|
||||
|
||||
// Set window title
|
||||
@@ -35,10 +34,11 @@ export default function Editor() {
|
||||
return (
|
||||
<>
|
||||
<UploadModal onClose={onUploadModalClose} isOpen={isUploadModalOpen} />
|
||||
<IntegrationModal onClose={onIntegrationModalClose} isOpen={isIntegrationModalOpen} />
|
||||
<ErrorBoundary>
|
||||
<ModalManager isOpen={isSettingsOpen} onClose={onSettingsClose} />
|
||||
</ErrorBoundary>
|
||||
<div className={styles.mainContainer} data-testid="event-editor">
|
||||
<div className={styles.mainContainer} data-testid='event-editor'>
|
||||
<Box id='settings' className={styles.settings}>
|
||||
<ErrorBoundary>
|
||||
<MenuBar
|
||||
@@ -47,6 +47,8 @@ export default function Editor() {
|
||||
onSettingsClose={onSettingsClose}
|
||||
isUploadOpen={isUploadModalOpen}
|
||||
onUploadOpen={onUploadModalOpen}
|
||||
isIntegrationOpen={isIntegrationModalOpen}
|
||||
onIntegrationOpen={onIntegrationModalOpen}
|
||||
/>
|
||||
</ErrorBoundary>
|
||||
</Box>
|
||||
|
||||
@@ -4,6 +4,8 @@ import { FiHelpCircle } from '@react-icons/all-files/fi/FiHelpCircle';
|
||||
import { FiMinimize } from '@react-icons/all-files/fi/FiMinimize';
|
||||
import { FiSave } from '@react-icons/all-files/fi/FiSave';
|
||||
import { FiUpload } from '@react-icons/all-files/fi/FiUpload';
|
||||
import { IoExtensionPuzzle } from '@react-icons/all-files/io5/IoExtensionPuzzle';
|
||||
import { IoExtensionPuzzleOutline } from '@react-icons/all-files/io5/IoExtensionPuzzleOutline';
|
||||
import { IoScan } from '@react-icons/all-files/io5/IoScan';
|
||||
import { IoSettingsOutline } from '@react-icons/all-files/io5/IoSettingsOutline';
|
||||
import { downloadRundown } from '../../common/api/ontimeApi';
|
||||
@@ -20,6 +22,8 @@ interface MenuBarProps {
|
||||
onSettingsClose: () => void;
|
||||
isUploadOpen: boolean;
|
||||
onUploadOpen: () => void;
|
||||
isIntegrationOpen: boolean;
|
||||
onIntegrationOpen: () => void;
|
||||
}
|
||||
|
||||
type Actions = 'min' | 'max' | 'shutdown' | 'help';
|
||||
@@ -29,42 +33,53 @@ const buttonStyle = {
|
||||
size: 'lg',
|
||||
colorScheme: 'white',
|
||||
_hover: {
|
||||
background: 'rgba(255, 255, 255, 0.10)' // $white-10
|
||||
background: 'rgba(255, 255, 255, 0.10)', // $white-10
|
||||
},
|
||||
_active: {
|
||||
background: 'rgba(255, 255, 255, 0.13)' // $white-13
|
||||
}
|
||||
background: 'rgba(255, 255, 255, 0.13)', // $white-13
|
||||
},
|
||||
};
|
||||
|
||||
export default function MenuBar(props: MenuBarProps) {
|
||||
const { isSettingsOpen, onSettingsOpen, onSettingsClose, isUploadOpen, onUploadOpen } = props;
|
||||
const {
|
||||
isSettingsOpen,
|
||||
onSettingsOpen,
|
||||
onSettingsClose,
|
||||
isUploadOpen,
|
||||
onUploadOpen,
|
||||
isIntegrationOpen,
|
||||
onIntegrationOpen,
|
||||
} = props;
|
||||
const { isElectron, sendToElectron } = useElectronEvent();
|
||||
|
||||
const actionHandler = useCallback((action: Actions) => {
|
||||
// Stop crashes when testing locally
|
||||
if (!isElectron) {
|
||||
if (action === 'help') {
|
||||
window.open('https://cpvalente.gitbook.io/ontime/');
|
||||
const actionHandler = useCallback(
|
||||
(action: Actions) => {
|
||||
// Stop crashes when testing locally
|
||||
if (!isElectron) {
|
||||
if (action === 'help') {
|
||||
window.open('https://cpvalente.gitbook.io/ontime/');
|
||||
}
|
||||
} else {
|
||||
switch (action) {
|
||||
case 'min':
|
||||
sendToElectron('set-window', 'to-tray');
|
||||
break;
|
||||
case 'max':
|
||||
sendToElectron('set-window', 'to-max');
|
||||
break;
|
||||
case 'shutdown':
|
||||
sendToElectron('shutdown', 'now');
|
||||
break;
|
||||
case 'help':
|
||||
sendToElectron('send-to-link', 'help');
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
switch (action) {
|
||||
case 'min':
|
||||
sendToElectron('set-window', 'to-tray');
|
||||
break;
|
||||
case 'max':
|
||||
sendToElectron('set-window', 'to-max');
|
||||
break;
|
||||
case 'shutdown':
|
||||
sendToElectron('shutdown', 'now');
|
||||
break;
|
||||
case 'help':
|
||||
sendToElectron('send-to-link', 'help');
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
}, [sendToElectron, isElectron]);
|
||||
},
|
||||
[sendToElectron, isElectron],
|
||||
);
|
||||
|
||||
// Handle keyboard shortcuts
|
||||
const handleKeyPress = useCallback(
|
||||
@@ -129,6 +144,15 @@ export default function MenuBar(props: MenuBarProps) {
|
||||
aria-label='Settings'
|
||||
/>
|
||||
<div className={style.gap} />
|
||||
<TooltipActionBtn
|
||||
{...buttonStyle}
|
||||
icon={isIntegrationOpen ? <IoExtensionPuzzle /> : <IoExtensionPuzzleOutline />}
|
||||
className={isIntegrationOpen ? style.open : ''}
|
||||
clickHandler={onIntegrationOpen}
|
||||
tooltip='Integrations'
|
||||
aria-label='Integrations'
|
||||
/>
|
||||
<div className={style.gap} />
|
||||
<TooltipActionBtn
|
||||
{...buttonStyle}
|
||||
icon={<FiUpload />}
|
||||
|
||||
@@ -0,0 +1,77 @@
|
||||
@use '../../theme/v2Styles' as *;
|
||||
@use '../../theme/ontimeColours' as *;
|
||||
|
||||
.headerNotes {
|
||||
font-size: $text-body-size;
|
||||
width: 100%;
|
||||
padding: 0 16px;
|
||||
color: $modal-note-color;
|
||||
margin-bottom: $section-spacing;
|
||||
}
|
||||
|
||||
.divider {
|
||||
margin: 8px 0;
|
||||
border: 0;
|
||||
border-top: 1px solid $gray-100;
|
||||
}
|
||||
|
||||
.sectionContainer {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%
|
||||
}
|
||||
|
||||
.splitSection {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
|
||||
border-radius: 3px;
|
||||
padding: 8px;
|
||||
|
||||
&:hover {
|
||||
background-color: $blue-50;
|
||||
}
|
||||
}
|
||||
|
||||
.sectionTitle {
|
||||
font-size: 14px;
|
||||
display: block;
|
||||
|
||||
&.main {
|
||||
font-weight: 600;
|
||||
}
|
||||
}
|
||||
|
||||
@mixin subsection {
|
||||
font-size: 14px;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.sectionSubtitle {
|
||||
@include subsection;
|
||||
color: $modal-note-color;
|
||||
}
|
||||
|
||||
.error {
|
||||
@include subsection;
|
||||
color: $error-red;
|
||||
}
|
||||
|
||||
.buttonSection {
|
||||
margin-top: 16px;
|
||||
padding-top: 16px;
|
||||
border-top: 1px solid $gray-100;
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.buttonSection {
|
||||
button:first-of-type {
|
||||
margin-right: auto;
|
||||
}
|
||||
}
|
||||
|
||||
.spacer {
|
||||
flex-grow: 1;
|
||||
}
|
||||
+11
-22
@@ -10,17 +10,19 @@ import {
|
||||
TabPanels,
|
||||
Tabs,
|
||||
} from '@chakra-ui/react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import AliasesModal from './AliasesModal';
|
||||
import AppSettingsModal from './AppSettingsModal';
|
||||
import EventSettingsModal from './EventSettingsModal';
|
||||
import IntegrationSettingsModal from './IntegrationSettingsModal';
|
||||
import OscSettingsModal from './OscSettingsModal';
|
||||
import TableOptionsModal from './TableOptionsModal';
|
||||
import ViewsSettingsModal from './ViewsSettingsModal';
|
||||
|
||||
export default function ModalManager(props) {
|
||||
interface ModalManagerProps {
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
export default function ModalManager(props: ModalManagerProps) {
|
||||
const { isOpen, onClose } = props;
|
||||
return (
|
||||
<Modal
|
||||
@@ -38,13 +40,11 @@ export default function ModalManager(props) {
|
||||
|
||||
<Tabs size='sm' isLazy>
|
||||
<TabList>
|
||||
<Tab style={{ fontSize: '0.9em' }}>App Settings</Tab>
|
||||
<Tab style={{ fontSize: '0.9em' }}>Viewers</Tab>
|
||||
<Tab style={{ fontSize: '0.9em' }}>Event Data</Tab>
|
||||
<Tab style={{ fontSize: '0.9em' }}>URL Aliases</Tab>
|
||||
<Tab style={{ fontSize: '0.9em' }}>Cuesheet</Tab>
|
||||
<Tab style={{ fontSize: '0.9em' }}>OSC</Tab>
|
||||
{/*<Tab style={{ fontSize: '0.9em' }}>Integration</Tab>*/}
|
||||
<Tab>App Settings</Tab>
|
||||
<Tab>Viewers</Tab>
|
||||
<Tab>Event Data</Tab>
|
||||
<Tab>URL Aliases</Tab>
|
||||
<Tab>Cuesheet</Tab>
|
||||
</TabList>
|
||||
<TabPanels>
|
||||
<TabPanel>
|
||||
@@ -62,20 +62,9 @@ export default function ModalManager(props) {
|
||||
<TabPanel>
|
||||
<TableOptionsModal />
|
||||
</TabPanel>
|
||||
<TabPanel>
|
||||
<OscSettingsModal />
|
||||
</TabPanel>
|
||||
{/*<TabPanel>*/}
|
||||
{/* <IntegrationSettingsModal />*/}
|
||||
{/*</TabPanel>*/}
|
||||
</TabPanels>
|
||||
</Tabs>
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
|
||||
ModalManager.propTypes = {
|
||||
isOpen: PropTypes.bool.isRequired,
|
||||
onClose: PropTypes.func.isRequired,
|
||||
};
|
||||
@@ -0,0 +1,23 @@
|
||||
import { Button, ModalFooter } from '@chakra-ui/react';
|
||||
|
||||
import styles from './Modal.module.scss';
|
||||
|
||||
export default function ModalSubmitFooter() {
|
||||
return (
|
||||
<ModalFooter className={styles.buttonSection}>
|
||||
<Button variant='ghosted' paddingLeft={0} color='#6c6c6c'>
|
||||
Revert to saved
|
||||
</Button>
|
||||
<Button colorScheme='gray'>Cancel</Button>
|
||||
<Button
|
||||
variant='ontime-filled'
|
||||
type='submit'
|
||||
// disabled={isSubmitting}
|
||||
isLoading={false}
|
||||
padding='0 2.5em'
|
||||
>
|
||||
Save
|
||||
</Button>
|
||||
</ModalFooter>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
import { PropsWithChildren } from 'react';
|
||||
import {
|
||||
Button,
|
||||
Modal,
|
||||
ModalCloseButton,
|
||||
ModalContent,
|
||||
ModalFooter,
|
||||
ModalHeader,
|
||||
ModalOverlay,
|
||||
} from '@chakra-ui/react';
|
||||
import styles from './Modal.module.scss';
|
||||
|
||||
interface ModalWrapperProps {
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
title: string;
|
||||
}
|
||||
|
||||
export default function ModalWrapper(props: PropsWithChildren<ModalWrapperProps>) {
|
||||
const { isOpen, onClose, title, children } = props;
|
||||
|
||||
return (
|
||||
<Modal
|
||||
onClose={onClose}
|
||||
isOpen={isOpen}
|
||||
closeOnOverlayClick={false}
|
||||
motionPreset='slideInBottom'
|
||||
size='xl'
|
||||
scrollBehavior='inside'
|
||||
preserveScrollBarGap
|
||||
variant='ontime'
|
||||
>
|
||||
<ModalOverlay />
|
||||
<ModalContent>
|
||||
<ModalHeader>{title}</ModalHeader>
|
||||
<ModalCloseButton />
|
||||
{children}
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
@use '../../theme/main' as *;
|
||||
@use '../../theme/ontimeColours' as *;
|
||||
@use '../../theme/v2Styles' as *;
|
||||
|
||||
//////////////////////////////////// main
|
||||
|
||||
@@ -7,7 +8,7 @@
|
||||
|
||||
.notes {
|
||||
font-weight: 400;
|
||||
color: $light-bg;
|
||||
color: $action-blue;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
height: 4em;
|
||||
@@ -16,7 +17,7 @@
|
||||
.modalFields {
|
||||
max-height: 45vh;
|
||||
overflow-y: auto;
|
||||
scrollbar-color: rgba($light-bg, 0.35) rgba($light-bg, 0.15);
|
||||
scrollbar-color: rgba($action-blue, 0.35) rgba($action-blue, 0.15);
|
||||
padding-right: 6px;
|
||||
|
||||
label {
|
||||
@@ -42,7 +43,7 @@
|
||||
grid-template-columns: 20% 1fr 4em;
|
||||
|
||||
.placeholder {
|
||||
background: $light-text;
|
||||
background: black;
|
||||
width: 100%;
|
||||
height: 24px;
|
||||
}
|
||||
@@ -51,19 +52,19 @@
|
||||
|
||||
/* Track */
|
||||
::-webkit-scrollbar-track {
|
||||
background: rgba($light-bg, 0.15);
|
||||
background: rgba($gray-50, 0.15);
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
/* Handle */
|
||||
::-webkit-scrollbar-thumb {
|
||||
background: rgba($light-bg, 0.35);
|
||||
background: rgba($gray-100, 0.35);
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
/* Handle on hover */
|
||||
::-webkit-scrollbar-thumb:hover {
|
||||
background: rgba($light-bg, 0.45);
|
||||
background: rgba($gray-200, 0.45);
|
||||
}
|
||||
|
||||
.modalInline {
|
||||
@@ -99,9 +100,8 @@
|
||||
padding-top: 2em;
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 1em;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
.modalBody > * {
|
||||
@@ -115,7 +115,7 @@ ul.featureList {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
svg {
|
||||
color: $ontime-accent-text;
|
||||
color: black;
|
||||
margin-right: 4px;
|
||||
}
|
||||
}
|
||||
@@ -124,7 +124,7 @@ ul.featureList {
|
||||
p {
|
||||
&.notes {
|
||||
text-align: center;
|
||||
border-color: $light-bg-transparent;
|
||||
border-color: black;
|
||||
border-width: 0 2px;
|
||||
font-size: 0.9em;
|
||||
margin-bottom: 1em;
|
||||
@@ -139,7 +139,7 @@ span {
|
||||
}
|
||||
|
||||
.blockNotes {
|
||||
background-color: $bg-gray;
|
||||
background-color: $gray-1100;
|
||||
margin: 1em 0;
|
||||
padding: 0.5em;
|
||||
font-size: 0.8em;
|
||||
@@ -147,7 +147,7 @@ span {
|
||||
|
||||
table {
|
||||
background-color: #fff;
|
||||
border-left: 4px solid lighten($ontime-pink, 5%);
|
||||
border-left: 4px solid lighten($ontime-color, 5%);
|
||||
width: 100%;
|
||||
margin: 0.5em 0;
|
||||
border-radius: 2px;
|
||||
@@ -181,12 +181,12 @@ span {
|
||||
}
|
||||
|
||||
.labelNote {
|
||||
color: $light-bg;
|
||||
color: $action-blue;
|
||||
padding-right: 1em;
|
||||
}
|
||||
|
||||
.labelNoteInline {
|
||||
color: $light-bg;
|
||||
color: $action-blue;
|
||||
}
|
||||
|
||||
.inlineFlex {
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
import { Tab, TabList, TabPanel, TabPanels, Tabs } from '@chakra-ui/react';
|
||||
|
||||
import ModalWrapper from '../ModalWrapper';
|
||||
|
||||
import OscIntegrationSettings from './OscIntegrationSettings';
|
||||
import OscSettingsModal from './OscSettingsModal';
|
||||
|
||||
import styles from '../Modal.module.scss';
|
||||
|
||||
interface IntegrationModalProps {
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
export default function IntegrationModal(props: IntegrationModalProps) {
|
||||
const { isOpen, onClose } = props;
|
||||
|
||||
return (
|
||||
<ModalWrapper title='Integration Settings' isOpen={isOpen} onClose={onClose}>
|
||||
<div className={styles.headerNotes}>
|
||||
Manage settings related to protocol integrations. <br />
|
||||
Changes take effect on app restart.
|
||||
</div>
|
||||
<Tabs variant='ontime' size='sm' isLazy>
|
||||
<TabList>
|
||||
<Tab>OSC</Tab>
|
||||
<Tab>Old OSC</Tab>
|
||||
</TabList>
|
||||
<TabPanels>
|
||||
<TabPanel>
|
||||
<OscIntegrationSettings />
|
||||
</TabPanel>
|
||||
<TabPanel>
|
||||
<OscSettingsModal />
|
||||
</TabPanel>
|
||||
</TabPanels>
|
||||
</Tabs>
|
||||
</ModalWrapper>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,181 @@
|
||||
import { useContext } from 'react';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import { Button, FormControl, Input, ModalBody, ModalFooter, Switch } from '@chakra-ui/react';
|
||||
|
||||
import { postOSC } from '../../../common/api/ontimeApi';
|
||||
import { LoggingContext } from '../../../common/context/LoggingContext';
|
||||
import useOscSettings from '../../../common/hooks-query/useOscSettings';
|
||||
import { PlaceholderSettings } from '../../../common/models/OscSettings.type';
|
||||
import { isIPAddress, isOnlyNumbers } from '../../../common/utils/regex';
|
||||
|
||||
import styles from '../Modal.module.scss';
|
||||
|
||||
export default function OscIntegrationSettings() {
|
||||
const { data } = useOscSettings();
|
||||
const { emitError } = useContext(LoggingContext);
|
||||
const {
|
||||
handleSubmit,
|
||||
register,
|
||||
reset,
|
||||
setError,
|
||||
formState: { errors, isSubmitting, isDirty, isValid },
|
||||
} = useForm<PlaceholderSettings>({
|
||||
defaultValues: data,
|
||||
values: data,
|
||||
});
|
||||
|
||||
const disableSubmit = isSubmitting || !isDirty || !isValid;
|
||||
|
||||
const onSubmit = async (values: PlaceholderSettings) => {
|
||||
const numericPortIn = Number(values.portIn);
|
||||
const numericPortOut = Number(values.portOut);
|
||||
|
||||
if (numericPortIn === numericPortOut) {
|
||||
setError('portIn', { message: 'OSC IN and OUT Ports cant be the same' });
|
||||
return;
|
||||
}
|
||||
|
||||
const parsedValues = {
|
||||
...values,
|
||||
portIn: numericPortIn,
|
||||
portOut: numericPortOut,
|
||||
};
|
||||
|
||||
try {
|
||||
await postOSC(parsedValues);
|
||||
} catch (error) {
|
||||
emitError(`Error setting OSC: ${error}`);
|
||||
}
|
||||
};
|
||||
|
||||
const resetForm = () => reset(data);
|
||||
|
||||
return (
|
||||
<>
|
||||
<form onSubmit={handleSubmit(onSubmit)} className={styles.sectionContainer} id='test'>
|
||||
<ModalBody>
|
||||
<div className={styles.splitSection}>
|
||||
<div>
|
||||
<span className={`${styles.sectionTitle} ${styles.main}`}>OSC Input</span>
|
||||
<span className={styles.sectionSubtitle}>Control Ontime with OSC</span>
|
||||
</div>
|
||||
<Switch {...register('enabledIn')} variant='ontime-on-light' />
|
||||
</div>
|
||||
|
||||
<FormControl isInvalid={!!errors.portIn} className={styles.splitSection}>
|
||||
<label htmlFor='portIn'>
|
||||
<span className={styles.sectionTitle}>Listen on Port</span>
|
||||
{errors.portIn ? (
|
||||
<span className={styles.error}>{errors.portIn.message}</span>
|
||||
) : (
|
||||
<span className={styles.sectionSubtitle}>Default 8888</span>
|
||||
)}
|
||||
</label>
|
||||
<Input
|
||||
id='portIn'
|
||||
placeholder='8888'
|
||||
width='75px'
|
||||
size='sm'
|
||||
textAlign='right'
|
||||
maxLength={5}
|
||||
{...register('portIn', {
|
||||
required: { value: true, message: 'Required field' },
|
||||
max: { value: 65535, message: 'Port in incorrect range (1024 - 65535)' },
|
||||
min: { value: 1024, message: 'Port in incorrect range (1024 - 65535)' },
|
||||
pattern: {
|
||||
value: isOnlyNumbers,
|
||||
message: 'Value should be numeric',
|
||||
},
|
||||
})}
|
||||
/>
|
||||
</FormControl>
|
||||
|
||||
<hr className={styles.divider} />
|
||||
|
||||
<div className={styles.splitSection}>
|
||||
<div>
|
||||
<span className={styles.sectionTitle} style={{ fontWeight: 600 }}>
|
||||
OSC Output
|
||||
</span>
|
||||
<span className={styles.sectionSubtitle}>Ontime data feedback</span>
|
||||
</div>
|
||||
<Switch {...register('enabledOut')} variant='ontime-on-light' />
|
||||
</div>
|
||||
|
||||
<FormControl isInvalid={!!errors.targetIP} className={styles.splitSection}>
|
||||
<label htmlFor='targetIP'>
|
||||
<span className={styles.sectionTitle}>OSC target IP</span>
|
||||
{errors.targetIP ? (
|
||||
<span className={styles.error}>{errors.targetIP.message}</span>
|
||||
) : (
|
||||
<span className={styles.sectionSubtitle}>Default 127.0.0.1</span>
|
||||
)}
|
||||
</label>
|
||||
<Input
|
||||
id='targetIP'
|
||||
placeholder='127.0.0.1'
|
||||
width='140px'
|
||||
size='sm'
|
||||
textAlign='right'
|
||||
{...register('targetIP', {
|
||||
required: { value: true, message: 'Required field' },
|
||||
pattern: {
|
||||
value: isIPAddress,
|
||||
message: 'Invalid IP address',
|
||||
},
|
||||
})}
|
||||
/>
|
||||
</FormControl>
|
||||
|
||||
<FormControl className={styles.splitSection}>
|
||||
<label htmlFor='portOut'>
|
||||
<span className={styles.sectionTitle}>OSC target Port</span>
|
||||
{errors.portOut ? (
|
||||
<span className={styles.error}>{errors.portOut.message}</span>
|
||||
) : (
|
||||
<span className={styles.sectionSubtitle}>Default 9999</span>
|
||||
)}
|
||||
</label>
|
||||
<Input
|
||||
id='portOut'
|
||||
placeholder='9999'
|
||||
width='75px'
|
||||
size='sm'
|
||||
textAlign='right'
|
||||
maxLength={5}
|
||||
{...register('portOut', {
|
||||
required: { value: true, message: 'Required field' },
|
||||
max: { value: 65535, message: 'Port in incorrect range (1024 - 65535)' },
|
||||
min: { value: 1024, message: 'Port in incorrect range (1024 - 65535)' },
|
||||
pattern: {
|
||||
value: isOnlyNumbers,
|
||||
message: 'Value should be numeric',
|
||||
},
|
||||
})}
|
||||
/>
|
||||
</FormControl>
|
||||
</ModalBody>
|
||||
{/*<ModalSubmitFooter />*/}
|
||||
</form>
|
||||
<ModalFooter className={styles.buttonSection}>
|
||||
<Button variant='ontime-ghost-on-light' size='sm' onClick={resetForm}>
|
||||
Revert to saved
|
||||
</Button>
|
||||
<Button variant='ontime-subtle-on-light' size='sm'>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
variant='ontime-filled'
|
||||
type='submit'
|
||||
form='test'
|
||||
disabled={disableSubmit}
|
||||
isLoading={isSubmitting}
|
||||
padding='0 2em'
|
||||
size='sm'
|
||||
>
|
||||
Save
|
||||
</Button>
|
||||
</ModalFooter>
|
||||
</>
|
||||
);
|
||||
}
|
||||
+8
-9
@@ -2,16 +2,15 @@ import { useCallback, useContext, useEffect, useState } from 'react';
|
||||
import { FormControl, FormLabel, Input, ModalBody } from '@chakra-ui/react';
|
||||
import { IoInformationCircleOutline } from '@react-icons/all-files/io5/IoInformationCircleOutline';
|
||||
|
||||
import { postOSC } from '../../common/api/ontimeApi';
|
||||
import EnableBtn from '../../common/components/buttons/EnableBtn';
|
||||
import { LoggingContext } from '../../common/context/LoggingContext';
|
||||
import useOscSettings from '../../common/hooks-query/useOscSettings';
|
||||
import { oscPlaceholderSettings } from '../../common/models/OscSettings.type';
|
||||
import { postOSC } from '../../../common/api/ontimeApi';
|
||||
import EnableBtn from '../../../common/components/buttons/EnableBtn';
|
||||
import { LoggingContext } from '../../../common/context/LoggingContext';
|
||||
import useOscSettings from '../../../common/hooks-query/useOscSettings';
|
||||
import { oscPlaceholderSettings } from '../../../common/models/OscSettings.type';
|
||||
import { inputProps, portInputProps } from '../modalHelper';
|
||||
import SubmitContainer from '../SubmitContainer';
|
||||
|
||||
import { inputProps, portInputProps } from './modalHelper';
|
||||
import SubmitContainer from './SubmitContainer';
|
||||
|
||||
import style from './Modals.module.scss';
|
||||
import style from '../Modals.module.scss';
|
||||
|
||||
// currently defined endpoints
|
||||
// temporary
|
||||
@@ -9,4 +9,4 @@ export const portInputProps = {
|
||||
type: 'number',
|
||||
min: '1024',
|
||||
max: '65535',
|
||||
};
|
||||
};
|
||||
|
||||
@@ -7,6 +7,13 @@
|
||||
scrollbar-width: thin;
|
||||
}
|
||||
|
||||
// reset box-sizing
|
||||
*,
|
||||
*:before,
|
||||
*:after {
|
||||
box-sizing: inherit;
|
||||
}
|
||||
|
||||
body,
|
||||
html,
|
||||
.App {
|
||||
|
||||
@@ -2,7 +2,6 @@ import { StrictMode } from 'react';
|
||||
import { createRoot } from 'react-dom/client';
|
||||
import * as Sentry from '@sentry/react';
|
||||
import { BrowserTracing } from '@sentry/tracing';
|
||||
import { TimerType } from 'ontime-types';
|
||||
|
||||
import App from './App';
|
||||
import { ONTIME_VERSION } from './ONTIME_VERSION';
|
||||
@@ -20,10 +19,6 @@ Sentry.init({
|
||||
enabled: import.meta.env.PROD,
|
||||
});
|
||||
|
||||
// TODO: apply code and remove, refs PR #290
|
||||
const sharedType: TimerType = TimerType.CountDown;
|
||||
console.log('WIP', sharedType);
|
||||
|
||||
root.render(
|
||||
<StrictMode>
|
||||
<App />
|
||||
|
||||
@@ -35,6 +35,8 @@ $bg-container-onlight: $gray-100;
|
||||
$box-shadow-l1: rgba(0, 0, 0, 0.15) 0 3px 3px 0;
|
||||
$box-shadow-l2: rgba(0, 0, 0, 0.15) 0 3px 3px 0;
|
||||
|
||||
$modal-note-color: $gray-700;
|
||||
|
||||
// interface elements
|
||||
$border-color-ondark: $white-10;
|
||||
$element-inner-spacing: 4px;
|
||||
|
||||
@@ -1,17 +1,13 @@
|
||||
const commonStyles = {
|
||||
letterSpacing: '0.3px',
|
||||
fontWeight: '400',
|
||||
borderRadius: '3px',
|
||||
};
|
||||
|
||||
export const ontimeButtonFilled = {
|
||||
...commonStyles,
|
||||
background: '#2B5ABC', // $blue-700
|
||||
color: '#fff', // pure-white
|
||||
border: '1px solid #2B5ABC', // $blue-700
|
||||
_hover: {
|
||||
backgroundColor: '#0A43B9', // $blue-800
|
||||
border: '1px solid #0A43B9', // $blue-800
|
||||
_disabled: {
|
||||
background: '#2B5ABC', // $blue-700
|
||||
},
|
||||
},
|
||||
_active: {
|
||||
backgroundColor: '#0036A6', // blue-900
|
||||
@@ -20,7 +16,6 @@ export const ontimeButtonFilled = {
|
||||
};
|
||||
|
||||
export const ontimeButtonOutlined = {
|
||||
...commonStyles,
|
||||
backgroundColor: '#2d2d2d', // $gray-1100
|
||||
color: '#e2e2e2', // $blue-400
|
||||
border: '1px solid rgba(255, 255, 255, 0.10)', // white-10
|
||||
@@ -34,7 +29,6 @@ export const ontimeButtonOutlined = {
|
||||
};
|
||||
|
||||
export const ontimeButtonSubtle = {
|
||||
...commonStyles,
|
||||
backgroundColor: '#303030', // $gray-1050
|
||||
color: '#779BE7', // $blue-400
|
||||
border: '1px solid transparent',
|
||||
@@ -47,6 +41,32 @@ export const ontimeButtonSubtle = {
|
||||
},
|
||||
};
|
||||
|
||||
export const ontimeButtonSubtleOnLight = {
|
||||
backgroundColor: '#ececec', // $gray-100
|
||||
color: '#595959', // $gray-800
|
||||
border: '1px solid transparent',
|
||||
_hover: {
|
||||
backgroundColor: '#cfcfcf', // $gray-200
|
||||
},
|
||||
_active: {
|
||||
backgroundColor: '#ececec', // $gray-200
|
||||
borderColor: '#ececec', // $gray-300
|
||||
},
|
||||
};
|
||||
|
||||
export const ontimeGhostOnLight = {
|
||||
backgroundColor: 'transparent',
|
||||
color: '#595959', // $gray-800
|
||||
_hover: {
|
||||
color: '#595959', // $gray-800
|
||||
backgroundColor: '#ececec', // $gray-200
|
||||
},
|
||||
_active: {
|
||||
backgroundColor: 'transparent',
|
||||
borderColor: '#595959', // $gray-800
|
||||
},
|
||||
};
|
||||
|
||||
export const ontimeButtonSubtleWhite = {
|
||||
...ontimeButtonSubtle,
|
||||
color: '#f6f6f6', // $gray-50
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
export const ontimeModal = {
|
||||
header: {
|
||||
fontWeight: 400,
|
||||
letterSpacing: '0.3px',
|
||||
padding: '8px 16px',
|
||||
fontSize: '20px',
|
||||
color: '#202020', // $gray-50
|
||||
},
|
||||
dialog: {
|
||||
borderRadius: '3px',
|
||||
padding: 0,
|
||||
minHeight: 'min(500px, 75vh)',
|
||||
},
|
||||
body: {
|
||||
padding: 0,
|
||||
},
|
||||
closeButton: {
|
||||
color: '#202020', // $gray-50
|
||||
},
|
||||
};
|
||||
@@ -1,5 +1,4 @@
|
||||
export const ontimeSwitch = {
|
||||
container: { },
|
||||
track: {
|
||||
background: '#2d2d2d', // $gray-1100
|
||||
border: '1px solid transparent',
|
||||
@@ -8,7 +7,19 @@ export const ontimeSwitch = {
|
||||
},
|
||||
_focus: {
|
||||
border: '1px solid #578AF4', // $blue-500
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export const lightSwitch = {
|
||||
track: {
|
||||
border: '2px solid transparent',
|
||||
background: '#cfcfcf', // $gray-300
|
||||
_checked: {
|
||||
background: `#578AF4`, // $blue-500
|
||||
},
|
||||
_focus: {
|
||||
border: '2px solid #D2DDFF', // $blue-200
|
||||
},
|
||||
},
|
||||
thumb: {},
|
||||
};
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
export const ontimeTab = {
|
||||
tab: {
|
||||
fontWeight: 600,
|
||||
borderBottom: '2px solid transparent',
|
||||
color: '#9d9d9d', // $gray-500
|
||||
marginBottom: '-2px',
|
||||
_selected: {
|
||||
color: '#101010', // $ui-black
|
||||
border: 'none',
|
||||
borderBottom: '2px solid #779BE7', // $blue-400
|
||||
},
|
||||
},
|
||||
tablist: {
|
||||
borderBottom: '2px solid #ececec', // $gray-100
|
||||
},
|
||||
};
|
||||
@@ -4,24 +4,26 @@ import {
|
||||
ontimeButtonFilled,
|
||||
ontimeButtonOutlined,
|
||||
ontimeButtonSubtle,
|
||||
ontimeButtonSubtleOnLight,
|
||||
ontimeButtonSubtleWhite,
|
||||
ontimeGhostOnLight,
|
||||
} from './ontimeButton';
|
||||
import { ontimeCheckboxOnDark } from './ontimeCheckbox';
|
||||
import { ontimeEditable } from './ontimeEditable';
|
||||
import { ontimeMenuOnDark } from './ontimeMenu';
|
||||
import { ontimeModal } from './ontimeModal';
|
||||
import { ontimeSelect } from './ontimeSelect';
|
||||
import { ontimeSwitch } from './ontimeSwitch';
|
||||
import {
|
||||
ontimeInputFilled,
|
||||
ontimeTextAreaFilled,
|
||||
ontimeTextAreaFilledOnLight,
|
||||
} from './ontimeTextInputs';
|
||||
import { lightSwitch, ontimeSwitch } from './ontimeSwitch';
|
||||
import { ontimeTab } from './ontimeTab';
|
||||
import { ontimeInputFilled, ontimeTextAreaFilled, ontimeTextAreaFilledOnLight } from './ontimeTextInputs';
|
||||
import { ontimeTooltip } from './ontimeTooltip';
|
||||
|
||||
const theme = extendTheme({
|
||||
components: {
|
||||
Button: {
|
||||
baseStyle: {
|
||||
letterSpacing: '0.3px',
|
||||
fontWeight: '400',
|
||||
borderRadius: '3px',
|
||||
},
|
||||
variants: {
|
||||
@@ -29,6 +31,8 @@ const theme = extendTheme({
|
||||
'ontime-outlined': { ...ontimeButtonOutlined },
|
||||
'ontime-subtle': { ...ontimeButtonSubtle },
|
||||
'ontime-subtle-white': { ...ontimeButtonSubtleWhite },
|
||||
'ontime-subtle-on-light': { ...ontimeButtonSubtleOnLight },
|
||||
'ontime-ghost-on-light': { ...ontimeGhostOnLight },
|
||||
},
|
||||
},
|
||||
Checkbox: {
|
||||
@@ -38,7 +42,7 @@ const theme = extendTheme({
|
||||
},
|
||||
Editable: {
|
||||
variants: {
|
||||
'ontime': { ...ontimeEditable },
|
||||
ontime: { ...ontimeEditable },
|
||||
},
|
||||
},
|
||||
Input: {
|
||||
@@ -50,6 +54,16 @@ const theme = extendTheme({
|
||||
'ontime-filled': { ...ontimeInputFilled },
|
||||
},
|
||||
},
|
||||
Modal: {
|
||||
variants: {
|
||||
ontime: { ...ontimeModal },
|
||||
},
|
||||
},
|
||||
Tabs: {
|
||||
variants: {
|
||||
ontime: { ...ontimeTab },
|
||||
},
|
||||
},
|
||||
Textarea: {
|
||||
baseStyle: {
|
||||
borderRadius: '3px',
|
||||
@@ -60,16 +74,17 @@ const theme = extendTheme({
|
||||
},
|
||||
},
|
||||
Tooltip: {
|
||||
baseStyle: { ...ontimeTooltip},
|
||||
baseStyle: { ...ontimeTooltip },
|
||||
},
|
||||
Switch: {
|
||||
variants: {
|
||||
'ontime': { ...ontimeSwitch },
|
||||
ontime: { ...ontimeSwitch },
|
||||
'ontime-on-light': { ...lightSwitch },
|
||||
},
|
||||
},
|
||||
Select: {
|
||||
variants: {
|
||||
'ontime': { ...ontimeSelect },
|
||||
ontime: { ...ontimeSelect },
|
||||
},
|
||||
},
|
||||
Menu: {
|
||||
|
||||
@@ -48,13 +48,13 @@ let tray = null;
|
||||
|
||||
try {
|
||||
const ontimeServer = require(nodePath)
|
||||
const { startServer, startOSCServer } = ontimeServer;
|
||||
const { startDb, startServer, startOSCServer, startIntegrations } = ontimeServer;
|
||||
|
||||
await startDb();
|
||||
|
||||
// Start express server
|
||||
loaded = await startServer();
|
||||
|
||||
// Start OSC Server
|
||||
await startOSCServer();
|
||||
await startIntegrations();
|
||||
} catch (error) {
|
||||
loaded = error;
|
||||
}
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
"body-parser": "^1.20.0",
|
||||
"cors": "^2.8.5",
|
||||
"dotenv": "^16.0.1",
|
||||
"express": "^4.18.1",
|
||||
"express": "^4.18.2",
|
||||
"express-session": "^1.17.3",
|
||||
"express-validator": "^6.14.2",
|
||||
"lowdb": "^5.0.5",
|
||||
@@ -24,8 +24,9 @@
|
||||
"socket.io": "^4.5.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/express": "^4.17.15",
|
||||
"@types/express": "^4.17.17",
|
||||
"@types/node": "^16.11.7",
|
||||
"@types/node-osc": "^6.0.0",
|
||||
"@typescript-eslint/eslint-plugin": "^5.48.1",
|
||||
"@typescript-eslint/parser": "^5.48.1",
|
||||
"esbuild": "^0.17.5",
|
||||
|
||||
+77
-50
@@ -6,11 +6,11 @@ import cors from 'cors';
|
||||
// import utils
|
||||
import { join, resolve } from 'path';
|
||||
|
||||
import { config } from './config/config.js';
|
||||
import { initiateOSC, shutdownOSCServer } from './controllers/OscController.js';
|
||||
import { initSentry } from './modules/sentry.js';
|
||||
import { currentDirectory, environment, isProduction, resolvedPath, uiPath } from './setup.js';
|
||||
import { ONTIME_VERSION } from './ONTIME_VERSION.js';
|
||||
import { OSCSettings } from 'ontime-types';
|
||||
|
||||
// Import Routes
|
||||
import { router as rundownRouter } from './routes/rundownRouter.js';
|
||||
@@ -22,12 +22,9 @@ import { router as playbackRouter } from './routes/playbackRouter.js';
|
||||
import { DataProvider } from './classes/data-provider/DataProvider.js';
|
||||
import { socketProvider } from './classes/socket/SocketController.js';
|
||||
import { eventTimer } from './services/TimerService.js';
|
||||
import { promise } from './modules/loadDb.js';
|
||||
import { TimerType } from 'ontime-types';
|
||||
|
||||
// TODO: apply code and remove, refs PR #290
|
||||
const sharedType: TimerType = TimerType.CountDown;
|
||||
console.log('WIP', sharedType);
|
||||
import { dbLoadingProcess } from './modules/loadDb.js';
|
||||
import { integrationService } from './services/integration-service/IntegrationService.js';
|
||||
import { OscIntegration } from './services/integration-service/OscIntegration.js';
|
||||
|
||||
console.log(`Starting Ontime version ${ONTIME_VERSION}`);
|
||||
|
||||
@@ -77,6 +74,7 @@ app.use((error, response) => {
|
||||
});
|
||||
|
||||
/*************** START SERVICES ***************/
|
||||
|
||||
/* Override config
|
||||
* ----------------
|
||||
*
|
||||
@@ -84,42 +82,35 @@ app.use((error, response) => {
|
||||
* It can be overridden here by the settings in the db
|
||||
* It can also be overridden on call
|
||||
*
|
||||
* Start order
|
||||
* ----------------
|
||||
*
|
||||
* The services need to be started in a certain order,
|
||||
* the enum below enforces that
|
||||
*/
|
||||
(async () => {
|
||||
try {
|
||||
await promise;
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
|
||||
enum OntimeStartOrder {
|
||||
Error,
|
||||
InitDB,
|
||||
InitServer,
|
||||
InitIO,
|
||||
}
|
||||
|
||||
let step = OntimeStartOrder.InitDB;
|
||||
const checkStart = (currentState) => {
|
||||
if (step !== currentState) {
|
||||
step = OntimeStartOrder.Error;
|
||||
throw new Error('Init order error: startDb > startServer > startOsc > startIntegrations');
|
||||
} else {
|
||||
if (step === 1 || step === 2) {
|
||||
step = step + 1;
|
||||
}
|
||||
}
|
||||
})();
|
||||
};
|
||||
|
||||
const { osc } = DataProvider.getData();
|
||||
const oscIP = osc?.targetIP || config.osc.targetIP;
|
||||
const oscOutPort = osc?.portOut || config.osc.portOut;
|
||||
const oscInPort = osc?.port || config.osc.port;
|
||||
const oscInEnabled = osc?.enabled !== undefined ? osc.enabled : config.osc.inputEnabled;
|
||||
const serverPort = 4001; // hardcoded for now
|
||||
|
||||
/**
|
||||
* @description starts OSC server
|
||||
* @description starts OSC server
|
||||
* @param overrideConfig
|
||||
* @return {Promise<void>}
|
||||
*/
|
||||
export const startOSCServer = async (overrideConfig = null) => {
|
||||
if (!oscInEnabled) {
|
||||
socketServer.info('RX', 'OSC Input Disabled');
|
||||
return;
|
||||
}
|
||||
|
||||
// Setup default port
|
||||
const oscSettings = {
|
||||
port: overrideConfig?.port || oscInPort,
|
||||
};
|
||||
|
||||
// Start OSC Server
|
||||
socketServer.info('RX', `Starting OSC Server on port: ${oscInPort}`);
|
||||
initiateOSC(oscSettings);
|
||||
export const startDb = async () => {
|
||||
checkStart(OntimeStartOrder.InitDB);
|
||||
await dbLoadingProcess;
|
||||
};
|
||||
|
||||
// create HTTP server
|
||||
@@ -130,32 +121,67 @@ const expressServer = http.createServer(app);
|
||||
* @return {Promise<string>}
|
||||
*/
|
||||
export const startServer = async () => {
|
||||
// Start server
|
||||
checkStart(OntimeStartOrder.InitServer);
|
||||
|
||||
const serverPort = 4001; // hardcoded for now
|
||||
const returnMessage = `Ontime is listening on port ${serverPort}`;
|
||||
expressServer.listen(serverPort, '0.0.0.0');
|
||||
|
||||
// init socket controller
|
||||
await socketServer.initServer(expressServer);
|
||||
socketServer.initServer(expressServer);
|
||||
socketServer.info('SERVER', 'Socket initialised');
|
||||
|
||||
socketServer.info('SERVER', returnMessage);
|
||||
socketServer.startListener();
|
||||
|
||||
return returnMessage;
|
||||
};
|
||||
|
||||
/**
|
||||
* starts integrations
|
||||
* @description starts OSC server
|
||||
* @description starts OSC server
|
||||
* @param overrideConfig
|
||||
* @return {Promise<void>}
|
||||
*/
|
||||
export const startIntegrations = async (overrideConfig = null) => {
|
||||
const { http } = DataProvider.getData();
|
||||
export const startOSCServer = async (overrideConfig = null) => {
|
||||
checkStart(OntimeStartOrder.InitIO);
|
||||
|
||||
// OSC Config
|
||||
const oscConfig = {
|
||||
ip: oscIP,
|
||||
port: overrideConfig?.port || oscOutPort,
|
||||
const { osc } = DataProvider.getData();
|
||||
|
||||
if (!osc.enabledIn) {
|
||||
socketServer.info('RX', 'OSC Input Disabled');
|
||||
return;
|
||||
}
|
||||
|
||||
// Setup default port
|
||||
const oscSettings = {
|
||||
...osc,
|
||||
portIn: overrideConfig?.port || osc.portIn,
|
||||
};
|
||||
|
||||
// Start OSC Server
|
||||
socketServer.info('RX', `Starting OSC Server on port: ${oscSettings.portIn}`);
|
||||
initiateOSC(oscSettings);
|
||||
};
|
||||
|
||||
/**
|
||||
* starts integrations
|
||||
*/
|
||||
export const startIntegrations = async (config?: { osc: OSCSettings }) => {
|
||||
checkStart(OntimeStartOrder.InitIO);
|
||||
|
||||
const { osc } = config ?? DataProvider.getData();
|
||||
|
||||
if (!osc) {
|
||||
return 'OSC Invalid configuration';
|
||||
}
|
||||
|
||||
const oscIntegration = new OscIntegration();
|
||||
const { success, message } = oscIntegration.init(osc);
|
||||
socketServer.info('RX', message);
|
||||
|
||||
if (success) {
|
||||
integrationService.register(oscIntegration);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -170,6 +196,7 @@ export const shutdown = async (exitCode = 0) => {
|
||||
shutdownOSCServer();
|
||||
eventTimer.shutdown();
|
||||
socketServer.shutdown();
|
||||
integrationService.shutdown();
|
||||
process.exit(exitCode);
|
||||
};
|
||||
|
||||
|
||||
+5
-33
@@ -3,6 +3,7 @@
|
||||
* and adds logic specific to ontime data
|
||||
*/
|
||||
import { data, db } from '../../modules/loadDb.js';
|
||||
import { safeMerge } from './DataProvider.utils.js';
|
||||
|
||||
export class DataProvider {
|
||||
static getData() {
|
||||
@@ -39,6 +40,7 @@ export class DataProvider {
|
||||
}
|
||||
|
||||
static async deleteEvent(eventId) {
|
||||
// @ts-expect-error -- this will go away once we type db
|
||||
data.rundown = Array.from(data.rundown).filter((e) => e.id !== eventId);
|
||||
await this.persist();
|
||||
}
|
||||
@@ -49,6 +51,7 @@ export class DataProvider {
|
||||
|
||||
static async clearRundown() {
|
||||
data.rundown = [];
|
||||
// @ts-expect-error -- not sure how to type, this is library side
|
||||
await db.write();
|
||||
}
|
||||
|
||||
@@ -149,11 +152,12 @@ export class DataProvider {
|
||||
}
|
||||
|
||||
static async persist() {
|
||||
// @ts-expect-error -- not sure how to type, this is library side
|
||||
await db.write();
|
||||
}
|
||||
|
||||
static async mergeIntoData(newData) {
|
||||
const mergedData = DataProvider.safeMerge(data, newData);
|
||||
const mergedData = safeMerge(data, newData);
|
||||
data.event = mergedData.event;
|
||||
data.settings = mergedData.settings;
|
||||
data.osc = mergedData.osc;
|
||||
@@ -163,36 +167,4 @@ export class DataProvider {
|
||||
data.rundown = mergedData.rundown;
|
||||
await this.persist();
|
||||
}
|
||||
|
||||
/**
|
||||
* Merges two data objects
|
||||
* @param {object} existing
|
||||
* @param {object} newData
|
||||
*/
|
||||
static safeMerge(existing, newData) {
|
||||
const mergedData = { ...existing };
|
||||
|
||||
if (typeof newData?.rundown !== 'undefined') {
|
||||
mergedData.rundown = newData.rundown;
|
||||
}
|
||||
if (typeof newData?.event !== 'undefined') {
|
||||
mergedData.event = { ...newData.event };
|
||||
}
|
||||
if (typeof newData?.settings !== 'undefined') {
|
||||
mergedData.settings = { ...newData.settings };
|
||||
}
|
||||
if (typeof newData?.osc !== 'undefined') {
|
||||
mergedData.osc = { ...newData.osc };
|
||||
}
|
||||
if (typeof newData?.http !== 'undefined') {
|
||||
mergedData.http = { ...newData.http };
|
||||
}
|
||||
if (typeof newData?.aliases !== 'undefined') {
|
||||
mergedData.aliases = [...newData.aliases];
|
||||
}
|
||||
if (typeof newData?.userFields !== 'undefined') {
|
||||
mergedData.userFields = { ...existing.userFields, ...newData.userFields };
|
||||
}
|
||||
return mergedData;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
/**
|
||||
* Merges two data objects
|
||||
* @param {object} existing
|
||||
* @param {object} newData
|
||||
*/
|
||||
export function safeMerge(existing, newData) {
|
||||
const { rundown, event, settings, osc, http, aliases, userFields } = newData || {};
|
||||
return {
|
||||
...existing,
|
||||
rundown: rundown ?? existing.rundown,
|
||||
event: { ...existing.event, ...event },
|
||||
settings: { ...existing.settings, ...settings },
|
||||
views: {
|
||||
overrideStyles: false,
|
||||
},
|
||||
aliases: aliases ?? existing.aliases,
|
||||
userFields: {
|
||||
...existing.userFields,
|
||||
...(userFields && Object.fromEntries(Object.entries(userFields).filter(([_, value]) => value !== null))),
|
||||
},
|
||||
osc: {
|
||||
...existing.osc,
|
||||
...osc,
|
||||
subscriptions: {
|
||||
...existing.osc?.subscriptions,
|
||||
...(newData?.osc?.subscriptions || {}),
|
||||
...(existing.osc?.subscriptions && newData?.osc?.subscriptions
|
||||
? Object.keys(existing.osc.subscriptions).reduce((acc, key) => {
|
||||
if (!(key in newData.osc.subscriptions)) {
|
||||
acc[key] = existing.osc.subscriptions[key];
|
||||
}
|
||||
return acc;
|
||||
}, {})
|
||||
: {}),
|
||||
},
|
||||
},
|
||||
http: { ...existing.http, ...http },
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,302 @@
|
||||
import { safeMerge } from '../DataProvider.utils.js';
|
||||
|
||||
describe('safeMerge', () => {
|
||||
const existing = {
|
||||
rundown: [],
|
||||
event: {
|
||||
title: 'existing title',
|
||||
publicUrl: 'existing public URL',
|
||||
backstageUrl: 'existing backstageUrl',
|
||||
backstageInfo: 'existing backstageInfo',
|
||||
endMessage: 'existing endMessage',
|
||||
},
|
||||
settings: {
|
||||
app: 'ontime',
|
||||
lock: true,
|
||||
pinCode: null,
|
||||
timeFormat: '24',
|
||||
version: 2,
|
||||
serverPort: 4001,
|
||||
},
|
||||
views: {
|
||||
overrideStyles: false,
|
||||
},
|
||||
aliases: [],
|
||||
userFields: {
|
||||
user0: 'existing user0',
|
||||
user1: 'existing user1',
|
||||
},
|
||||
osc: {
|
||||
portIn: 8888,
|
||||
portOut: 9999,
|
||||
targetIP: '127.0.0.1',
|
||||
enabledIn: false,
|
||||
enabledOut: false,
|
||||
subscriptions: {
|
||||
onLoad: {
|
||||
message: '',
|
||||
enabled: false,
|
||||
},
|
||||
onStart: {
|
||||
message: '',
|
||||
enabled: false,
|
||||
},
|
||||
onPause: {
|
||||
message: '',
|
||||
enabled: false,
|
||||
},
|
||||
onStop: {
|
||||
message: '',
|
||||
enabled: false,
|
||||
},
|
||||
onUpdate: {
|
||||
message: '',
|
||||
enabled: false,
|
||||
},
|
||||
onFinish: {
|
||||
message: '',
|
||||
enabled: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
http: {
|
||||
enabled: true,
|
||||
user: null,
|
||||
pwd: null,
|
||||
},
|
||||
};
|
||||
|
||||
it('returns existing data if new data is not provided', () => {
|
||||
const mergedData = safeMerge(existing, undefined);
|
||||
expect(mergedData).toEqual(existing);
|
||||
});
|
||||
|
||||
it('merges the rundown key', () => {
|
||||
const newData = {
|
||||
rundown: [{ name: 'item 1' }, { name: 'item 2' }],
|
||||
};
|
||||
const mergedData = safeMerge(existing, newData);
|
||||
expect(mergedData.rundown).toEqual(newData.rundown);
|
||||
});
|
||||
|
||||
it('merges the event key', () => {
|
||||
const newData = {
|
||||
event: {
|
||||
title: 'new title',
|
||||
publicInfo: 'new public info',
|
||||
},
|
||||
};
|
||||
const mergedData = safeMerge(existing, newData);
|
||||
expect(mergedData.event).toEqual({
|
||||
title: 'new title',
|
||||
publicUrl: 'existing public URL',
|
||||
publicInfo: 'new public info',
|
||||
backstageUrl: 'existing backstageUrl',
|
||||
backstageInfo: 'existing backstageInfo',
|
||||
endMessage: 'existing endMessage',
|
||||
});
|
||||
});
|
||||
|
||||
it('merges the settings key', () => {
|
||||
const newData = {
|
||||
settings: {
|
||||
serverPort: 3000,
|
||||
lock: '1234',
|
||||
},
|
||||
};
|
||||
const mergedData = safeMerge(existing, newData);
|
||||
expect(mergedData.settings).toEqual({
|
||||
app: 'ontime',
|
||||
version: 2,
|
||||
serverPort: 3000,
|
||||
lock: '1234',
|
||||
pinCode: null,
|
||||
timeFormat: '24',
|
||||
});
|
||||
});
|
||||
|
||||
it('merges the osc key', () => {
|
||||
const newData = {
|
||||
osc: {
|
||||
portIn: 7777,
|
||||
subscriptions: {
|
||||
onStart: {
|
||||
message: 'new message',
|
||||
enabled: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
const mergedData = safeMerge(existing, newData);
|
||||
expect(mergedData.osc).toEqual({
|
||||
portIn: 7777,
|
||||
portOut: 9999,
|
||||
targetIP: '127.0.0.1',
|
||||
enabledIn: false,
|
||||
enabledOut: false,
|
||||
subscriptions: {
|
||||
onLoad: {
|
||||
message: '',
|
||||
enabled: false,
|
||||
},
|
||||
onStart: {
|
||||
message: 'new message',
|
||||
enabled: true,
|
||||
},
|
||||
onPause: {
|
||||
message: '',
|
||||
enabled: false,
|
||||
},
|
||||
onStop: {
|
||||
message: '',
|
||||
enabled: false,
|
||||
},
|
||||
onUpdate: {
|
||||
message: '',
|
||||
enabled: false,
|
||||
},
|
||||
onFinish: {
|
||||
message: '',
|
||||
enabled: false,
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('should merge the aliases key when present', () => {
|
||||
const existingData = {
|
||||
rundown: [],
|
||||
event: {
|
||||
title: '',
|
||||
publicUrl: '',
|
||||
publicInfo: '',
|
||||
backstageUrl: '',
|
||||
backstageInfo: '',
|
||||
endMessage: '',
|
||||
},
|
||||
settings: {
|
||||
app: 'ontime',
|
||||
version: 2,
|
||||
serverPort: 4001,
|
||||
lock: null,
|
||||
pinCode: null,
|
||||
timeFormat: '24',
|
||||
},
|
||||
views: {
|
||||
overrideStyles: false,
|
||||
},
|
||||
aliases: [],
|
||||
userFields: {
|
||||
user0: 'user0',
|
||||
user1: 'user1',
|
||||
user2: 'user2',
|
||||
user3: 'user3',
|
||||
user4: 'user4',
|
||||
user5: 'user5',
|
||||
user6: 'user6',
|
||||
user7: 'user7',
|
||||
user8: 'user8',
|
||||
user9: 'user9',
|
||||
},
|
||||
osc: {
|
||||
portIn: 8888,
|
||||
portOut: 9999,
|
||||
targetIP: '127.0.0.1',
|
||||
enabledIn: false,
|
||||
enabledOut: false,
|
||||
subscriptions: {
|
||||
onLoad: {
|
||||
message: '',
|
||||
enabled: false,
|
||||
},
|
||||
onStart: {
|
||||
message: '',
|
||||
enabled: false,
|
||||
},
|
||||
onPause: {
|
||||
message: '',
|
||||
enabled: false,
|
||||
},
|
||||
onStop: {
|
||||
message: '',
|
||||
enabled: false,
|
||||
},
|
||||
onUpdate: {
|
||||
message: '',
|
||||
enabled: false,
|
||||
},
|
||||
onFinish: {
|
||||
message: '',
|
||||
enabled: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
http: {
|
||||
user: null,
|
||||
pwd: null,
|
||||
messages: {
|
||||
onLoad: {
|
||||
url: '',
|
||||
enabled: false,
|
||||
},
|
||||
onStart: {
|
||||
url: '',
|
||||
enabled: false,
|
||||
},
|
||||
onUpdate: {
|
||||
url: '',
|
||||
enabled: false,
|
||||
},
|
||||
onPause: {
|
||||
url: '',
|
||||
enabled: false,
|
||||
},
|
||||
onStop: {
|
||||
url: '',
|
||||
enabled: false,
|
||||
},
|
||||
onFinish: {
|
||||
url: '',
|
||||
enabled: false,
|
||||
},
|
||||
},
|
||||
enabled: true,
|
||||
},
|
||||
};
|
||||
|
||||
const newData = {
|
||||
aliases: ['alias1', 'alias2'],
|
||||
};
|
||||
|
||||
const mergedData = safeMerge(existingData, newData);
|
||||
|
||||
expect(mergedData.aliases).toEqual(newData.aliases);
|
||||
});
|
||||
|
||||
it('merges userFields into existing object', () => {
|
||||
const existing = {
|
||||
userFields: {
|
||||
user0: 'Alice',
|
||||
user1: 'Bob',
|
||||
},
|
||||
};
|
||||
|
||||
const newData = {
|
||||
userFields: {
|
||||
user2: 'Charlie',
|
||||
user3: 'David',
|
||||
user4: null,
|
||||
},
|
||||
};
|
||||
|
||||
const expected = {
|
||||
user0: 'Alice',
|
||||
user1: 'Bob',
|
||||
user2: 'Charlie',
|
||||
user3: 'David',
|
||||
};
|
||||
|
||||
const result = safeMerge(existing, newData);
|
||||
expect(result.userFields).toEqual(expected);
|
||||
});
|
||||
});
|
||||
@@ -1,4 +1,4 @@
|
||||
import { DataProvider } from '../data-provider/DataProvider.js';
|
||||
import { DataProvider } from '../data-provider/DataProvider.ts';
|
||||
import { getRollTimers } from '../../services/rollUtils.js';
|
||||
|
||||
let instance;
|
||||
|
||||
@@ -1,53 +0,0 @@
|
||||
import * as http from 'http';
|
||||
|
||||
/**
|
||||
* @description Class contains logic towards outgoing HTTP communications
|
||||
* @class
|
||||
*/
|
||||
export class HTTPIntegration {
|
||||
constructor() {
|
||||
// nothing to do here
|
||||
}
|
||||
|
||||
/**
|
||||
* @description Initializes oscClient
|
||||
* @param {object} httpConfig - Http configurations options
|
||||
*/
|
||||
init(httpConfig) {}
|
||||
|
||||
/**
|
||||
* @description Sends http get request from predefined messages
|
||||
* @param {string} path - complete http path
|
||||
*/
|
||||
async send(path) {
|
||||
if (path == null) {
|
||||
console.log('HTTP ERROR: Message undefined');
|
||||
return;
|
||||
}
|
||||
|
||||
const options = new URL(path);
|
||||
let str = '';
|
||||
|
||||
const req = http.request(options, (res) => {
|
||||
console.log(`statusCode: ${res.statusCode}`);
|
||||
|
||||
res.on('data', function (chunk) {
|
||||
str += chunk;
|
||||
});
|
||||
|
||||
res.on('end', function () {
|
||||
console.log(str);
|
||||
});
|
||||
});
|
||||
|
||||
req.on('error', (error) => {
|
||||
console.error(error);
|
||||
});
|
||||
|
||||
req.end();
|
||||
}
|
||||
|
||||
shutdown() {
|
||||
/* Nothing to shutdown */
|
||||
}
|
||||
}
|
||||
@@ -1,169 +0,0 @@
|
||||
import { Client, Message } from 'node-osc';
|
||||
|
||||
/**
|
||||
* @description Class contains logic towards outgoing OSC communications
|
||||
* @class
|
||||
*/
|
||||
export class OSCIntegration {
|
||||
constructor() {
|
||||
// OSC Client
|
||||
this.ADDRESS = '/ontime';
|
||||
this.oscClient = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @description Returns list of implemented messages
|
||||
* @returns {object} implemented messages
|
||||
*/
|
||||
get implemented() {
|
||||
return {
|
||||
play: 'play',
|
||||
pause: 'pause',
|
||||
stop: 'stop',
|
||||
previous: 'prev',
|
||||
next: 'next',
|
||||
reload: 'reload',
|
||||
finished: 'finished',
|
||||
time: 'time',
|
||||
overtime: 'overtime',
|
||||
title: 'title',
|
||||
eventNumber: 'eventNumber',
|
||||
presenter: 'presenter',
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* @description Initializes oscClient
|
||||
* @param {object} oscConfig - oscClient configuration options
|
||||
* @param {string} oscConfig.ip - oscClient object
|
||||
* @param {number} oscConfig.port - OSC Destination Port
|
||||
*/
|
||||
init(oscConfig) {
|
||||
const { ip, port } = oscConfig;
|
||||
const validateType = typeof ip !== 'string' || typeof port !== 'number';
|
||||
const validateNull = ip == null || port == null;
|
||||
|
||||
if (validateType || validateNull) {
|
||||
return {
|
||||
success: false,
|
||||
message: `Config options incorrect`,
|
||||
};
|
||||
}
|
||||
try {
|
||||
this.oscClient = new Client(ip, port);
|
||||
return {
|
||||
success: true,
|
||||
message: `Initialised OSC Client at ${ip}:${port}`,
|
||||
};
|
||||
} catch (error) {
|
||||
this.oscClient = null;
|
||||
return {
|
||||
success: false,
|
||||
message: `Failed initialising OSC Client: ${error}`,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @description Sends osc from predefined messages
|
||||
* @param {string} messageType - message to be sent
|
||||
* @param {string} [payload] - optional payload required in some message types
|
||||
*/
|
||||
async send(messageType, payload) {
|
||||
const reply = {
|
||||
success: true,
|
||||
message: 'OSC Message sent',
|
||||
};
|
||||
|
||||
if (this.oscClient == null) {
|
||||
reply.success = false;
|
||||
reply.message = 'Client not initialised';
|
||||
return reply;
|
||||
}
|
||||
|
||||
if (messageType == null) {
|
||||
reply.success = false;
|
||||
reply.message = 'Message undefined';
|
||||
return reply;
|
||||
}
|
||||
|
||||
// only specify special cases
|
||||
switch (payload) {
|
||||
case 'overtime': {
|
||||
// Whether timer is negative
|
||||
this.oscClient.send(`${this.ADDRESS}/overtime`, payload, (err) => {
|
||||
if (err) {
|
||||
reply.success = false;
|
||||
reply.message = err;
|
||||
}
|
||||
});
|
||||
break;
|
||||
}
|
||||
case 'title': {
|
||||
if (payload != null && payload !== '') {
|
||||
// Send Title of current event
|
||||
this.oscClient.send(`${this.ADDRESS}/title`, payload, (err) => {
|
||||
if (err) {
|
||||
reply.success = false;
|
||||
reply.message = err;
|
||||
}
|
||||
});
|
||||
} else {
|
||||
reply.success = false;
|
||||
reply.message = 'Missing message data';
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 'eventNumber': {
|
||||
if (payload != null && payload !== '') {
|
||||
// Send event number of current event
|
||||
this.oscClient.send(`${this.ADDRESS}/eventNumber`, payload, (err) => {
|
||||
if (err) {
|
||||
reply.success = false;
|
||||
reply.message = err;
|
||||
}
|
||||
});
|
||||
} else {
|
||||
reply.success = false;
|
||||
reply.message = 'Missing message data';
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 'presenter': {
|
||||
if (payload != null && payload !== '') {
|
||||
// Send timer data on current event
|
||||
this.oscClient.send(`${this.ADDRESS}/presenter`, payload, (err) => {
|
||||
if (err) {
|
||||
reply.success = false;
|
||||
reply.message = err;
|
||||
}
|
||||
});
|
||||
} else {
|
||||
reply.success = false;
|
||||
reply.message = 'Missing message data';
|
||||
}
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
// catch all for messages, allows to add new messages
|
||||
// but should be used with the integrations definition
|
||||
const message = new Message(`${this.ADDRESS}/${messageType}`);
|
||||
if (payload != null) message.append(payload);
|
||||
this.oscClient.send(message, (err) => {
|
||||
if (err) {
|
||||
reply.success = false;
|
||||
reply.message = err;
|
||||
}
|
||||
});
|
||||
break;
|
||||
}
|
||||
}
|
||||
return reply;
|
||||
}
|
||||
|
||||
shutdown() {
|
||||
// Shutdown client object
|
||||
this.oscClient.close();
|
||||
this.oscClient = null;
|
||||
}
|
||||
}
|
||||
@@ -1,163 +0,0 @@
|
||||
|
||||
import { OSCIntegration } from '../Osc';
|
||||
import { Server } from 'node-osc';
|
||||
|
||||
test('Class initialises correctly', () => {
|
||||
const osc = new OSCIntegration();
|
||||
expect(osc.ADDRESS).toBe('/ontime');
|
||||
expect(osc.oscClient).toBe(null);
|
||||
|
||||
// defined objects
|
||||
expect(osc.implemented.play).toBeDefined();
|
||||
expect(osc.implemented.pause).toBeDefined();
|
||||
expect(osc.implemented.stop).toBeDefined();
|
||||
expect(osc.implemented.previous).toBeDefined();
|
||||
expect(osc.implemented.next).toBeDefined();
|
||||
expect(osc.implemented.reload).toBeDefined();
|
||||
expect(osc.implemented.finished).toBeDefined();
|
||||
expect(osc.implemented.time).toBeDefined();
|
||||
expect(osc.implemented.overtime).toBeDefined();
|
||||
expect(osc.implemented.title).toBeDefined();
|
||||
expect(osc.implemented.eventNumber).toBeDefined();
|
||||
expect(osc.implemented.presenter).toBeDefined();
|
||||
|
||||
// initialise client succeeds
|
||||
const { ip, port } = { ip: '127.0.0.1', port: 12345 };
|
||||
const init = osc.init({ ip, port });
|
||||
expect(init.message).toBe(`Initialised OSC Client at ${ip}:${port}`);
|
||||
expect(init.success).toBe(true);
|
||||
expect(osc.oscClient).not.toBe(null);
|
||||
|
||||
// object shutdown as expected
|
||||
osc.shutdown();
|
||||
expect(osc.oscClient).toBe(null);
|
||||
});
|
||||
|
||||
describe('OSC fails to initialise when incorrect data is given', () => {
|
||||
it('IP of wrong type', () => {
|
||||
const osc = new OSCIntegration();
|
||||
const init = osc.init({ ip: 123, port: 8888 });
|
||||
expect(init.message).toBe('Config options incorrect');
|
||||
expect(init.success).toBe(false);
|
||||
expect(osc.oscClient).toBe(null);
|
||||
});
|
||||
|
||||
it('IP is null', () => {
|
||||
const osc = new OSCIntegration();
|
||||
const init = osc.init({ ip: null, port: 8888 });
|
||||
expect(init.message).toBe('Config options incorrect');
|
||||
expect(init.success).toBe(false);
|
||||
expect(osc.oscClient).toBe(null);
|
||||
});
|
||||
|
||||
it('Port of wrong type', () => {
|
||||
const osc = new OSCIntegration();
|
||||
const init = osc.init({ ip: 'localhost', port: 'test' });
|
||||
expect(init.message).toBe('Config options incorrect');
|
||||
expect(init.success).toBe(false);
|
||||
expect(osc.oscClient).toBe(null);
|
||||
});
|
||||
|
||||
it('Port is null', () => {
|
||||
const osc = new OSCIntegration();
|
||||
const init = osc.init({ ip: 'localhost', port: null });
|
||||
expect(init.message).toBe('Config options incorrect');
|
||||
expect(init.success).toBe(false);
|
||||
expect(osc.oscClient).toBe(null);
|
||||
});
|
||||
});
|
||||
|
||||
test('Test messages sending', async () => {
|
||||
const testPort = 9999;
|
||||
const testIP = 'localhost';
|
||||
const testPayload = 'test';
|
||||
const osc = new OSCIntegration();
|
||||
|
||||
const messages = [];
|
||||
|
||||
// prepare dummy server to receive messages
|
||||
const oscServer = new Server(testPort, testIP);
|
||||
|
||||
oscServer.on('message', (m) => {
|
||||
messages.push({ yay: m });
|
||||
});
|
||||
|
||||
// try and send a message before initialising
|
||||
const test = await osc.send('test');
|
||||
expect(test.success).toBe(false);
|
||||
expect(test.message).toBe('Client not initialised');
|
||||
|
||||
// initialise osc
|
||||
osc.init({ ip: testIP, port: testPort });
|
||||
|
||||
// try and send unrecognised message
|
||||
const test2 = await osc.send('test');
|
||||
expect(test2.success).toBe(true);
|
||||
|
||||
// send play message
|
||||
const playAddress = osc.implemented.play;
|
||||
const playSent = await osc.send(playAddress);
|
||||
expect(playSent.success).toBe(true);
|
||||
|
||||
// send pause message
|
||||
const pauseAddress = osc.implemented.pause;
|
||||
const pauseSent = await osc.send(pauseAddress);
|
||||
expect(pauseSent.success).toBe(true);
|
||||
|
||||
// send stop message
|
||||
const stopAddress = osc.implemented.stop;
|
||||
const stopSent = await osc.send(stopAddress);
|
||||
expect(stopSent.success).toBe(true);
|
||||
|
||||
// send previous message
|
||||
const previousAddress = osc.implemented.previous;
|
||||
const previousSent = await osc.send(previousAddress);
|
||||
expect(previousSent.success).toBe(true);
|
||||
|
||||
// send next message
|
||||
const nextAddress = osc.implemented.next;
|
||||
const nextSent = await osc.send(nextAddress);
|
||||
expect(nextSent.success).toBe(true);
|
||||
|
||||
// send reload message
|
||||
const reloadAddress = osc.implemented.reload;
|
||||
const reloadSent = await osc.send(reloadAddress);
|
||||
expect(reloadSent.success).toBe(true);
|
||||
|
||||
// send finished message
|
||||
const finishedAddress = osc.implemented.finished;
|
||||
const finishedSent = await osc.send(finishedAddress);
|
||||
expect(finishedSent.success).toBe(true);
|
||||
|
||||
// send time message
|
||||
const timeAddress = osc.implemented.time;
|
||||
const timeSent = await osc.send(timeAddress);
|
||||
expect(timeSent.success).toBe(true);
|
||||
|
||||
// send overtime message
|
||||
const overtimeAddress = osc.implemented.overtime;
|
||||
const overtimeSent = await osc.send(overtimeAddress, testPayload);
|
||||
expect(overtimeSent.success).toBe(true);
|
||||
|
||||
// send title message
|
||||
const titleAddress = osc.implemented.title;
|
||||
const titleSent = await osc.send(titleAddress, testPayload);
|
||||
expect(titleSent.success).toBe(true);
|
||||
|
||||
// send eventNumber message
|
||||
const eventNumberAddress = osc.implemented.eventNumber;
|
||||
const eventNumberSent = await osc.send(eventNumberAddress, testPayload);
|
||||
expect(eventNumberSent.success).toBe(true);
|
||||
|
||||
// send timer message
|
||||
const presenterAddress = osc.implemented.presenter;
|
||||
const presenterSent = await osc.send(presenterAddress, testPayload);
|
||||
expect(presenterSent.success).toBe(true);
|
||||
|
||||
// cleanup
|
||||
await osc.shutdown();
|
||||
await oscServer.close();
|
||||
|
||||
// see messagesObject
|
||||
// expect(messages.length).toBe(5);
|
||||
});
|
||||
@@ -7,7 +7,7 @@ import { messageManager } from '../message-manager/MessageManager.js';
|
||||
import { PlaybackService } from '../../services/PlaybackService.js';
|
||||
|
||||
import { ADDRESS_MESSAGE_CONTROL } from './socketConfig.js';
|
||||
import { eventTimer, TimerService } from '../../services/TimerService.js';
|
||||
import { eventTimer, TimerService } from '../../services/TimerService.ts';
|
||||
import { EventLoader, eventLoader } from '../event-loader/EventLoader.js';
|
||||
|
||||
class SocketController {
|
||||
|
||||
@@ -1,25 +1,8 @@
|
||||
export const config = {
|
||||
timer: {
|
||||
refresh: 1000,
|
||||
},
|
||||
server: {
|
||||
port: 4001,
|
||||
},
|
||||
database: {
|
||||
testdb: 'test-db',
|
||||
directory: 'preloaded-db',
|
||||
filename: 'db.json',
|
||||
tablename: 'events',
|
||||
},
|
||||
osc: {
|
||||
port: 8888,
|
||||
portOut: 9999,
|
||||
targetIP: '127.0.0.1',
|
||||
inputEnabled: true,
|
||||
},
|
||||
http: {
|
||||
user: '',
|
||||
pwd: '',
|
||||
enabled: true,
|
||||
},
|
||||
};
|
||||
|
||||
+7
-9
@@ -1,4 +1,6 @@
|
||||
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';
|
||||
@@ -14,11 +16,10 @@ export const shutdownOSCServer = () => {
|
||||
};
|
||||
|
||||
/**
|
||||
* @description initialises OSC server
|
||||
* @param {object} config
|
||||
* Initialises OSC server
|
||||
*/
|
||||
export const initiateOSC = (config) => {
|
||||
oscServer = new Server(config.port, '0.0.0.0');
|
||||
export const initiateOSC = (config: OSCSettings) => {
|
||||
oscServer = new Server(config.portIn, '0.0.0.0');
|
||||
|
||||
oscServer.on('error', console.error);
|
||||
|
||||
@@ -34,7 +35,7 @@ export const initiateOSC = (config) => {
|
||||
|
||||
// get first part before (ontime)
|
||||
if (address !== 'ontime') {
|
||||
console.error('RX', `OSC IN: Message address ${address} not recognised`);
|
||||
console.error('RX', `OSC IN: Message address ${address} not recognised`, msg);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -123,10 +124,7 @@ export const initiateOSC = (config) => {
|
||||
try {
|
||||
const eventIndex = Number(args);
|
||||
if (isNaN(eventIndex) || eventIndex <= 0) {
|
||||
socketProvider.error(
|
||||
'RX',
|
||||
`OSC IN: event index not recognised or out of range ${eventIndex}`
|
||||
);
|
||||
socketProvider.error('RX', `OSC IN: event index not recognised or out of range ${eventIndex}`);
|
||||
} else {
|
||||
PlaybackService.loadByIndex(eventIndex - 1);
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
import { removeUndefined } from '../utils/parserUtils.js';
|
||||
import { failEmptyObjects } from '../utils/routerUtils.js';
|
||||
import { DataProvider } from '../classes/data-provider/DataProvider.js';
|
||||
import { DataProvider } from '../classes/data-provider/DataProvider.ts';
|
||||
|
||||
// Create controller for GET request to 'event'
|
||||
export const getEvent = async (req, res) => {
|
||||
|
||||
@@ -2,7 +2,7 @@ import fs from 'fs';
|
||||
import { networkInterfaces } from 'os';
|
||||
import { generateId } from 'ontime-utils';
|
||||
import { fileHandler } from '../utils/parser.js';
|
||||
import { DataProvider } from '../classes/data-provider/DataProvider.js';
|
||||
import { DataProvider } from '../classes/data-provider/DataProvider.ts';
|
||||
import { failEmptyObjects, failIsNotArray } from '../utils/routerUtils.js';
|
||||
import { mergeObject } from '../utils/parserUtils.js';
|
||||
import { PlaybackService } from '../services/PlaybackService.js';
|
||||
|
||||
@@ -65,10 +65,11 @@ export const validateSettings = [
|
||||
* @description Validates object for POST /ontime/osc
|
||||
*/
|
||||
export const validateOSC = [
|
||||
body('port').exists().isInt({ min: 0, max: 65353 }),
|
||||
body('portOut').exists().isInt({ min: 0, max: 65353 }),
|
||||
body('portIn').exists().isInt({ min: 1024, max: 65535 }),
|
||||
body('portOut').exists().isInt({ min: 1024, max: 65535 }),
|
||||
body('targetIP').exists().isIP(),
|
||||
body('enabled').exists().isBoolean(),
|
||||
body('enabledIn').exists().isBoolean(),
|
||||
body('enabledOut').exists().isBoolean(),
|
||||
(req, res, next) => {
|
||||
const errors = validationResult(req);
|
||||
if (!errors.isEmpty()) return res.status(422).json({ errors: errors.array() });
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { DataProvider } from '../classes/data-provider/DataProvider.js';
|
||||
import { DataProvider } from '../classes/data-provider/DataProvider.ts';
|
||||
import { failEmptyObjects } from '../utils/routerUtils.js';
|
||||
import {
|
||||
addEvent,
|
||||
|
||||
@@ -1,16 +1,14 @@
|
||||
// import { promise } from './modules/loadDb.js';
|
||||
import { startOSCServer, startServer } from './app.js';
|
||||
import { startDb, startIntegrations, startOSCServer, startServer } from './app.js';
|
||||
|
||||
async function startOntime() {
|
||||
try {
|
||||
// await promise;
|
||||
await startDb();
|
||||
|
||||
// Start express server
|
||||
const loaded = await startServer();
|
||||
console.log(loaded);
|
||||
|
||||
// Start OSC Server (API)
|
||||
await startOSCServer();
|
||||
await startIntegrations();
|
||||
} catch (error) {
|
||||
console.log('Error starting Ontime');
|
||||
console.log(error);
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
export const dbModel = {
|
||||
import { DatabaseModel } from 'ontime-types';
|
||||
|
||||
export const dbModel: DatabaseModel = {
|
||||
rundown: [],
|
||||
event: {
|
||||
title: '',
|
||||
@@ -33,10 +35,37 @@ export const dbModel = {
|
||||
user9: 'user9',
|
||||
},
|
||||
osc: {
|
||||
port: 8888,
|
||||
portIn: 8888,
|
||||
portOut: 9999,
|
||||
targetIP: '127.0.0.1',
|
||||
enabled: true,
|
||||
enabledIn: false,
|
||||
enabledOut: false,
|
||||
subscriptions: {
|
||||
onLoad: {
|
||||
message: '',
|
||||
enabled: false,
|
||||
},
|
||||
onStart: {
|
||||
message: '',
|
||||
enabled: false,
|
||||
},
|
||||
onPause: {
|
||||
message: '',
|
||||
enabled: false,
|
||||
},
|
||||
onStop: {
|
||||
message: '',
|
||||
enabled: false,
|
||||
},
|
||||
onUpdate: {
|
||||
message: '',
|
||||
enabled: false,
|
||||
},
|
||||
onFinish: {
|
||||
message: '',
|
||||
enabled: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
http: {
|
||||
user: null,
|
||||
@@ -1,6 +1,8 @@
|
||||
import { Low } from 'lowdb';
|
||||
import { JSONFile } from 'lowdb/node';
|
||||
import { copyFileSync, existsSync } from 'fs';
|
||||
import { DatabaseModel } from 'ontime-types';
|
||||
|
||||
import { ensureDirectory } from '../utils/fileManagement.js';
|
||||
import { validateFile } from '../utils/parserUtils.js';
|
||||
import { dbModel } from '../models/dataModel.js';
|
||||
@@ -51,7 +53,7 @@ const parseDb = async (fileToRead, adapterToUse) => {
|
||||
async function loadDb() {
|
||||
const dbInDisk = populateDb();
|
||||
|
||||
const adapter = new JSONFile(dbInDisk);
|
||||
const adapter = new JSONFile<DatabaseModel>(dbInDisk);
|
||||
const db = new Low(adapter);
|
||||
|
||||
const data = await parseDb(dbInDisk, db);
|
||||
@@ -63,11 +65,11 @@ async function loadDb() {
|
||||
}
|
||||
|
||||
export let db = {};
|
||||
export let data = {};
|
||||
export const promise = loadDb();
|
||||
export let data = {} as DatabaseModel;
|
||||
export const dbLoadingProcess = loadDb();
|
||||
|
||||
const init = async () => {
|
||||
const dbProvider = await promise;
|
||||
const dbProvider = await dbLoadingProcess;
|
||||
db = dbProvider.db;
|
||||
data = dbProvider.data;
|
||||
};
|
||||
@@ -3,7 +3,7 @@
|
||||
*/
|
||||
import { socketProvider } from '../classes/socket/SocketController.js';
|
||||
import { eventLoader, EventLoader } from '../classes/event-loader/EventLoader.js';
|
||||
import { eventTimer, TimerService } from './TimerService.js';
|
||||
import { eventTimer, TimerService } from './TimerService.ts';
|
||||
|
||||
/**
|
||||
* Service manages playback status of app
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { generateId } from 'ontime-utils';
|
||||
import { DataProvider } from '../classes/data-provider/DataProvider.js';
|
||||
import { DataProvider } from '../classes/data-provider/DataProvider.ts';
|
||||
import {
|
||||
block as blockDef,
|
||||
delay as delayDef,
|
||||
@@ -7,7 +7,7 @@ import {
|
||||
} from '../models/eventsDefinition.js';
|
||||
import { MAX_EVENTS } from '../settings.js';
|
||||
import { EventLoader, eventLoader } from '../classes/event-loader/EventLoader.js';
|
||||
import { eventTimer } from './TimerService.js';
|
||||
import { eventTimer } from './TimerService.ts';
|
||||
import { socketProvider } from '../classes/socket/SocketController.js';
|
||||
|
||||
/**
|
||||
|
||||
+37
-9
@@ -1,15 +1,40 @@
|
||||
import { TimerLifeCycle } from 'ontime-types';
|
||||
|
||||
import { runtimeState } from '../stores/EventStore.js';
|
||||
import { PlaybackService } from './PlaybackService.js';
|
||||
import { updateRoll } from './rollUtils.js';
|
||||
import { DAY_TO_MS } from '../utils/time.js';
|
||||
import { integrationService } from './integration-service/IntegrationService.js';
|
||||
|
||||
export class TimerService {
|
||||
private readonly _interval: NodeJS.Timer;
|
||||
|
||||
private playback: string;
|
||||
|
||||
private loadedTimerId: null;
|
||||
private _pausedInterval: number;
|
||||
private _pausedAt: number | null;
|
||||
private _secondaryTarget: number | null;
|
||||
|
||||
timer: {
|
||||
clock: number;
|
||||
current: number | null;
|
||||
elapsed: number | null;
|
||||
expectedFinish: number | null;
|
||||
addedTime: number;
|
||||
startedAt: number | null;
|
||||
finishedAt: number | null;
|
||||
secondaryTimer: number | null;
|
||||
selectedEventId: string | null;
|
||||
duration: number | null;
|
||||
};
|
||||
|
||||
/**
|
||||
* @constructor
|
||||
* @param {object} [timerConfig]
|
||||
* @param {number} [timerConfig.refresh]
|
||||
*/
|
||||
constructor(timerConfig) {
|
||||
constructor(timerConfig?) {
|
||||
this._clear();
|
||||
this._interval = setInterval(() => this.update(), timerConfig?.refresh || 1000);
|
||||
}
|
||||
@@ -45,7 +70,7 @@ export class TimerService {
|
||||
|
||||
return Math.max(
|
||||
this.timer.startedAt + this.timer.duration + this._pausedInterval + this.timer.addedTime,
|
||||
this.timer.startedAt
|
||||
this.timer.startedAt,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -64,6 +89,8 @@ export class TimerService {
|
||||
startedAt: null,
|
||||
finishedAt: null,
|
||||
secondaryTimer: null,
|
||||
selectedEventId: null,
|
||||
duration: null,
|
||||
};
|
||||
this.loadedTimerId = null;
|
||||
this._pausedInterval = 0;
|
||||
@@ -138,6 +165,7 @@ export class TimerService {
|
||||
_onLoad() {
|
||||
runtimeState.set('playback', this.playback);
|
||||
runtimeState.set('ontime-timer', this.timer);
|
||||
integrationService.dispatch(TimerLifeCycle.onLoad);
|
||||
}
|
||||
|
||||
start() {
|
||||
@@ -172,6 +200,7 @@ export class TimerService {
|
||||
_onStart() {
|
||||
runtimeState.set('playback', this.playback);
|
||||
runtimeState.set('ontime-timer', this.timer);
|
||||
integrationService.dispatch(TimerLifeCycle.onStart);
|
||||
}
|
||||
|
||||
pause() {
|
||||
@@ -188,6 +217,7 @@ export class TimerService {
|
||||
_onPause() {
|
||||
runtimeState.set('playback', this.playback);
|
||||
runtimeState.set('ontime-timer', this.timer);
|
||||
integrationService.dispatch(TimerLifeCycle.onPause);
|
||||
}
|
||||
|
||||
stop() {
|
||||
@@ -202,6 +232,7 @@ export class TimerService {
|
||||
_onStop() {
|
||||
runtimeState.set('playback', this.playback);
|
||||
runtimeState.set('ontime-timer', this.timer);
|
||||
integrationService.dispatch(TimerLifeCycle.onStop);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -249,8 +280,7 @@ export class TimerService {
|
||||
secondaryTimer: this.timer.secondaryTimer,
|
||||
_secondaryTarget: this._secondaryTarget,
|
||||
};
|
||||
const { updatedTimer, updatedSecondaryTimer, doRollLoad, isFinished } =
|
||||
updateRoll(tempCurrentTimer);
|
||||
const { updatedTimer, updatedSecondaryTimer, doRollLoad, isFinished } = updateRoll(tempCurrentTimer);
|
||||
|
||||
this.timer.current = updatedTimer;
|
||||
this.timer.secondaryTimer = updatedSecondaryTimer;
|
||||
@@ -272,11 +302,7 @@ export class TimerService {
|
||||
}
|
||||
|
||||
this.timer.current =
|
||||
this.timer.startedAt +
|
||||
this.timer.duration +
|
||||
this.timer.addedTime +
|
||||
this._pausedInterval -
|
||||
this.timer.clock;
|
||||
this.timer.startedAt + this.timer.duration + this.timer.addedTime + this._pausedInterval - this.timer.clock;
|
||||
this.timer.elapsed = this.timer.duration - this.timer.current;
|
||||
|
||||
if (this.playback === 'play' && this.timer.current <= 0 && this.timer.finishedAt === null) {
|
||||
@@ -294,11 +320,13 @@ export class TimerService {
|
||||
_onUpdate() {
|
||||
runtimeState.set('playback', this.playback);
|
||||
runtimeState.set('ontime-timer', this.timer);
|
||||
integrationService.dispatch(TimerLifeCycle.onUpdate);
|
||||
}
|
||||
|
||||
_onFinish() {
|
||||
runtimeState.set('playback', this.playback);
|
||||
runtimeState.set('ontime-timer', this.timer);
|
||||
integrationService.dispatch(TimerLifeCycle.onFinish);
|
||||
}
|
||||
|
||||
roll(currentEvent, nextEvent, timers) {
|
||||
@@ -0,0 +1,24 @@
|
||||
import { TimerLifeCycle, OscSubscription } from 'ontime-types';
|
||||
|
||||
export type TimerLifeCycleKey = keyof typeof TimerLifeCycle;
|
||||
|
||||
export default interface IIntegration {
|
||||
subscriptions: OscSubscription;
|
||||
init: (config: unknown) => OperationReturn;
|
||||
dispatch: (action: TimerLifeCycleKey, state?: object) => OperationReturn;
|
||||
emit: (...args: unknown[]) => unknown;
|
||||
shutdown: () => void;
|
||||
}
|
||||
|
||||
// either went well, or explain what failed
|
||||
type OperationReturn = ReturnOnSuccess | ReturnOnError;
|
||||
|
||||
type ReturnOnSuccess = {
|
||||
success: true;
|
||||
message?: string;
|
||||
};
|
||||
|
||||
type ReturnOnError = {
|
||||
success: false;
|
||||
message: string;
|
||||
};
|
||||
@@ -0,0 +1,34 @@
|
||||
import IIntegration, { TimerLifeCycleKey } from './IIntegration.js';
|
||||
import { runtimeState } from '../../stores/EventStore.js';
|
||||
|
||||
class IntegrationService {
|
||||
private integrations: IIntegration[];
|
||||
|
||||
constructor() {
|
||||
this.integrations = [];
|
||||
}
|
||||
|
||||
register(integrationService: IIntegration) {
|
||||
this.integrations.push(integrationService);
|
||||
}
|
||||
|
||||
unregister(integrationService: IIntegration) {
|
||||
this.integrations = this.integrations.filter((int) => int !== integrationService);
|
||||
}
|
||||
|
||||
dispatch(action: TimerLifeCycleKey) {
|
||||
const state = runtimeState.poll();
|
||||
this.integrations.forEach((integration) => {
|
||||
integration.dispatch(action, state);
|
||||
});
|
||||
}
|
||||
|
||||
shutdown() {
|
||||
console.log('Shutdown integrations');
|
||||
this.integrations.forEach((integration) => {
|
||||
integration.shutdown();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export const integrationService = new IntegrationService();
|
||||
@@ -0,0 +1,118 @@
|
||||
import { ArgumentType, Client, Message } from 'node-osc';
|
||||
import { OSCSettings, OscSubscription } from 'ontime-types';
|
||||
|
||||
import IIntegration, { TimerLifeCycleKey } from './IIntegration.js';
|
||||
import { parseTemplate } from './integrationUtils.js';
|
||||
import { isObject } from '../../utils/varUtils.js';
|
||||
|
||||
type Action = TimerLifeCycleKey | string;
|
||||
|
||||
/**
|
||||
* @description Class contains logic towards outgoing OSC communications
|
||||
* @class
|
||||
*/
|
||||
export class OscIntegration implements IIntegration {
|
||||
protected oscClient: null | Client;
|
||||
subscriptions: OscSubscription;
|
||||
|
||||
constructor() {
|
||||
this.oscClient = null;
|
||||
this.subscriptions = {};
|
||||
}
|
||||
|
||||
/**
|
||||
* Initializes oscClient
|
||||
*/
|
||||
init(config: OSCSettings) {
|
||||
const { targetIP, portOut, subscriptions } = config;
|
||||
|
||||
this.initSubscriptions(subscriptions);
|
||||
|
||||
// runtime validation
|
||||
const validateType = typeof targetIP !== 'string' || typeof portOut !== 'number';
|
||||
const validateNull = !targetIP || !portOut;
|
||||
|
||||
if (validateType || validateNull) {
|
||||
return {
|
||||
success: false,
|
||||
message: `Config options incorrect`,
|
||||
};
|
||||
}
|
||||
try {
|
||||
this.oscClient = new Client(targetIP, portOut);
|
||||
return {
|
||||
success: true,
|
||||
message: `OSC integration client connected to ${targetIP}:${portOut}`,
|
||||
};
|
||||
} catch (error) {
|
||||
this.oscClient = null;
|
||||
return {
|
||||
success: false,
|
||||
message: `Failed initialising OSC Client: ${error}`,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
initSubscriptions(subscriptionOptions: OscSubscription) {
|
||||
this.subscriptions = { ...this.subscriptions, ...subscriptionOptions };
|
||||
}
|
||||
|
||||
dispatch(action: Action, state?: object) {
|
||||
if (!this.oscClient) {
|
||||
return {
|
||||
success: false,
|
||||
message: 'Client not initialised',
|
||||
};
|
||||
}
|
||||
|
||||
if (!action) {
|
||||
return {
|
||||
success: false,
|
||||
message: 'OSC called with no action',
|
||||
};
|
||||
}
|
||||
|
||||
// check subscriptions for action
|
||||
const { enabled, message } = this.subscriptions?.[action] || {};
|
||||
if (enabled) {
|
||||
const parsedMessage = parseTemplate(message, state || {});
|
||||
this.emit('address/', parsedMessage);
|
||||
}
|
||||
}
|
||||
|
||||
emit(path: string, payload?: ArgumentType) {
|
||||
const message = new Message(path);
|
||||
if (payload) {
|
||||
try {
|
||||
if (isObject(payload)) {
|
||||
message.append(JSON.stringify(payload));
|
||||
} else {
|
||||
message.append(payload);
|
||||
}
|
||||
} catch (error) {
|
||||
console.log('OSC ERROR', error, payload);
|
||||
}
|
||||
}
|
||||
|
||||
this.oscClient.send(message, (error) => {
|
||||
if (error) {
|
||||
return {
|
||||
success: false,
|
||||
message: `error is here ${JSON.stringify(error)}`,
|
||||
};
|
||||
}
|
||||
return {
|
||||
success: true,
|
||||
message: 'OSC Message sent',
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
shutdown() {
|
||||
console.log('Shutting down OSC integration');
|
||||
if (this.oscClient) {
|
||||
this.oscClient?.close();
|
||||
this.oscClient = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
import { parseTemplate } from './integrationUtils.js';
|
||||
|
||||
describe('integrationUtils', () => {
|
||||
it('correctly parses a given string', () => {
|
||||
const mockState = { test: 'this' };
|
||||
const testString = 'That should replace {{test}}';
|
||||
const expected = `That should replace ${mockState.test}`;
|
||||
|
||||
const result = parseTemplate(testString, mockState);
|
||||
expect(result).toStrictEqual(expected);
|
||||
});
|
||||
|
||||
it('parses string with multiple variables', () => {
|
||||
const mockState = { test1: 'that', test2: 'this' };
|
||||
const testString = '{{test1}} should replace {{test2}}';
|
||||
const expected = `${mockState.test1} should replace ${mockState.test2}`;
|
||||
|
||||
const result = parseTemplate(testString, mockState);
|
||||
expect(result).toStrictEqual(expected);
|
||||
});
|
||||
|
||||
it('correctly parses a string without templates', () => {
|
||||
const testString = 'That should replace {test}';
|
||||
|
||||
const result = parseTemplate(testString, {});
|
||||
expect(result).toStrictEqual(testString);
|
||||
});
|
||||
|
||||
it('handles scenarios with missing variables', () => {
|
||||
// by failing to provide a value, we give visibility to
|
||||
// potential issues in the given string
|
||||
const mockState = { test1: 'that', test2: 'this' };
|
||||
const testString = '{{test1}} should replace {{test2}}, but not {{test3}}';
|
||||
const expected = `${mockState.test1} should replace ${mockState.test2}, but not {{test3}}`;
|
||||
|
||||
const result = parseTemplate(testString, mockState);
|
||||
expect(result).toStrictEqual(expected);
|
||||
});
|
||||
|
||||
it('doesnt yet handle nested variables', () => {
|
||||
const mockState = {
|
||||
timer: {
|
||||
time: '10',
|
||||
},
|
||||
enabled: 'is',
|
||||
};
|
||||
const testString = 'Timer {{enabled}} enabled with {{timer.time}}ms interval';
|
||||
const expected = 'Timer is enabled with {{timer.time}}ms interval';
|
||||
|
||||
const result = parseTemplate(testString, mockState);
|
||||
expect(result).toStrictEqual(expected);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,18 @@
|
||||
// any value inside double curly braces {{val}}
|
||||
const placeholderRegex = /{{(.*?)}}/g;
|
||||
|
||||
/**
|
||||
* Parses a templated string
|
||||
*/
|
||||
export function parseTemplate(template: string, state: object): string {
|
||||
let parsedTemplate = template;
|
||||
let match;
|
||||
while ((match = placeholderRegex.exec(template)) !== null) {
|
||||
const variableName = match[1];
|
||||
if (Object.hasOwn(state, variableName)) {
|
||||
parsedTemplate = parsedTemplate.replace(match[0], state[variableName]);
|
||||
}
|
||||
}
|
||||
|
||||
return parsedTemplate;
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
import { isObject } from '../varUtils.js';
|
||||
|
||||
describe('isObject', () => {
|
||||
const testCases = [1, 0, false, undefined, 'test', null, () => undefined, []];
|
||||
testCases.forEach((test) => {
|
||||
it(`recognises normal primitives ${test}`, () => {
|
||||
const result = isObject(test);
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
});
|
||||
});
|
||||
+21
-20
@@ -1,4 +1,6 @@
|
||||
import { generateId } from 'ontime-utils';
|
||||
import { OSCSettings } from 'ontime-types';
|
||||
|
||||
import { block as blockDef, delay as delayDef } from '../models/eventsDefinition.js';
|
||||
import { dbModel } from '../models/dataModel.js';
|
||||
import { validateEvent } from './parser.js';
|
||||
@@ -148,31 +150,27 @@ export const parseViews = (data, enforce) => {
|
||||
|
||||
/**
|
||||
* Parse osc portion of an entry
|
||||
* @param {object} data - data object
|
||||
* @param {boolean} enforce - whether to create a definition if one is missing
|
||||
* @returns {object} - event object data
|
||||
*/
|
||||
export const parseOsc = (data, enforce) => {
|
||||
let newOsc = {};
|
||||
export const parseOsc = (
|
||||
data: { osc?: Partial<OSCSettings> },
|
||||
enforce: boolean,
|
||||
): OSCSettings | Record<string, never> => {
|
||||
if ('osc' in data) {
|
||||
console.log('Found OSC definition, importing...');
|
||||
const s = data.osc;
|
||||
const osc = {};
|
||||
|
||||
if (s.port) osc.port = s.port;
|
||||
if (s.portOut) osc.portOut = s.portOut;
|
||||
if (s.targetIP) osc.targetIP = s.targetIP;
|
||||
if (typeof s.enabled !== 'undefined') osc.enabled = s.enabled;
|
||||
// write to db
|
||||
newOsc = {
|
||||
...dbModel.osc,
|
||||
...osc,
|
||||
const loadedConfig = data?.osc || {};
|
||||
return {
|
||||
portIn: loadedConfig.portIn ?? dbModel.osc.portIn,
|
||||
portOut: loadedConfig.portOut ?? dbModel.osc.portOut,
|
||||
targetIP: loadedConfig.targetIP ?? dbModel.osc.targetIP,
|
||||
enabledIn: loadedConfig.enabledIn ?? dbModel.osc.enabledIn,
|
||||
enabledOut: loadedConfig.enabledOut ?? dbModel.osc.enabledOut,
|
||||
subscriptions: loadedConfig.subscriptions ?? dbModel.osc.subscriptions,
|
||||
};
|
||||
} else if (enforce) {
|
||||
newOsc = { ...dbModel.osc };
|
||||
console.log(`Created OSC object in db`);
|
||||
}
|
||||
return newOsc;
|
||||
return { ...dbModel.osc };
|
||||
} else return {};
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -185,18 +183,21 @@ export const parseHttp = (data, enforce) => {
|
||||
const newHttp = {};
|
||||
if ('http' in data) {
|
||||
console.log('Found HTTP definition, importing...');
|
||||
const h = data.osc;
|
||||
const h = data.http;
|
||||
const http = {};
|
||||
|
||||
// @ts-expect-error -- not yet
|
||||
if (h.user) http.user = h.user;
|
||||
// @ts-expect-error -- not yet
|
||||
if (h.pwd) http.pwd = h.pwd;
|
||||
|
||||
// write to db
|
||||
// @ts-expect-error -- not yet
|
||||
newHttp.http = {
|
||||
...dbModel.http,
|
||||
...http,
|
||||
};
|
||||
} else if (enforce) {
|
||||
// @ts-expect-error -- not yet
|
||||
newHttp.http = { ...dbModel.http };
|
||||
console.log(`Created http object in db`);
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
export function isObject(variable: unknown): boolean {
|
||||
return typeof variable === 'object' && variable !== null && !Array.isArray(variable);
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
import { OSCSettings } from './core/OscSettings.type.js';
|
||||
|
||||
export type DatabaseModel = {
|
||||
rundown: any;
|
||||
event: any;
|
||||
settings: any;
|
||||
views: any;
|
||||
aliases: any;
|
||||
userFields: any;
|
||||
osc: OSCSettings;
|
||||
http: any;
|
||||
};
|
||||
+2
-2
@@ -1,7 +1,7 @@
|
||||
enum TimerType {
|
||||
enum TimerTypeType {
|
||||
CountDown = 'count-down',
|
||||
CountUp = 'count-up',
|
||||
Clock = 'clock'
|
||||
}
|
||||
|
||||
export default TimerType
|
||||
export default TimerTypeType
|
||||
@@ -0,0 +1,13 @@
|
||||
import { TimerLifeCycle } from './TimerLifecycle.type';
|
||||
|
||||
export type TimerLifeCycleKey = keyof typeof TimerLifeCycle;
|
||||
export type OscSubscription = { [key in TimerLifeCycleKey]?: { message: string; enabled: boolean } };
|
||||
|
||||
export interface OSCSettings {
|
||||
portIn: number;
|
||||
portOut: number;
|
||||
targetIP: string;
|
||||
enabledIn: boolean;
|
||||
enabledOut: boolean;
|
||||
subscriptions: OscSubscription;
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
export enum TimerLifeCycle {
|
||||
onLoad = 'onLoad',
|
||||
onStart = 'onStart',
|
||||
onPause = 'onPause',
|
||||
onStop = 'onStop',
|
||||
onUpdate = 'onUpdate',
|
||||
onFinish = 'onFinish',
|
||||
}
|
||||
@@ -1,3 +1,24 @@
|
||||
import TimerType from './definitions/TimerType.js';
|
||||
import TimerTypeType from './definitions/TimerType.type.js';
|
||||
import { DatabaseModel } from './definitions/DataModel.type.js';
|
||||
import { TimerLifeCycle } from './definitions/core/TimerLifecycle.type.js';
|
||||
import { OSCSettings, OscSubscription } from './definitions/core/OscSettings.type.js';
|
||||
|
||||
export { TimerType };
|
||||
// DATA MODEL
|
||||
export type { DatabaseModel };
|
||||
|
||||
// ---> Rundown
|
||||
export type { TimerTypeType };
|
||||
|
||||
// ---> Event
|
||||
// ---> Settings
|
||||
// ---> Views
|
||||
// ---> Aliases
|
||||
// ---> User Fields
|
||||
// ---> OSC
|
||||
export type { OscSubscription, OSCSettings };
|
||||
// ---> HTTP
|
||||
|
||||
// SERVER
|
||||
export { TimerLifeCycle };
|
||||
|
||||
// CLIENT
|
||||
|
||||
Generated
+25
-8
@@ -79,6 +79,7 @@ importers:
|
||||
react-beautiful-dnd: ^13.1.1
|
||||
react-dom: ^18.2.0
|
||||
react-fast-compare: ^3.2.0
|
||||
react-hook-form: ^7.43.1
|
||||
react-qr-code: ^2.0.11
|
||||
react-router-dom: ^6.3.0
|
||||
react-table: ^7.7.0
|
||||
@@ -116,6 +117,7 @@ importers:
|
||||
react-beautiful-dnd: 13.1.1_biqbaboplfbrettd7655fr4n2y
|
||||
react-dom: 18.2.0_react@18.2.0
|
||||
react-fast-compare: 3.2.0
|
||||
react-hook-form: 7.43.1_react@18.2.0
|
||||
react-qr-code: 2.0.11_react@18.2.0
|
||||
react-router-dom: 6.6.2_biqbaboplfbrettd7655fr4n2y
|
||||
react-table: 7.8.0_react@18.2.0
|
||||
@@ -177,8 +179,9 @@ importers:
|
||||
specifiers:
|
||||
'@sentry/node': ^7.24.1
|
||||
'@sentry/tracing': ^7.24.1
|
||||
'@types/express': ^4.17.15
|
||||
'@types/express': ^4.17.17
|
||||
'@types/node': ^16.11.7
|
||||
'@types/node-osc': ^6.0.0
|
||||
'@typescript-eslint/eslint-plugin': ^5.48.1
|
||||
'@typescript-eslint/parser': ^5.48.1
|
||||
body-parser: ^1.20.0
|
||||
@@ -187,7 +190,7 @@ importers:
|
||||
esbuild: ^0.17.5
|
||||
eslint: ^8.31.0
|
||||
eslint-plugin-prettier: ^4.2.1
|
||||
express: ^4.18.1
|
||||
express: ^4.18.2
|
||||
express-session: ^1.17.3
|
||||
express-validator: ^6.14.2
|
||||
lowdb: ^5.0.5
|
||||
@@ -225,8 +228,9 @@ importers:
|
||||
passport-local: 1.0.0
|
||||
socket.io: 4.5.4
|
||||
devDependencies:
|
||||
'@types/express': 4.17.15
|
||||
'@types/express': 4.17.17
|
||||
'@types/node': 16.18.11
|
||||
'@types/node-osc': 6.0.0
|
||||
'@typescript-eslint/eslint-plugin': 5.48.1_3jon24igvnqaqexgwtxk6nkpse
|
||||
'@typescript-eslint/parser': 5.48.1_iukboom6ndih5an6iafl45j2fe
|
||||
esbuild: 0.17.5
|
||||
@@ -2922,19 +2926,19 @@ packages:
|
||||
resolution: {integrity: sha512-WulqXMDUTYAXCjZnk6JtIHPigp55cVtDgDrO2gHRwhyJto21+1zbVCtOYB2L1F9w4qCQ0rOGWBnBe0FNTiEJIQ==}
|
||||
dev: true
|
||||
|
||||
/@types/express-serve-static-core/4.17.32:
|
||||
resolution: {integrity: sha512-aI5h/VOkxOF2Z1saPy0Zsxs5avets/iaiAJYznQFm5By/pamU31xWKL//epiF4OfUA2qTOc9PV6tCUjhO8wlZA==}
|
||||
/@types/express-serve-static-core/4.17.33:
|
||||
resolution: {integrity: sha512-TPBqmR/HRYI3eC2E5hmiivIzv+bidAfXofM+sbonAGvyDhySGw9/PQZFt2BLOrjUUR++4eJVpx6KnLQK1Fk9tA==}
|
||||
dependencies:
|
||||
'@types/node': 18.11.18
|
||||
'@types/qs': 6.9.7
|
||||
'@types/range-parser': 1.2.4
|
||||
dev: true
|
||||
|
||||
/@types/express/4.17.15:
|
||||
resolution: {integrity: sha512-Yv0k4bXGOH+8a+7bELd2PqHQsuiANB+A8a4gnQrkRWzrkKlb6KHaVvyXhqs04sVW/OWlbPyYxRgYlIXLfrufMQ==}
|
||||
/@types/express/4.17.17:
|
||||
resolution: {integrity: sha512-Q4FmmuLGBG58btUnfS1c1r/NQdlp3DMfGDGig8WhfpA2YRUtEkxAjkZb0yvplJGYdF1fsQ81iMDcH24sSCNC/Q==}
|
||||
dependencies:
|
||||
'@types/body-parser': 1.19.2
|
||||
'@types/express-serve-static-core': 4.17.32
|
||||
'@types/express-serve-static-core': 4.17.33
|
||||
'@types/qs': 6.9.7
|
||||
'@types/serve-static': 1.15.0
|
||||
dev: true
|
||||
@@ -3021,6 +3025,10 @@ packages:
|
||||
resolution: {integrity: sha512-iiUgKzV9AuaEkZqkOLDIvlQiL6ltuZd9tGcW3gwpnX8JbuiuhFlEGmmFXEXkN50Cvq7Os88IY2v0dkDqXYWVgA==}
|
||||
dev: true
|
||||
|
||||
/@types/node-osc/6.0.0:
|
||||
resolution: {integrity: sha512-25DwJOFe1KueUZz2oIURT3qCMQ28Jdvy9JqGz8d0mKM1Mlx0agHD9N3S0hMKajVCjw7TGtf3gGjbl5gDCFfIWQ==}
|
||||
dev: true
|
||||
|
||||
/@types/node/16.18.11:
|
||||
resolution: {integrity: sha512-3oJbGBUWuS6ahSnEq1eN2XrCyf4YsWI8OyCvo7c64zQJNplk3mO84t53o8lfTk+2ji59g5ycfc6qQ3fdHliHuA==}
|
||||
dev: true
|
||||
@@ -7462,6 +7470,15 @@ packages:
|
||||
use-sidecar: 1.1.2_kzbn2opkn2327fwg5yzwzya5o4
|
||||
dev: false
|
||||
|
||||
/react-hook-form/7.43.1_react@18.2.0:
|
||||
resolution: {integrity: sha512-+s3+s8LLytRMriwwuSqeLStVjRXFGxgjjx2jED7Z+wz1J/88vpxieRQGvJVvzrzVxshZ0BRuocFERb779m2kNg==}
|
||||
engines: {node: '>=12.22.0'}
|
||||
peerDependencies:
|
||||
react: ^16.8.0 || ^17 || ^18
|
||||
dependencies:
|
||||
react: 18.2.0
|
||||
dev: false
|
||||
|
||||
/react-is/16.13.1:
|
||||
resolution: {integrity: sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user