diff --git a/README.md b/README.md index 1bc3cc8ac..1c5e7667f 100644 --- a/README.md +++ b/README.md @@ -18,17 +18,18 @@ Once installed and running, ontime starts a background server that is the heart The app, is used to add / edit your running order in the event list, and running the timers using the Playback Control function. From here, any device in the same network with a browser is able to render the views as described. This is done by reaching the ontime server at the _default port 4001_ eg: `localhost:4001` or `192.168.1.3:4001` -You can then use the the ontime logo on the top right corner to select the desired view. +You can then use the ontime logo in the top right corner to select the desired view (event in the lower thirds view, where it is hidden). -In case of unnatended machines or automations, it is possible to use different URL to recall individual views +In case of unattended machines or automations, it is possible to use different URL to recall individual views ``` -IP.ADDRESS:4001 > Web server default to stage timer view -IP.ADDRESS:4001/speaker > Speaker / Stage timer view +IP.ADDRESS:4001 > Web server default to presenter timer view +IP.ADDRESS:4001/preseter > Presenter / Stage timer view IP.ADDRESS:4001/sm > Stage Manager / Backstage view IP.ADDRESS:4001/public > Public / Foyer view IP.ADDRESS:4001/pip > Picture in Picture view IP.ADDRESS:4001/lower > Lower Thirds +IP.ADDRESS:4001/studio > Studio Clock ``` More documentation available [here](https://cpvalente.gitbook.io/ontime/) @@ -43,43 +44,54 @@ More documentation available [here](https://cpvalente.gitbook.io/ontime/) - [x] Send live messages to different screen types - [x] Ability to differentiate between backstage and public data - [x] Manage delays workflow -- [x] OSC Control and Feedback +- [x] Open Sound Control (OSC) Control and Feedback - [x] Roll mode: run independently using the system clock +- [x] Import event list from Excel ## Unopinionated -We are not interested in forcing workflows and have made ontime so it is flexible to whichever way you would like to work. +We are not interested in forcing workflows and have made ontime, so it is flexible to whichever way you would like to work. - [x] You do not need an order list to use the timer. Create an empty event and the OSC API works just the same - [x] If you want just the info screens, no need to use the timer! -- [x] Dont have or care for a schedule? +- [x] Don't have or care for a schedule? - [x] a single event with no data is enough to use the OSC API and get going - - [x] use the order list to create a set of quick timers by setting the beggining and start times to 00:00 and 00:10 (**BAM**! 10 minute timer). You can quick recall this with OSC as always + - [x] use the order list to create a set of quick timers by setting the beginning and start times to 00:00 and 00:10 (**BAM**! 10 minute timer). You can quickly recall this with OSC as always + +## Integrations and Workflow +The app is being currently developed to a wide user base, from broadcast to entertainment and conference halls. + +Taking advantage of the integrations in Ontime, we currently use Ontime with: +- `disguise`: trigger ontime from d3's timeline using the **OSC API**, **render views** using d3's webmodule +- `OBS`: **render views** using the Browser Module +- `QLab`: trigger ontime using **OSC API** +- `Companion`: trigger ontime and manipulate timer using **OSC API** + ## Roadmap -### For version 1 -Almost reaching a feature set that we can call v1. Before that: -- [ ] Mac OS version -- [ ] Finish Documentation -### Continuing +### Continued development +There are several features planned in the roadmap. +These will be implemented in a development friendly order unless there is user demand to bump any of them. - [ ] Linux version - [ ] Headless version (run server only anywhere, configure from a browser locally) -- [ ] Companion integration +- [ ] Companion module - [ ] Lower Third Manager - [ ] Note only event - [ ] URL Aliases (define configurable aliases to ease onsite setup) - [ ] Logging view - [ ] Reach Schedule: way to speedup timer to meet a deadline -- [ ] Excel Import - [ ] vMix integration +### For version 1 +Almost reaching a feature set that we can call v1. Before that: +- [ ] Mac OS version + ### Issues The app is still in pre-release and there are a few issues, mainly concerning style. This will be receiving attention as we near v1 release #### Style -- [ ] App appears visually broken: Please ensure that windows settings have no display zoom (it is 125% by default) -- [ ] app needs improvement on handling zoomed interfaces +- [ ] App needs improvement on handling zoomed interfaces: Please ensure that windows settings have no display zoom (it is 125% by default) - [ ] Very long titles might cause interface to shift # Help diff --git a/client/package.json b/client/package.json index d1d214bae..6d9f85bc9 100644 --- a/client/package.json +++ b/client/package.json @@ -24,6 +24,7 @@ "react-scripts": "4.0.3", "socket.io-client": "^4.3.2", "typeface-open-sans": "^1.1.13", + "use-fit-text": "^2.4.0", "web-vitals": "^1.0.1" }, "scripts": { @@ -50,5 +51,7 @@ "last 1 safari version" ] }, - "devDependencies": {} + "devDependencies": { + "sass": "^1.44.0" + } } diff --git a/client/src/App.jsx b/client/src/App.jsx index b3af1e576..a56c126de 100644 --- a/client/src/App.jsx +++ b/client/src/App.jsx @@ -22,6 +22,8 @@ const Lower = lazy(() => ); const Pip = lazy(() => import('features/viewers/production/Pip')); +const StudioClock = lazy(() => import('features/viewers/studio/StudioClock')); + const queryClient = new QueryClient(); // Seemed to cause issues // broadcastQueryClient({ @@ -29,12 +31,13 @@ const queryClient = new QueryClient(); // broadcastChannel: 'ontime', // }); -const SSpeaker = withSocket(PresenterView); -const SSpeakerSimple = withSocket(PresenterSimple); +const SPresenter = withSocket(PresenterView); +const SPresenterSimple = withSocket(PresenterSimple); const SStageManager = withSocket(StageManager); const SPublic = withSocket(Public); const SLowerThird = withSocket(Lower); const SPip = withSocket(Pip); +const SStudio = withSocket(StudioClock); function App() { // Handle keyboard shortcuts @@ -69,18 +72,20 @@ function App() { - + - - - + + + + + {/* Lower cannot have fallback */} {/* Send to default if nothing found */} - + diff --git a/client/src/assets/fonts/digital-7.mono.ttf b/client/src/assets/fonts/digital-7.mono.ttf new file mode 100644 index 000000000..a481b97b4 Binary files /dev/null and b/client/src/assets/fonts/digital-7.mono.ttf differ diff --git a/client/src/common/components/nav/NavLogo.jsx b/client/src/common/components/nav/NavLogo.jsx index 7d0ca6483..4497624ef 100644 --- a/client/src/common/components/nav/NavLogo.jsx +++ b/client/src/common/components/nav/NavLogo.jsx @@ -1,11 +1,13 @@ +import PropTypes from "prop-types"; import { Link, Redirect } from 'react-router-dom'; import { Image } from '@chakra-ui/react'; import { AnimatePresence, motion } from 'framer-motion'; import { useState, useEffect, useCallback } from 'react'; import navlogo from 'assets/images/logos/LOGO-72.png'; -import style from './NavLogo.module.css'; +import style from './NavLogo.module.scss'; -export default function NavLogo() { +export default function NavLogo(props) { + const {isHidden} = props; const [showNav, setShowNav] = useState(false); const handleClick = () => { @@ -30,9 +32,10 @@ export default function NavLogo() { }; }, [handleKeyPress]); + const baseOpacity = (isHidden) ? 0 : 0.5 return ( @@ -51,12 +54,12 @@ export default function NavLogo() { className={showNav ? style.nav : style.navHidden} > } + onKeyDownCapture={() => } > - Speaker + Presenter PIP + } + > + Studio Clock + )} ); } + +NavLogo.propTypes = { + isHidden: PropTypes.bool, +} diff --git a/client/src/common/components/nav/NavLogo.module.css b/client/src/common/components/nav/NavLogo.module.scss similarity index 87% rename from client/src/common/components/nav/NavLogo.module.css rename to client/src/common/components/nav/NavLogo.module.scss index ad75df5b4..3f594bfe9 100644 --- a/client/src/common/components/nav/NavLogo.module.css +++ b/client/src/common/components/nav/NavLogo.module.scss @@ -1,6 +1,9 @@ +$nav-color: #fff; + .navContainer { position: absolute; right: 2vw; + top: 1vw; display: flex; flex-direction: column; align-items: flex-end; @@ -23,4 +26,5 @@ background-color: rgba(0, 0, 0, 0.7); padding: 0.5vh 1vw; border-radius: 4px; + color: $nav-color; } diff --git a/client/src/common/utils/__tests__/eventsManager.test.js b/client/src/common/utils/__tests__/eventsManager.test.js new file mode 100644 index 000000000..68e26c91c --- /dev/null +++ b/client/src/common/utils/__tests__/eventsManager.test.js @@ -0,0 +1,410 @@ +import {formatEventList, getEventsWithDelay, trimEventlist} from "../eventsManager"; + +test('getEventsWithDelay function', () => { + + const testData = [ + { + "title": "Welcome to Ontime", + "timeStart": 28800000, + "timeEnd": 30600000, + "type": "event", + "id": "5946" + }, + { + "duration": 60000, + "type": "delay", + "id": "24240" + }, + { + "title": "Unless recalled by the OSC address", + "timeStart": 34920000, + "timeEnd": 35520000, + "type": "event", + "id": "8ee5" + }, + { + "title": "Use simpler times to create a timer", + "timeStart": 120000, + "timeEnd": 720000, + "type": "event", + "id": "8222" + }, + { + "duration": 900000, + "type": "delay", + "revision": 0, + "id": "a386" + }, + { + "title": "Add delay blocks to affect all events", + "timeStart": 37320000, + "timeEnd": 38520000, + "type": "event", + "id": "6dce" + }, + { + "title": "Add and remove events with [+] and [-]", + "timeStart": 38520000, + "timeEnd": 45120000, + "type": "event", + "id": "2651" + }, + { + "type": "block", + "id": "e6a1" + }, + { + "title": "And control whether they are public", + "timeStart": 46800000, + "timeEnd": 57600000, + "type": "event", + "id": "1358" + } + ]; + + const expected = [ + { + "title": "Welcome to Ontime", + "timeStart": 28800000, + "timeEnd": 30600000, + "type": "event", + "id": "5946" + }, + { + "title": "Unless recalled by the OSC address", + "timeStart": 34920000+60000, + "timeEnd": 35520000+60000, + "type": "event", + "id": "8ee5" + }, + { + "title": "Use simpler times to create a timer", + "timeStart": 120000+60000, + "timeEnd": 720000+60000, + "type": "event", + "id": "8222" + }, + { + "title": "Add delay blocks to affect all events", + "timeStart": 37320000+60000+900000, + "timeEnd": 38520000+60000+900000, + "type": "event", + "id": "6dce" + }, + { + "title": "Add and remove events with [+] and [-]", + "timeStart": 38520000+60000+900000, + "timeEnd": 45120000+60000+900000, + "type": "event", + "id": "2651" + }, + { + "title": "And control whether they are public", + "timeStart": 46800000, + "timeEnd": 57600000, + "type": "event", + "id": "1358" + } + ] + + expect(getEventsWithDelay(testData)).toStrictEqual(expected); +}); + +describe('getEventsWithDelay edge cases', () => { + + test('given an empty array', () => { + const emptyArray = { + test: [], + expect: [], + } + + expect(getEventsWithDelay(emptyArray.test)).toStrictEqual(emptyArray.expect); + }); + + test('given an undefined object', () => { + const withUndefined = { + test: undefined, + expect: [], + } + + expect(getEventsWithDelay(withUndefined.test)).toStrictEqual(withUndefined.expect); + }); + + test('given a corrupted event object', () => { + const testData = [ + { + "title": "Welcome to Ontime", + "timeEnd": 30600000, + "type": "event", + "id": "5946" + }, + { + "duration": 60000, + "type": "delay", + "id": "24240" + }, + { + "title": "Unless recalled by the OSC address", + "timeStart": 34920000, + "timeEnd": 35520000, + "type": "event", + "id": "8ee5" + } + ]; + const expected = [ + { + "title": "Welcome to Ontime", + "timeEnd": 30600000, + "type": "event", + "id": "5946" + }, + { + "title": "Unless recalled by the OSC address", + "timeStart": 34920000+60000, + "timeEnd": 35520000+60000, + "type": "event", + "id": "8ee5" + } + ]; + + expect(getEventsWithDelay(testData)).toStrictEqual(expected); + }); + + test('given a corrupted delay object', () => { + const testData = [ + { + "title": "Welcome to Ontime", + "timeStart": 28800000, + "timeEnd": 30600000, + "type": "event", + "id": "5946" + }, + { + "type": "delay", + "id": "24240" + }, + { + "title": "Unless recalled by the OSC address", + "timeStart": 34920000, + "timeEnd": 35520000, + "type": "event", + "id": "8ee5" + } + ]; + const expected = [ + { + "title": "Welcome to Ontime", + "timeStart": 28800000, + "timeEnd": 30600000, + "type": "event", + "id": "5946" + }, + { + "title": "Unless recalled by the OSC address", + "timeStart": 34920000, + "timeEnd": 35520000, + "type": "event", + "id": "8ee5" + } + ]; + + expect(getEventsWithDelay(testData)).toStrictEqual(expected); + }); +}); + +describe('test trimEventlist function', () => { + + const limit = 8; + const testData = [ + {id: '1'}, + {id: '2'}, + {id: '3'}, + {id: '4'}, + {id: '5'}, + {id: '6'}, + {id: '7'}, + {id: '8'}, + {id: '9'}, + {id: '10'}, + {id: '11'}, + {id: '12'}, + ]; + + + test('when we use the first item', () => { + const selectedId = '1'; + const expected = [ + {id: '1'}, + {id: '2'}, + {id: '3'}, + {id: '4'}, + {id: '5'}, + {id: '6'}, + {id: '7'}, + {id: '8'}, + ]; + + const l = trimEventlist(testData, selectedId, limit); + expect(l.length).toBe(limit); + expect(l).toStrictEqual(expected); + }); + + test('when we use the third item', () => { + const selectedId = '3'; + const expected = [ + {id: '1'}, + {id: '2'}, + {id: '3'}, + {id: '4'}, + {id: '5'}, + {id: '6'}, + {id: '7'}, + {id: '8'} + ]; + + const l = trimEventlist(testData, selectedId, limit); + expect(l.length).toBe(limit); + expect(l).toStrictEqual(expected); + }); + + test('when we use the fourth item', () => { + const selectedId = '4'; + const expected = [ + {id: '2'}, + {id: '3'}, + {id: '4'}, + {id: '5'}, + {id: '6'}, + {id: '7'}, + {id: '8'}, + {id: '9'} + ]; + + const l = trimEventlist(testData, selectedId, limit); + expect(l.length).toBe(limit); + expect(l).toStrictEqual(expected); + }); + + test('if selected is not found', () => { + const selectedId = '15'; + const expected = [ + {id: '1'}, + {id: '2'}, + {id: '3'}, + {id: '4'}, + {id: '5'}, + {id: '6'}, + {id: '7'}, + {id: '8'}, + ]; + + const l = trimEventlist(testData, selectedId, limit); + expect(l.length).toBe(limit); + expect(l).toStrictEqual(expected); + }); +}); + +describe('test formatEvents function', () => { + const testEvent = [ + { + "title": "Welcome to Ontime", + "subtitle": "Subtitles are useful", + "presenter": "cpvalente", + "note": "Maybe a running note for the operator?", + "timeStart": 28800000, + "timeEnd": 30600000, + "isPublic": false, + "type": "event", + "revision": 0, + "id": "5946" + }, + { + "title": "Unless recalled by the OSC address", + "subtitle": "", + "presenter": "", + "note": "In green, below", + "timeStart": 34800000, + "timeEnd": 35400000, + "isPublic": false, + "type": "event", + "revision": 0, + "id": "8ee5" + } + ]; + + test ('it parses correctly', () => { + const selectedId = 'otherEvent'; + const nextId = 'notHere'; + const expected = [ + { + id: '5946', + time: '08:00 - 08:30', + title: 'Welcome to Ontime', + isNow: false, + isNext: false, + }, + { + id: '8ee5', + time: '09:40 - 09:50', + title: 'Unless recalled by the OSC address', + isNow: false, + isNext: false, + }, + + ] + + const parsed = formatEventList(testEvent, selectedId, nextId, true); + expect(parsed).toStrictEqual(expected); + }); + + test ('it handles selected correctly', () => { + const selectedId = '5946'; + const nextId = '8ee5'; + const expected = [ + { + id: '5946', + time: '08:00 - 08:30', + title: 'Welcome to Ontime', + isNow: true, + isNext: false, + }, + { + id: '8ee5', + time: '09:40 - 09:50', + title: 'Unless recalled by the OSC address', + isNow: false, + isNext: true, + }, + + ] + + const parsed = formatEventList(testEvent, selectedId, nextId,true); + expect(parsed).toStrictEqual(expected); + }); + + test ('it handles next correctly', () => { + const selectedId = '8ee5'; + const nextId = 'notHere'; + + const expected = [ + { + id: '5946', + time: '08:00 - 08:30', + title: 'Welcome to Ontime', + isNow: false, + isNext: false, + }, + { + id: '8ee5', + time: '09:40 - 09:50', + title: 'Unless recalled by the OSC address', + isNow: true, + isNext: false, + }, + + ] + + const parsed = formatEventList(testEvent, selectedId, nextId, true); + expect(parsed).toStrictEqual(expected); + }); +}) \ No newline at end of file diff --git a/client/src/common/utils/dateConfig.js b/client/src/common/utils/dateConfig.js index 0c95fff74..3bb3af543 100644 --- a/client/src/common/utils/dateConfig.js +++ b/client/src/common/utils/dateConfig.js @@ -40,11 +40,11 @@ export const stringFromMillis = ( * another go at simpler string formatting (counters) * @description Converts seconds to string representing time * @param {number} seconds - time in seconds - * @param {boolean} hideZero - wether to show hours in case its 00 + * @param {boolean} [hideZero] - whether to show hours in case its 00 * @returns {string} String representing absolute time 00:12:02 */ -export function formatDisplay(seconds, hideZero) { +export function formatDisplay(seconds, hideZero=false) { // add an extra 0 if necessary const format = (val) => `0${Math.floor(val)}`.slice(-2); @@ -59,7 +59,6 @@ export function formatDisplay(seconds, hideZero) { /** * @description Converts milliseconds to seconds * @param {number} millis - time in seconds - * @param {boolean} hideZero - wether to show hours in case its 00 * @returns {number} Amount in seconds */ @@ -71,7 +70,6 @@ export const millisToSeconds = (millis) => { /** * @description Converts milliseconds to seconds * @param {number} millis - time in seconds - * @param {boolean} hideZero - wether to show hours in case its 00 * @returns {number} Amount in seconds */ diff --git a/client/src/common/utils/eventsManager.js b/client/src/common/utils/eventsManager.js new file mode 100644 index 000000000..b00db9df4 --- /dev/null +++ b/client/src/common/utils/eventsManager.js @@ -0,0 +1,84 @@ +/** + * @description From a list of events, returns only events of type event with calculated delays + * @param {Object[]} events - given events + * @returns {Object[]} Filtered events with calculated delays + */ +import {stringFromMillis} from "./dateConfig"; + +export const getEventsWithDelay = (events) => { + + if (events == null) return []; + + const unfilteredEvents = [...events]; + + // Add running delay + let delay = 0; + for (const e of unfilteredEvents) { + if (e.type === 'block') delay = 0; + else if (e.type === 'delay') delay = delay + e.duration; + else if (e.type === 'event' && delay > 0) { + e.timeStart += delay; + e.timeEnd += delay; + } + } + + // filter just events + return unfilteredEvents.filter((e) => e.type === 'event'); +}; + +/** + * @description Returns trimmed event list array + * @param {Object[]} events - given events + * @param {string} selectedId - id of currently selected event + * @param {number} limit - max number of events to return + * @returns {Object[]} Event list with maximum objects + */ +export const trimEventlist = (events, selectedId, limit) => { + if (events == null) return []; + + const BEFORE = 2; + const trimmedEvents = [...events]; + + // limit events length if necessary + if (limit != null) { + while (trimmedEvents.length > limit) { + const idx = trimmedEvents.findIndex((e) => e.id === selectedId); + if (idx <= BEFORE) { trimmedEvents.pop(); } + else { trimmedEvents.shift(); } + } + } + return trimmedEvents; +}; + +/** + * @description Returns list of events formatted to be displayed + * @param {Object[]} events - given events + * @param {string} selectedId - id of currently selected event + * @param {string} nextId - id of next event + * @param {boolean} [showEnd] - whether to show the end time + * @returns {Object[]} Formatted list of events [{time: -, title: -, isNow, isNext}] + */ +export const formatEventList = (events, selectedId, nextId, showEnd = false) => { + if (events == null) return []; + + const givenEvents = [...events]; + + // format list + let formattedEvents = []; + for (const g of givenEvents) { + const start = stringFromMillis(g.timeStart, false); + const end = stringFromMillis(g.timeEnd, false); + + formattedEvents.push({ + id: g.id, + time: showEnd ? `${start} - ${end}` : start, + title: g.title, + isNow: g.id === selectedId, + isNext: g.id === nextId, + }); + } + + return formattedEvents; +}; + + diff --git a/client/src/features/control/MessageControl.jsx b/client/src/features/control/MessageControl.jsx index e881cb5f1..264c4c5f8 100644 --- a/client/src/features/control/MessageControl.jsx +++ b/client/src/features/control/MessageControl.jsx @@ -1,15 +1,16 @@ -import { Editable, EditableInput, EditablePreview } from '@chakra-ui/editable'; -import { useEffect, useState } from 'react'; -import { useSocket } from 'app/context/socketContext'; +import {Editable, EditableInput, EditablePreview} from '@chakra-ui/editable'; +import {Switch} from "@chakra-ui/react"; +import {useEffect, useState} from 'react'; +import {useSocket} from 'app/context/socketContext'; import VisibleIconBtn from 'common/components/buttons/VisibleIconBtn'; -import style from './MessageControl.module.css'; +import style from './MessageControl.module.scss'; const inputProps = { size: 'sm', }; const InputRow = (props) => { - const { label, placeholder, text, visible } = props; + const {label, placeholder, text, visible} = props; return ( <> @@ -22,8 +23,8 @@ const InputRow = (props) => { className={style.inline} color={text === '' ? '#666' : 'inherit'} > - - + + { if (socket == null) return; // Handle presenter messages socket.on('messages-presenter', (data) => { - setPres({ ...data }); + setPres({...data}); }); // Handle public messages socket.on('messages-public', (data) => { - setPubl({ ...data }); + setPubl({...data}); }); // Handle lower third messages socket.on('messages-lower', (data) => { - setLower({ ...data }); + setLower({...data}); + }); + + // Handle lower third messages + socket.on('onAir', (data) => { + setonAir(data); }); // Ask for up to date data socket.emit('get-messages'); + // Ask for onAir state + socket.emit('get-onAir'); // Clear listeners return () => { socket.off('messages-public'); socket.off('messages-presenter'); socket.off('messages-lower'); + socket.off('onAir'); }; }, [socket]); @@ -99,38 +109,54 @@ export default function MessageControl() { case 'toggle-lower-visible': socket.emit('set-lower-visible', !lower.visible); break; - + case 'toggle-onAir': + socket.emit('set-onAir', !onAir); + break; default: break; } }; return ( -
- messageControl('pres-text', event)} - actionHandler={() => messageControl('toggle-pres-visible')} - /> - messageControl('publ-text', event)} - actionHandler={() => messageControl('toggle-publ-visible')} - /> - messageControl('lower-text', event)} - actionHandler={() => messageControl('toggle-lower-visible')} - /> -
+ <> +
+ messageControl('pres-text', event)} + actionHandler={() => messageControl('toggle-pres-visible')} + /> + messageControl('publ-text', event)} + actionHandler={() => messageControl('toggle-publ-visible')} + /> + messageControl('lower-text', event)} + actionHandler={() => messageControl('toggle-lower-visible')} + /> +
+
+ messageControl('toggle-onAir')}> + On Air? + + + {`/ontime/offAir << OSC >> /ontime/onAir`} + +
+ ); } diff --git a/client/src/features/control/MessageControl.module.css b/client/src/features/control/MessageControl.module.css deleted file mode 100644 index 6c67eeb37..000000000 --- a/client/src/features/control/MessageControl.module.css +++ /dev/null @@ -1,32 +0,0 @@ -.messageContainer { - background-color: rgba(0, 0, 0, 0.05); - border: 1px solid rgba(0, 0, 0, 0.05); - border-radius: 4px; - display: flex; - flex-direction: column; - gap: 0.5em; - padding: 0.5em; -} - -.inputItems { - display: grid; - grid-template-columns: 1fr auto; - gap: 1em; -} - -.label { - padding: 0; - margin: 0; - font-size: 0.9em; - color: #ccc; -} - -.inline { - border-radius: 4px; - background-color: rgba(255, 255, 255, 0.05); - border: 1px solid rgba(255, 255, 255, 0.05); -} - -.padleft { - padding-left: 0.5em; -} diff --git a/client/src/features/control/MessageControl.module.scss b/client/src/features/control/MessageControl.module.scss new file mode 100644 index 000000000..52889d3fb --- /dev/null +++ b/client/src/features/control/MessageControl.module.scss @@ -0,0 +1,54 @@ +.messageContainer, +.onAirToggle { + background-color: rgba(0, 0, 0, 0.05); + border: 1px solid rgba(0, 0, 0, 0.05); + border-radius: 4px; + display: flex; + gap: 0.5em; + padding: 0.5em; + +} + +.messageContainer { + flex-direction: column; + + .inputItems { + display: grid; + grid-template-columns: 1fr auto; + gap: 1em; + } + .label { + padding: 0; + margin: 0; + font-size: 0.9em; + color: #ccc; + } + .inline { + border-radius: 4px; + background-color: rgba(255, 255, 255, 0.05); + border: 1px solid rgba(255, 255, 255, 0.05); + } + .padleft { + padding-left: 0.5em; + } +} + +.onAirToggle { + margin-top: 1em; + display: flex; + gap: 1em; + align-items: center; + line-height: 3em; + + .onAirLabel { + font-size: 1.2em; + } + .oscLabel { + color: #4bffabcc; + font-size: 0.8em; + float: right; + padding-right: 1em; + -webkit-user-select: text; + user-select: text; + } +} diff --git a/client/src/features/viewers/ViewWrapper.jsx b/client/src/features/viewers/ViewWrapper.jsx index 6d90c0a7e..7f58dea19 100644 --- a/client/src/features/viewers/ViewWrapper.jsx +++ b/client/src/features/viewers/ViewWrapper.jsx @@ -10,13 +10,9 @@ const withSocket = (Component) => { const WrappedComponent = (props) => { const { data: eventsData, - status: eventsDataStatus, - isError: eventsDataIsError, } = useFetch(EVENTS_TABLE, fetchAllEvents); const { data: genData, - status: genDataStatus, - isError: genDataIsError, } = useFetch(EVENT_TABLE, fetchEvent); const [publicEvents, setPublicEvents] = useState([]); @@ -58,6 +54,7 @@ const withSocket = (Component) => { presenterNext: '', }); const [selectedId, setSelectedId] = useState(null); + const [nextId, setNextId] = useState(null); const [publicSelectedId, setPublicSelectedId] = useState(null); const [general, setGeneral] = useState({ title: '', @@ -67,6 +64,7 @@ const withSocket = (Component) => { endMessage: '', }); const [playback, setPlayback] = useState(null); + const [onAir, setOnAir] = useState(false); // Ask for update on load useEffect(() => { @@ -96,6 +94,9 @@ const withSocket = (Component) => { socket.on('playstate', (data) => { setPlayback(data); }); + socket.on('onAir', (data) => { + setOnAir(data); + }); // Handle titles socket.on('titles', (data) => { @@ -112,6 +113,9 @@ const withSocket = (Component) => { socket.on('publicselected-id', (data) => { setPublicSelectedId(data); }); + socket.on('next-id', (data) => { + setNextId(data); + }); // Ask for up to date data socket.emit('get-messages'); @@ -124,6 +128,7 @@ const withSocket = (Component) => { // ask for playstate socket.emit('get-playstate'); + socket.emit('get-onAir') // Ask for up titles socket.emit('get-titles'); @@ -131,6 +136,7 @@ const withSocket = (Component) => { // Ask for up selected socket.emit('get-selected-id'); + socket.emit('get-next-id'); // Clear listeners return () => { @@ -139,9 +145,11 @@ const withSocket = (Component) => { socket.off('messages-lower'); socket.off('timer'); socket.off('playstate'); + socket.off('onAir'); socket.off('titles'); socket.off('publictitles'); socket.off('selected-id'); + socket.emit('next-id'); }; }, [socket]); @@ -224,6 +232,7 @@ const withSocket = (Component) => { ...timer, finished: playback === 'start' && timer.running <= 0 && timer.startedAt, clock: stringFromMillis(timer.clock), + clockNoSeconds: stringFromMillis(timer.clock, false), playstate: playback, }; @@ -240,7 +249,9 @@ const withSocket = (Component) => { backstageEvents={backstageEvents} selectedId={selectedId} publicSelectedId={publicSelectedId} + nextId={nextId} general={general} + onAir={onAir} /> ); }; diff --git a/client/src/features/viewers/backstage/StageManager.jsx b/client/src/features/viewers/backstage/StageManager.jsx index 3e843b2a1..39d507a83 100644 --- a/client/src/features/viewers/backstage/StageManager.jsx +++ b/client/src/features/viewers/backstage/StageManager.jsx @@ -6,6 +6,7 @@ import NavLogo from 'common/components/nav/NavLogo'; import { useEffect, useState } from 'react'; import { AnimatePresence, motion } from 'framer-motion'; import TitleSide from 'common/components/views/TitleSide'; +import {getEventsWithDelay} from "../../../common/utils/eventsManager"; export default function StageManager(props) { const { publ, title, time, backstageEvents, selectedId, general } = props; @@ -20,23 +21,9 @@ export default function StageManager(props) { useEffect(() => { if (backstageEvents == null) return; - let events = [...backstageEvents]; + setFilteredEvents(getEventsWithDelay(backstageEvents)); - // Add running delay - let delay = 0; - for (const e of events) { - if (e.type === 'block') delay = 0; - else if (e.type === 'delay') delay = delay + e.duration; - else if (e.type === 'event' && delay > 0) { - e.timeStart += delay; - e.timeEnd += delay; - } - } - - // filter just events - let filtered = events.filter((e) => e.type === 'event'); - - setFilteredEvents(filtered); + console.log('hhh', getEventsWithDelay(backstageEvents)) }, [backstageEvents]); // Format messages diff --git a/client/src/features/viewers/presenter/PresenterView.jsx b/client/src/features/viewers/presenter/PresenterView.jsx index 59957f1f1..7e185b1e1 100644 --- a/client/src/features/viewers/presenter/PresenterView.jsx +++ b/client/src/features/viewers/presenter/PresenterView.jsx @@ -11,7 +11,7 @@ export default function PresenterView(props) { // Set window title useEffect(() => { - document.title = 'ontime - Speaker Screen'; + document.title = 'ontime - Presenter Screen'; }, []); const showOverlay = pres.text !== '' && pres.visible; diff --git a/client/src/features/viewers/production/lower/LowerClean.jsx b/client/src/features/viewers/production/lower/LowerClean.jsx index fb9fc8c40..b26dec608 100644 --- a/client/src/features/viewers/production/lower/LowerClean.jsx +++ b/client/src/features/viewers/production/lower/LowerClean.jsx @@ -1,6 +1,7 @@ import { AnimatePresence, motion } from 'framer-motion'; import { useEffect, useState } from 'react'; import style from './LowerClean.module.css'; +import NavLogo from "../../../../common/components/nav/NavLogo"; export default function LowerClean(props) { const { lower, title, options } = props; @@ -86,6 +87,9 @@ export default function LowerClean(props) { fontSize: `${sizeMultiplier}vh`, }} > + + + {showLower && ( + + + {showLower && ( { + document.title = 'ontime - Studio Clock'; + }, []); + + // Prepare event list + // Todo: useMemo() + useEffect(() => { + if (backstageEvents == null) return; + + const events = backstageEvents.filter((e) => e.type === 'event'); + + let e = trimEventlist(events, selectedId, MAX_TITLES); + e = formatEventList(e, selectedId, nextId); + setSchedule(e); + + }, [backstageEvents, selectedId, nextId]); + + return ( +
+ +
+
{time.clockNoSeconds}
+
+ {title.titleNext} +
+
0 ? style.nextCountdown: style.nextCountdown__overtime}> + {selectedId != null && formatDisplay(time.running)} +
+
+ {hoursIndicators.map(i => ( +
+ ) + )} + {minutesIndicators.map(i => ( +
+ ) + )} +
+
+
+
ON AIR
+
+
    + {schedule.map((s) => ( +
  • +
    {`${s.time} ${s.title}`} +
  • + )) + } +
