diff --git a/client/src/common/components/buttons/QuitIconBtn.jsx b/client/src/common/components/buttons/QuitIconBtn.tsx similarity index 83% rename from client/src/common/components/buttons/QuitIconBtn.jsx rename to client/src/common/components/buttons/QuitIconBtn.tsx index 301acaa48..5c35b1f37 100644 --- a/client/src/common/components/buttons/QuitIconBtn.jsx +++ b/client/src/common/components/buttons/QuitIconBtn.tsx @@ -6,19 +6,25 @@ import { AlertDialogFooter, AlertDialogHeader, AlertDialogOverlay, - Button, IconButton, Tooltip, + Button, + IconButton, + Tooltip, } from '@chakra-ui/react'; import { FiPower } from '@react-icons/all-files/fi/FiPower'; -import PropTypes from 'prop-types'; import { LoggingContext } from '../../context/LoggingContext'; +import { Size } from '../../models/UtilTypes'; -export default function QuitIconBtn(props) { +interface QuitIconBtnProps { + clickHandler: () => void; + size?: Size; +} +export default function QuitIconBtn(props: QuitIconBtnProps) { const { clickHandler, size = 'lg', ...rest } = props; const [isOpen, setIsOpen] = useState(false); const { emitInfo } = useContext(LoggingContext); const onClose = () => setIsOpen(false); - const cancelRef = useRef(); + const cancelRef = useRef(null); useEffect(() => { if (window.process?.type === 'renderer') { @@ -38,6 +44,7 @@ export default function QuitIconBtn(props) { <> } colorScheme='red' @@ -58,7 +65,7 @@ export default function QuitIconBtn(props) { This will shutdown the program and all running servers. Are you sure? - + ); } @@ -178,6 +184,7 @@ export default function EventList(props) { let eventIndex = -1; let previousEnd = 0; let thisEnd = 0; + let previousEventId = null; return (
@@ -198,6 +205,7 @@ export default function EventList(props) { eventIndex++; previousEnd = thisEnd; thisEnd = e.timeEnd; + previousEventId = e.id; } const isLast = index === events.length - 1; return ( @@ -226,6 +234,7 @@ export default function EventList(props) { diff --git a/client/src/features/editors/list/EventListItem.tsx b/client/src/features/editors/list/EventListItem.tsx index e9ab3a50e..474717012 100644 --- a/client/src/features/editors/list/EventListItem.tsx +++ b/client/src/features/editors/list/EventListItem.tsx @@ -1,12 +1,16 @@ import { useCallback, useContext } from 'react'; -import { defaultPublicAtom, startTimeIsLastEndAtom } from 'common/atoms/LocalEventSettings'; +import { + defaultPublicAtom, + editorEventId, + startTimeIsLastEndAtom, +} from 'common/atoms/LocalEventSettings'; import { LoggingContext } from 'common/context/LoggingContext'; import { useEventAction } from 'common/hooks/useEventAction'; import { OntimeEvent, OntimeEventEntry } from 'common/models/EventTypes'; import { Playstate } from 'common/models/OntimeTypes'; import { duplicateEvent } from 'common/utils/eventsManager'; import { calculateDuration } from 'common/utils/timesManager'; -import { useAtomValue } from 'jotai'; +import { useAtom, useAtomValue } from 'jotai'; import { CursorContext } from '../../../common/context/CursorContext'; import BlockBlock from '../block-block/BlockBlock'; @@ -40,6 +44,7 @@ export default function EventListItem(props: EventListItemProps) { const defaultPublic = useAtomValue(defaultPublicAtom); const { addEvent, updateEvent, deleteEvent } = useEventAction(); const { moveCursorTo } = useContext(CursorContext); + const [openId, setOpenId] = useAtom(editorEventId); // Create / delete new events type FieldValue = { @@ -75,6 +80,9 @@ export default function EventListItem(props: EventListItemProps) { } case 'delete': { deleteEvent(data.id); + if (openId === data.id) { + setOpenId(null); + } break; } case 'clone': { diff --git a/client/src/features/editors/list/EventListWrapper.tsx b/client/src/features/editors/list/EventListWrapper.tsx index fbec73cc4..613da0577 100644 --- a/client/src/features/editors/list/EventListWrapper.tsx +++ b/client/src/features/editors/list/EventListWrapper.tsx @@ -20,13 +20,15 @@ export default function EventListWrapper() { }, [emitError, isError]); return ( -
+ <> - {status === 'success' && data ? ( - - ) : ( - - )} -
+
+ {status === 'success' && data ? ( + + ) : ( + + )} +
+ ); } diff --git a/client/src/features/event-editor/EventEditor.jsx b/client/src/features/event-editor/EventEditor.jsx index 6f79142c0..c9bf1063d 100644 --- a/client/src/features/event-editor/EventEditor.jsx +++ b/client/src/features/event-editor/EventEditor.jsx @@ -145,10 +145,6 @@ export default function EventEditor() {
-
- - -
+
+ + +
- +
- Running on port 4001 + Ontime running on port 4001 {selected}
diff --git a/client/src/features/info/Info.module.scss b/client/src/features/info/Info.module.scss index 58be30b7c..d0ca345a4 100644 --- a/client/src/features/info/Info.module.scss +++ b/client/src/features/info/Info.module.scss @@ -9,13 +9,12 @@ .container { @include container; - border: 1px solid transparent; } .main { font-size: 0.9em; - color: $ontime-pink; + color: $label-gray; display: flex; justify-content: space-between; } diff --git a/client/src/features/menu/EventListMenu.module.scss b/client/src/features/menu/EventListMenu.module.scss index d7e7f7306..d57d3ceac 100644 --- a/client/src/features/menu/EventListMenu.module.scss +++ b/client/src/features/menu/EventListMenu.module.scss @@ -2,4 +2,5 @@ align-content: center; justify-content: space-between; gap: 8px; + padding-top: 24px; } diff --git a/client/src/features/viewers/ViewWrapper.jsx b/client/src/features/viewers/ViewWrapper.jsx index 8960ae5f0..425f3a019 100644 --- a/client/src/features/viewers/ViewWrapper.jsx +++ b/client/src/features/viewers/ViewWrapper.jsx @@ -2,7 +2,7 @@ import { useEffect, useMemo, useState } from 'react'; import { useSocket } from '../../common/context/socketContext'; -import useSubscription from '../../common/context/useSubscription'; +import useSubscription from '../../common/hooks/useSubscription'; import useEvent from '../../common/hooks-query/useEvent'; import useEventsList from '../../common/hooks-query/useEventsList'; import useViewSettings from '../../common/hooks-query/useViewSettings'; diff --git a/client/src/features/viewers/public/Public.scss b/client/src/features/viewers/public/Public.scss index f13abfa95..a8bfe290a 100644 --- a/client/src/features/viewers/public/Public.scss +++ b/client/src/features/viewers/public/Public.scss @@ -110,6 +110,13 @@ transition: 0.5s; transition-property: opacity; } + + .message { + font-size: 1.5vw; + line-height: 2vw; + white-space: pre-line; + overflow: hidden; + } } .clock-container { diff --git a/server/main.js b/server/main.js index c81cce4c9..e9d0ea52b 100644 --- a/server/main.js +++ b/server/main.js @@ -60,6 +60,7 @@ let tray = null; // Ensure there isn't another instance of the app running already const lock = app.requestSingleInstanceLock(); + if (!lock) { dialog.showErrorBox('Multiple instances', 'An instance if the App is already running.'); app.quit(); @@ -111,6 +112,7 @@ function createWindow() { win.setMenu(null); } +app.disableHardwareAcceleration(); app.whenReady().then(() => { // Set app title in windows if (process.platform === 'win32') { diff --git a/server/src/app.js b/server/src/app.js index b4b8fb829..513a80170 100644 --- a/server/src/app.js +++ b/server/src/app.js @@ -141,7 +141,7 @@ export const startServer = async (overrideConfig = null) => { // init timer global.timer = new EventTimer(socket, config.timer, oscConfig, http); - global.timer.setupWithEventList(events); + global.timer.setupWithEventList(events.filter((entry) => entry.type === 'event')); socket.info('SERVER', returnMessage); socket.startListener(); diff --git a/server/src/classes/data-provider/DataProvider.js b/server/src/classes/data-provider/DataProvider.js index e8d29920c..a2d3a162f 100644 --- a/server/src/classes/data-provider/DataProvider.js +++ b/server/src/classes/data-provider/DataProvider.js @@ -95,7 +95,9 @@ export class DataProvider { */ static async insertEventAfterId(entry, id) { const index = [...data.events].findIndex((event) => event.id === id); - await DataProvider.insertEventAt(entry, index + 1); + // eslint-disable-next-line no-unused-vars + const { _after, ...sanitisedEvent } = entry; + await DataProvider.insertEventAt(sanitisedEvent, index + 1); } static getSettings() { diff --git a/server/src/classes/timer/EventTimer.js b/server/src/classes/timer/EventTimer.js index afb3aa82e..d54025c45 100644 --- a/server/src/classes/timer/EventTimer.js +++ b/server/src/classes/timer/EventTimer.js @@ -772,40 +772,45 @@ export class EventTimer extends Timer { * @param previousId */ insertEventAfterId(event, previousId) { - // find object in events - const previousIndex = this._eventlist.findIndex((e) => e.id === previousId); - if (previousIndex === -1) { - throw 'Event not found'; - } - - if (previousIndex + 1 >= this._eventlist.length) { - this._eventlist.push(event); + if (typeof previousId === 'undefined') { + // Insert at beginning + this._eventlist.unshift(event); } else { - this._eventlist.splice(previousIndex + 1, 0, event); - } + // find object in events + const previousIndex = this._eventlist.findIndex((e) => e.id === previousId); + if (previousIndex === -1) { + throw 'Event not found'; + } - try { - // check if entry is running - if (event.id === this.selectedEventId) { - // handle reload selected - // Reload data if running - const type = - this.selectedEventId === event.id && this._startedAt != null ? 'reload' : 'load'; - this.loadEvent(this.selectedEventIndex, type); - } else if (event.id === this.nextEventId) { - // roll needs to recalculate - if (this.state === 'roll') { - this.rollLoad(); + if (previousIndex + 1 >= this._eventlist.length) { + this._eventlist.push(event); + } else { + this._eventlist.splice(previousIndex + 1, 0, event); + } + + try { + // check if entry is running + if (event.id === this.selectedEventId) { + // handle reload selected + // Reload data if running + const type = + this.selectedEventId === event.id && this._startedAt != null ? 'reload' : 'load'; + this.loadEvent(this.selectedEventIndex, type); + } else if (event.id === this.nextEventId) { + // roll needs to recalculate + if (this.state === 'roll') { + this.rollLoad(); + } } - } - // load titles - if ('title' in event || 'subtitle' in event || 'presenter' in event) { - this._loadTitlesNext(); - this._loadTitlesNow(); + // load titles + if ('title' in event || 'subtitle' in event || 'presenter' in event) { + this._loadTitlesNext(); + this._loadTitlesNow(); + } + } catch (error) { + this.socket.error('SERVER', error); } - } catch (error) { - this.socket.error('SERVER', error); } // update clients @@ -815,21 +820,6 @@ export class EventTimer extends Timer { this.runCycle(); } - /** - * @description inserts an event in the first position of the list - * @param event - */ - insertEventAtStart(event) { - // Insert at beginning - this._eventlist.unshift(event); - - // update clients - this.broadcastState(); - - // run cycle - this.runCycle(); - } - /** * Deleted an event from the list by its id * @param {string} eventId diff --git a/server/src/controllers/eventController.validate.js b/server/src/controllers/eventController.validate.js index 49861cae3..b99b0915c 100644 --- a/server/src/controllers/eventController.validate.js +++ b/server/src/controllers/eventController.validate.js @@ -1,11 +1,11 @@ import { body, validationResult } from 'express-validator'; export const eventSanitizer = [ - body('title').optional().isString().trim().escape(), - body('url').optional().isString().trim().escape(), - body('publicInfo').optional().isString().trim().escape(), - body('backstageInfo').optional().isString().trim().escape(), - body('endMessage').optional().isString().trim().escape(), + body('title').optional().isString().trim(), + body('url').optional().isString().trim(), + body('publicInfo').optional().isString().trim(), + body('backstageInfo').optional().isString().trim(), + body('endMessage').optional().isString().trim(), (req, res, next) => { const errors = validationResult(req); if (!errors.isEmpty()) return res.status(422).json({ errors: errors.array() }); diff --git a/server/src/controllers/eventsController.js b/server/src/controllers/eventsController.js index 413f3bc8a..9d15ec6dd 100644 --- a/server/src/controllers/eventsController.js +++ b/server/src/controllers/eventsController.js @@ -14,17 +14,20 @@ import { socketProvider } from '../classes/socket/SocketController.js'; const socket = socketProvider; async function _insertAndSync(newEvent) { - if (newEvent.order) { - const events = DataProvider.getEvents(); - await DataProvider.insertEventAt(newEvent, newEvent.order); - const previousId = events?.[newEvent.order - 1]?.id; - _insertEventInTimerAfterId(newEvent, previousId); - } else if (newEvent.after) { - await DataProvider.insertEventAfterId(newEvent, newEvent.after); - _insertEventInTimerAfterId(newEvent, newEvent.after); - } else { + const afterId = newEvent?.after; + if (typeof afterId === 'undefined') { await DataProvider.insertEventAt(newEvent, 0); - _insertEventInTimerAfterId(newEvent); + if (newEvent.type === 'event') { + _insertEventInTimerAfterId(newEvent); + } + } else { + delete newEvent.after; + await DataProvider.insertEventAfterId(newEvent, afterId); + if (newEvent.type === 'event') { + const events = DataProvider.getEvents(); + const { id } = getPreviousPlayable(events, newEvent.id); + _insertEventInTimerAfterId(newEvent, id); + } } } @@ -51,14 +54,10 @@ function _updateTimers() { * @private */ function _insertEventInTimerAfterId(event, previousId) { - if (typeof previousId === 'undefined') { - global.timer.insertEventAtStart(event); - } else { - try { - global.timer.insertEventAfterId(event, previousId); - } catch (error) { - socket.error('SERVER', `Unable to update object: ${error}`); - } + try { + global.timer.insertEventAfterId(event, previousId); + } catch (error) { + socket.error('SERVER', `Unable to update object: ${error}`); } } @@ -147,28 +146,29 @@ export const eventsPut = async (req, res) => { const eventDataFromRequest = req.body; const eventId = eventDataFromRequest.id; - const event = DataProvider.getEventById(eventId); + const eventInMemory = DataProvider.getEventById(eventId); - if (typeof event === 'undefined') { + if (typeof eventInMemory === 'undefined') { res.status(400).send(`No event with ID found`); return; } try { - const newData = await DataProvider.updateEventById(eventId, eventDataFromRequest); + const patchedObject = await DataProvider.updateEventById(eventId, eventDataFromRequest); - if (newData.skip) { - _deleteTimerId(eventId); - // if it is a skip, make sure it is deleted from timer - // event id might already not exist - } else { - try { - _updateTimersSingle(newData.id, eventDataFromRequest); - } catch (error) { - if (error === 'Event not found') { + if (patchedObject.type === 'event') { + if (patchedObject.skip) { + // if it is a skip, make sure it is deleted from timer + _deleteTimerId(patchedObject.id); + } else { + if (eventInMemory.skip) { + // if it was skipped before we add it to the timer const events = DataProvider.getEvents(); - const { id: previousId } = getPreviousPlayable(events, newData.id); - _insertEventInTimerAfterId(newData, previousId); + const { id } = getPreviousPlayable(events, patchedObject.id); + _insertEventInTimerAfterId(patchedObject, id); + } else { + // otherwise update as normal + _updateTimersSingle(patchedObject.id, patchedObject); } } } diff --git a/server/src/controllers/eventsController.validate.js b/server/src/controllers/eventsController.validate.js index cbf559ba3..f03d4f6af 100644 --- a/server/src/controllers/eventsController.validate.js +++ b/server/src/controllers/eventsController.validate.js @@ -1,5 +1,14 @@ import { body, param, validationResult } from 'express-validator'; +export const eventsPostValidator = [ + body('type').isString().exists().isIn(['event', 'delay', 'block']), + (req, res, next) => { + const errors = validationResult(req); + if (!errors.isEmpty()) return res.status(422).json({ errors: errors.array() }); + next(); + }, +]; + export const eventsPutValidator = [ body('id').isString().exists(), (req, res, next) => { diff --git a/server/src/controllers/ontimeController.js b/server/src/controllers/ontimeController.js index 07897b39c..6170d3189 100644 --- a/server/src/controllers/ontimeController.js +++ b/server/src/controllers/ontimeController.js @@ -56,7 +56,7 @@ const uploadAndParse = async (file, req, res, options) => { } else { await DataProvider.mergeIntoData(result.data); } - global.timer.setupWithEventList(newEvents); + global.timer.setupWithEventList(newEvents.filter((entry) => entry.type === 'event')); } res.sendStatus(200); } else { diff --git a/server/src/routes/eventsRouter.js b/server/src/routes/eventsRouter.js index 58a2ce53b..276472d52 100644 --- a/server/src/routes/eventsRouter.js +++ b/server/src/routes/eventsRouter.js @@ -1,23 +1,24 @@ import express from 'express'; -export const router = express.Router(); - // import events controller import { + eventsApplyDelay, + eventsDelete, + eventsDeleteAll, eventsGetAll, eventsGetById, + eventsPatch, eventsPost, eventsPut, - eventsPatch, eventsReorder, - eventsApplyDelay, - eventsDeleteAll, - eventsDelete, } from '../controllers/eventsController.js'; import { + eventsPostValidator, eventsPutValidator, paramsMustHaveEventId, } from '../controllers/eventsController.validate.js'; +export const router = express.Router(); + // create route between controller and '/events/' endpoint router.get('/', eventsGetAll); @@ -25,7 +26,7 @@ router.get('/', eventsGetAll); router.get('/:eventId', eventsGetById); // create route between controller and '/events/' endpoint -router.post('/', eventsPost); +router.post('/', eventsPostValidator, eventsPost); // create route between controller and '/events/' endpoint router.put('/', eventsPutValidator, eventsPut);