+
+
+
+ ); +} diff --git a/client/src/features/viewers/studio/StudioClock.module.scss b/client/src/features/viewers/studio/StudioClock.module.scss new file mode 100644 index 000000000..ebf1f7e53 --- /dev/null +++ b/client/src/features/viewers/studio/StudioClock.module.scss @@ -0,0 +1,190 @@ +@font-face { + font-family: "digital-clock"; + src: local('digital-7'), url('./../../../assets/fonts/digital-7.mono.ttf') format('truetype') ; +} + +.container { + margin: 0; + box-sizing: border-box; /* reset */ + overflow: hidden; + width: 100%; /* restrict the page width to viewport */ + height: 100vh; + padding: 1vw; + + background: #111; + + display: grid; + grid-template-columns: 1000px 1fr; + gap: 50px; + grid-template-areas: "clck schd"; + + /* =============== CLOCK STUFF ==================*/ + + $clock-size: 900px; + $size-hours: 20px; + $half-hours: 10px; + $size-min: 18px; + $half-min: 9px; + $red-active: #c53030; + $red-idle: darken($red-active, 30%); + $cyan-active: #0ff; + $cyan-idle: #0aa; + + .clockContainer { + display: grid; + place-content: center; + grid-area: clck; + width: $clock-size; + height: $clock-size; + text-align: center; + position: relative; + margin: auto; + font-family: digital-clock, monospace; + text-transform: uppercase; + + + .time { + margin-top: 175px; + color: $red-active; + font-size: 275px; + line-height: 0.8em; + text-shadow: rgb(180,0,0) 0 0 20px; + } + .nextTitle:after, + .nextCountdown:after, + .nextCountdown__overtime:after { + content: '\200b'; + } + .nextTitle { + color:$cyan-idle; + line-height: 100px; + } + .nextCountdown, + .nextCountdown__overtime { + font-size: 100px; + line-height: 1em; + } + .nextCountdown { + color: $cyan-active; + text-shadow: rgb(0,100,100) 0 0 20px; + } + .nextCountdown::before { + content: '-'; + } + .nextCountdown__overtime { + color: darken($red-active, 10%); + } + .indicators { + position: absolute; + width: 100%; + height: 100%; + + .min, + .min__active, + .hours, + .hours__active { + border-radius: 50%; + position: absolute; + background: $red-idle; + } + .min, + .min__active { + min-height: $size-min; + width: $size-min; + top: calc(50% - #{$half_min}); + left: calc(50% - #{$half_min}); + } + .hours, + .hours__active{ + min-height:$size-hours; + width: $size-hours; + top: calc(50% - #{$half_hours}); + left: calc(50% - #{$half_hours}); + } + .min__active, + .hours__active { + background: $red-active; + box-shadow: 0 0 10px 2px rgba(255,0,0,0.25); + } + } + } + + /* ============= SCHEDULE STUFF =================*/ + + .scheduleContainer { + grid-area: schd; + margin: 50px 0; + padding-right: 50px; + font-family: digital-clock, monospace; + text-transform: uppercase; + + + .onAir, + .onAir__idle{ + padding-bottom: 50px; + font-size: 190px; + line-height: 0.9em; + letter-spacing: 0.05em; + } + .onAir { + color: $red-active; + text-shadow: rgb(150,0,0) 0 0 20px; + } + .onAir__idle { + color: $red-idle; + } + .schedule { + + ul { + color: $cyan-idle; + line-height: 1em; + list-style: none; + font-size: 40px; + } + li { + padding-bottom: 0.5em; + display: flex; + align-items: center; + gap: 20px; + } + .now { + color: $cyan-active; + text-shadow: rgb(0,100,100) 0 0 20px; + } + .next { + color: $red-active; + text-shadow: rgb(100,0,0) 0 0 20px; + } + .decorator, + .decorator__active, + .decorator__next { + aspect-ratio: 1; + border-radius: 50%; + background: $red-idle; + min-height: $size-hours; + max-height: $size-hours; + width: $size-hours; + } + .decorator__active { + background: $cyan-active; + box-shadow: 0 0 10px 2px rgba(0,255,255,0.25); + } + .decorator__next{ + background: $red-active; + box-shadow: 0 0 10px 2px rgba(255,0,0,0.25); + } + } + } +} + +@media only screen and (max-width: 1600px) { + .container { + display: grid; + grid-template-areas: "clck"; + grid-template-columns: 100%; + place-content: center; + } + .scheduleContainer{ + display: none; + } +} diff --git a/client/yarn.lock b/client/yarn.lock index a15a4edc8..967a81dfc 100644 --- a/client/yarn.lock +++ b/client/yarn.lock @@ -3073,6 +3073,14 @@ anymatch@^3.0.3, anymatch@~3.1.1: normalize-path "^3.0.0" picomatch "^2.0.4" +anymatch@~3.1.2: + version "3.1.2" + resolved "https://registry.yarnpkg.com/anymatch/-/anymatch-3.1.2.tgz#c0557c096af32f106198f4f4e2a383537e378716" + integrity sha512-P43ePfOAIupkguHUycrc4qJ9kz8ZiuOUijaETwX7THt0Y/GNK7v0aa8rY816xWjZ7rJdA5XdMcpVFTKMq+RvWg== + dependencies: + normalize-path "^3.0.0" + picomatch "^2.0.4" + aproba@^1.1.1: version "1.2.0" resolved "https://registry.yarnpkg.com/aproba/-/aproba-1.2.0.tgz#6802e6264efd18c790a1b0d517f0f2627bf2c94a" @@ -3990,6 +3998,21 @@ check-types@^11.1.1: resolved "https://registry.yarnpkg.com/check-types/-/check-types-11.1.2.tgz#86a7c12bf5539f6324eb0e70ca8896c0e38f3e2f" integrity sha512-tzWzvgePgLORb9/3a0YenggReLKAIb2owL03H2Xdoe5pKcUyWRSEQ8xfCar8t2SIAuEDwtmx2da1YB52YuHQMQ== +"chokidar@>=3.0.0 <4.0.0": + version "3.5.2" + resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-3.5.2.tgz#dba3976fcadb016f66fd365021d91600d01c1e75" + integrity sha512-ekGhOnNVPgT77r4K/U3GDhu+FQ2S8TnK/s2KbIGXi0SZWuwkZ2QNyfWdZW+TVfn84DpEP7rLeCt2UI6bJ8GwbQ== + dependencies: + anymatch "~3.1.2" + braces "~3.0.2" + glob-parent "~5.1.2" + is-binary-path "~2.1.0" + is-glob "~4.0.1" + normalize-path "~3.0.0" + readdirp "~3.6.0" + optionalDependencies: + fsevents "~2.3.2" + chokidar@^2.1.8: version "2.1.8" resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-2.1.8.tgz#804b3a7b6a99358c3c5c61e71d8728f041cff917" @@ -6054,7 +6077,7 @@ fsevents@^1.2.7: bindings "^1.5.0" nan "^2.12.1" -fsevents@^2.1.2, fsevents@^2.1.3, fsevents@~2.3.1: +fsevents@^2.1.2, fsevents@^2.1.3, fsevents@~2.3.1, fsevents@~2.3.2: version "2.3.2" resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.3.2.tgz#8a526f78b8fdf4623b709e0b975c52c24c02fd1a" integrity sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA== @@ -6137,7 +6160,7 @@ glob-parent@^3.1.0: is-glob "^3.1.0" path-dirname "^1.0.0" -glob-parent@^5.0.0, glob-parent@^5.1.0, glob-parent@~5.1.0: +glob-parent@^5.0.0, glob-parent@^5.1.0, glob-parent@~5.1.0, glob-parent@~5.1.2: version "5.1.2" resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-5.1.2.tgz#869832c58034fe68a4093c17dc15e8340d8401c4" integrity sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow== @@ -6608,6 +6631,11 @@ immer@8.0.1: resolved "https://registry.yarnpkg.com/immer/-/immer-8.0.1.tgz#9c73db683e2b3975c424fb0572af5889877ae656" integrity sha512-aqXhGP7//Gui2+UrEtvxZxSquQVXTpZ7KDxfCcKAF3Vysvw0CViVaW9RZ1j1xlIYqaaaipBoqdqeibkc18PNvA== +immutable@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/immutable/-/immutable-4.0.0.tgz#b86f78de6adef3608395efb269a91462797e2c23" + integrity sha512-zIE9hX70qew5qTUjSS7wi1iwj/l7+m54KWU247nhM3v806UdGj1yDndXj+IOYxxtW9zyLI+xqFNZjTuDaLUqFw== + import-cwd@^2.0.0: version "2.1.0" resolved "https://registry.yarnpkg.com/import-cwd/-/import-cwd-2.1.0.tgz#aa6cf36e722761285cb371ec6519f53e2435b0a9" @@ -10366,6 +10394,13 @@ readdirp@~3.5.0: dependencies: picomatch "^2.2.1" +readdirp@~3.6.0: + version "3.6.0" + resolved "https://registry.yarnpkg.com/readdirp/-/readdirp-3.6.0.tgz#74a370bd857116e245b29cc97340cd431a02a6c7" + integrity sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA== + dependencies: + picomatch "^2.2.1" + recursive-readdir@2.2.2: version "2.2.2" resolved "https://registry.yarnpkg.com/recursive-readdir/-/recursive-readdir-2.2.2.tgz#9946fb3274e1628de6e36b2f6714953b4845094f" @@ -10565,6 +10600,11 @@ requires-port@^1.0.0: resolved "https://registry.yarnpkg.com/requires-port/-/requires-port-1.0.0.tgz#925d2601d39ac485e091cf0da5c6e694dc3dcaff" integrity sha1-kl0mAdOaxIXgkc8NpcbmlNw9yv8= +resize-observer-polyfill@^1.5.1: + version "1.5.1" + resolved "https://registry.yarnpkg.com/resize-observer-polyfill/-/resize-observer-polyfill-1.5.1.tgz#0e9020dd3d21024458d4ebd27e23e40269810464" + integrity sha512-LwZrotdHOo12nQuZlHEmtuXdqGoOD0OhaxopaNFxWzInpEgaLWoVuAMbTzixuosCx2nEG58ngzW3vxdWoxIgdg== + resolve-cwd@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/resolve-cwd/-/resolve-cwd-2.0.0.tgz#00a9f7387556e27038eae232caa372a6a59b665a" @@ -10811,6 +10851,14 @@ sass-loader@^10.0.5: schema-utils "^3.0.0" semver "^7.3.2" +sass@^1.44.0: + version "1.44.0" + resolved "https://registry.yarnpkg.com/sass/-/sass-1.44.0.tgz#619aa0a2275c097f9af5e6b8fe8a95e3056430fb" + integrity sha512-0hLREbHFXGQqls/K8X+koeP+ogFRPF4ZqetVB19b7Cst9Er8cOR0rc6RU7MaI4W1JmUShd1BPgPoeqmmgMMYFw== + dependencies: + chokidar ">=3.0.0 <4.0.0" + immutable "^4.0.0" + sax@~1.2.4: version "1.2.4" resolved "https://registry.yarnpkg.com/sax/-/sax-1.2.4.tgz#2816234e2378bddc4e5354fab5caa895df7100d9" @@ -12131,6 +12179,13 @@ use-callback-ref@^1.2.3, use-callback-ref@^1.2.5: resolved "https://registry.yarnpkg.com/use-callback-ref/-/use-callback-ref-1.2.5.tgz#6115ed242cfbaed5915499c0a9842ca2912f38a5" integrity sha512-gN3vgMISAgacF7sqsLPByqoePooY3n2emTH59Ur5d/M8eg4WTWu1xp8i8DHjohftIyEx0S08RiYxbffr4j8Peg== +use-fit-text@^2.4.0: + version "2.4.0" + resolved "https://registry.yarnpkg.com/use-fit-text/-/use-fit-text-2.4.0.tgz#d3d1cd72f6d29cfb2233ec0e0b38c6f25d319f67" + integrity sha512-Iy4LMrXcdxWlyZ5phntMpJMgyXGB1p3tV73y2r0QrZ6f/thPh+/QU3ie6RCXmjF8tHMs20FKMPskXeDYIla/Ww== + dependencies: + resize-observer-polyfill "^1.5.1" + use-memo-one@^1.1.1: version "1.1.2" resolved "https://registry.yarnpkg.com/use-memo-one/-/use-memo-one-1.1.2.tgz#0c8203a329f76e040047a35a1197defe342fab20" diff --git a/server/package.json b/server/package.json index dca7b404e..3a1099646 100644 --- a/server/package.json +++ b/server/package.json @@ -1,6 +1,6 @@ { "name": "ontime", - "version": "0.4.1", + "version": "0.4.2", "author": "Carlos Valente", "description": "Time keeping for live events", "repository": "https://github.com/cpvalente/ontime", diff --git a/server/src/classes/EventTimer.js b/server/src/classes/EventTimer.js index 2224b2d72..c7fc10943 100644 --- a/server/src/classes/EventTimer.js +++ b/server/src/classes/EventTimer.js @@ -63,6 +63,7 @@ export class EventTimer extends Timer { nextPublicEventId = null; numEvents = null; _eventlist = null; + onAir = false; constructor(httpServer, oscClient, config) { // call super constructor @@ -199,7 +200,7 @@ export class EventTimer extends Timer { // send current timer broadcastTimer() { // through websockets - this.io.emit('timer', this.getObject()); + this.io.emit('timer', this.getTimes()); // through OSC, only if running if (this.state === 'start' || this.state === 'roll') { @@ -211,7 +212,7 @@ export class EventTimer extends Timer { // broadcast state broadcastState(update = true) { - this.io.emit('timer', this.getObject(update)); + this.io.emit('timer', this.getTimes(update)); this.io.emit('playstate', this.state); this.io.emit('selected', { id: this.selectedEventId, @@ -224,6 +225,7 @@ export class EventTimer extends Timer { this.io.emit('publicnext-id', this.nextPublicEventId); this.io.emit('titles', this.titles); this.io.emit('publictitles', this.titlesPublic); + this.io.emit('onAir', this.onAir); } // broadcast message @@ -359,7 +361,7 @@ export class EventTimer extends Timer { ); // send state - socket.emit('timer', this.getObject()); + socket.emit('timer', this.getTimes()); socket.emit('playstate', this.state); socket.emit('selected-id', this.selectedEventId); socket.emit('next-id', this.nextEventId); @@ -385,7 +387,7 @@ export class EventTimer extends Timer { /*******************************************/ // general playback state socket.on('get-state', () => { - socket.emit('timer', this.getObject()); + socket.emit('timer', this.getTimes()); socket.emit('playstate', this.state); socket.emit('selected-id', this.selectedEventId); socket.emit('next-id', this.nextEventId); @@ -400,7 +402,7 @@ export class EventTimer extends Timer { }); socket.on('get-timer', () => { - socket.emit('timer', this.getObject()); + socket.emit('timer', this.getTimes()); }); socket.on('increment-timer', (data) => { @@ -419,6 +421,15 @@ export class EventTimer extends Timer { socket.emit('playstate', this.state); }); + socket.on('set-onAir', (data) => { + this.onAir = data; + this.broadcastThis('onAir', this.onAir); + }); + + socket.on('get-onAir', () => { + socket.emit('onAir', this.onAir); + }); + /*******************************************/ // selection data socket.on('get-selected', () => { @@ -949,6 +960,14 @@ export class EventTimer extends Timer { `; } + /** + * @description Set onAir property of timer + * @param {boolean} onAir - whether flag is active + */ + set onAir(onAir) { + this.onAir = onAir; + } + start() { // if there is nothing selected, no nothing if (this.selectedEventId == null) return; diff --git a/server/src/classes/Timer.js b/server/src/classes/Timer.js index 5ed019a14..8cf6eea38 100644 --- a/server/src/classes/Timer.js +++ b/server/src/classes/Timer.js @@ -139,8 +139,8 @@ export class Timer { return this.duration - this.current; } - // getObject - getObject(update = true) { + // get time object + getTimes(update = true) { // update timer if (update) this.update(); diff --git a/server/src/controllers/OscController.js b/server/src/controllers/OscController.js index a9a59e1b4..5eff71e31 100644 --- a/server/src/controllers/OscController.js +++ b/server/src/controllers/OscController.js @@ -38,6 +38,12 @@ export const initiateOSC = (config) => { // get second part (command) switch (path.toLowerCase()) { + case 'onAir': + global.timer.onAir = true; + break; + case 'offAir': + global.timer.onAir = false; + break; case 'start': case 'play': console.log('calling play'); diff --git a/server/src/controllers/playbackController.js b/server/src/controllers/playbackController.js index 16b3d7f78..0e2477878 100644 --- a/server/src/controllers/playbackController.js +++ b/server/src/controllers/playbackController.js @@ -4,6 +4,22 @@ export const pbGet = async (req, res) => { res.send(global.timer.playState); }; +// Create controller for GET request to '/playback/onAir' +// Turns onAir flag to true +export const onAir = async (req, res) => { + console.log('Setting onAir to true'); + global.timer.onAir = true; + res.sendStatus(200); +}; + +// Create controller for GET request to '/playback/onAir' +// Turns onAir flag to true +export const offAir = async (req, res) => { + console.log('Setting onAir to false'); + global.timer.onAir = false; + res.sendStatus(200); +}; + // Create controller for GET request to '/playback/start' // Starts timer object export const pbStart = async (req, res) => { diff --git a/server/src/routes/playbackRouter.js b/server/src/routes/playbackRouter.js index 3b71b9c86..d31d1bc99 100644 --- a/server/src/routes/playbackRouter.js +++ b/server/src/routes/playbackRouter.js @@ -7,6 +7,12 @@ const playbackController = require('../controllers/playbackController'); // create route between controller and '/playback/' endpoint router.get('/', playbackController.pbGet); +// create route between controller and '/playback/onAir' endpoint +router.get('/onAir', playbackController.onAir); + +// create route between controller and '/playback/offAir' endpoint +router.get('/offAir', playbackController.offAir); + // create route between controller and '/playback/start' endpoint router.get('/start', playbackController.pbStart);