Feat/studio clock (#60)

This commit is contained in:
Carlos Valente
2021-12-13 21:29:56 +01:00
committed by GitHub
parent 627cce7ba6
commit 80c2e9a8ba
26 changed files with 1099 additions and 135 deletions
+29 -17
View File
@@ -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. 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` 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 > Web server default to presenter timer view
IP.ADDRESS:4001/speaker > Speaker / Stage timer view IP.ADDRESS:4001/preseter > Presenter / Stage timer view
IP.ADDRESS:4001/sm > Stage Manager / Backstage view IP.ADDRESS:4001/sm > Stage Manager / Backstage view
IP.ADDRESS:4001/public > Public / Foyer view IP.ADDRESS:4001/public > Public / Foyer view
IP.ADDRESS:4001/pip > Picture in Picture view IP.ADDRESS:4001/pip > Picture in Picture view
IP.ADDRESS:4001/lower > Lower Thirds IP.ADDRESS:4001/lower > Lower Thirds
IP.ADDRESS:4001/studio > Studio Clock
``` ```
More documentation available [here](https://cpvalente.gitbook.io/ontime/) 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] Send live messages to different screen types
- [x] Ability to differentiate between backstage and public data - [x] Ability to differentiate between backstage and public data
- [x] Manage delays workflow - [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] Roll mode: run independently using the system clock
- [x] Import event list from Excel
## Unopinionated ## 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] 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] 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] 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 ## Roadmap
### For version 1 ### Continued development
Almost reaching a feature set that we can call v1. Before that: There are several features planned in the roadmap.
- [ ] Mac OS version These will be implemented in a development friendly order unless there is user demand to bump any of them.
- [ ] Finish Documentation
### Continuing
- [ ] Linux version - [ ] Linux version
- [ ] Headless version (run server only anywhere, configure from a browser locally) - [ ] Headless version (run server only anywhere, configure from a browser locally)
- [ ] Companion integration - [ ] Companion module
- [ ] Lower Third Manager - [ ] Lower Third Manager
- [ ] Note only event - [ ] Note only event
- [ ] URL Aliases (define configurable aliases to ease onsite setup) - [ ] URL Aliases (define configurable aliases to ease onsite setup)
- [ ] Logging view - [ ] Logging view
- [ ] Reach Schedule: way to speedup timer to meet a deadline - [ ] Reach Schedule: way to speedup timer to meet a deadline
- [ ] Excel Import
- [ ] vMix integration - [ ] vMix integration
### For version 1
Almost reaching a feature set that we can call v1. Before that:
- [ ] Mac OS version
### Issues ### Issues
The app is still in pre-release and there are a few issues, mainly concerning style. 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 This will be receiving attention as we near v1 release
#### Style #### 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: Please ensure that windows settings have no display zoom (it is 125% by default)
- [ ] app needs improvement on handling zoomed interfaces
- [ ] Very long titles might cause interface to shift - [ ] Very long titles might cause interface to shift
# Help # Help
+4 -1
View File
@@ -24,6 +24,7 @@
"react-scripts": "4.0.3", "react-scripts": "4.0.3",
"socket.io-client": "^4.3.2", "socket.io-client": "^4.3.2",
"typeface-open-sans": "^1.1.13", "typeface-open-sans": "^1.1.13",
"use-fit-text": "^2.4.0",
"web-vitals": "^1.0.1" "web-vitals": "^1.0.1"
}, },
"scripts": { "scripts": {
@@ -50,5 +51,7 @@
"last 1 safari version" "last 1 safari version"
] ]
}, },
"devDependencies": {} "devDependencies": {
"sass": "^1.44.0"
}
} }
+12 -7
View File
@@ -22,6 +22,8 @@ const Lower = lazy(() =>
); );
const Pip = lazy(() => import('features/viewers/production/Pip')); const Pip = lazy(() => import('features/viewers/production/Pip'));
const StudioClock = lazy(() => import('features/viewers/studio/StudioClock'));
const queryClient = new QueryClient(); const queryClient = new QueryClient();
// Seemed to cause issues // Seemed to cause issues
// broadcastQueryClient({ // broadcastQueryClient({
@@ -29,12 +31,13 @@ const queryClient = new QueryClient();
// broadcastChannel: 'ontime', // broadcastChannel: 'ontime',
// }); // });
const SSpeaker = withSocket(PresenterView); const SPresenter = withSocket(PresenterView);
const SSpeakerSimple = withSocket(PresenterSimple); const SPresenterSimple = withSocket(PresenterSimple);
const SStageManager = withSocket(StageManager); const SStageManager = withSocket(StageManager);
const SPublic = withSocket(Public); const SPublic = withSocket(Public);
const SLowerThird = withSocket(Lower); const SLowerThird = withSocket(Lower);
const SPip = withSocket(Pip); const SPip = withSocket(Pip);
const SStudio = withSocket(StudioClock);
function App() { function App() {
// Handle keyboard shortcuts // Handle keyboard shortcuts
@@ -69,18 +72,20 @@ function App() {
<ErrorBoundary> <ErrorBoundary>
<Suspense fallback={null}> <Suspense fallback={null}>
<Switch> <Switch>
<Route exact path='/' component={SSpeaker} /> <Route exact path='/' component={SPresenter} />
<Route exact path='/sm' component={SStageManager} /> <Route exact path='/sm' component={SStageManager} />
<Route exact path='/speaker' component={SSpeaker} /> <Route exact path='/speaker' component={SPresenter} />
<Route exact path='/stage' component={SSpeaker} /> <Route exact path='/presenter' component={SPresenter} />
<Route exact path='/speakersimple' component={SSpeakerSimple} /> <Route exact path='/stage' component={SPresenter} />
<Route exact path='/presentersimple' component={SPresenterSimple} />
<Route exact path='/editor' component={Editor} /> <Route exact path='/editor' component={Editor} />
<Route exact path='/public' component={SPublic} /> <Route exact path='/public' component={SPublic} />
<Route exact path='/pip' component={SPip} /> <Route exact path='/pip' component={SPip} />
<Route exact path='/studio' component={SStudio} />
{/* Lower cannot have fallback */} {/* Lower cannot have fallback */}
<Route exact path='/lower' component={SLowerThird} /> <Route exact path='/lower' component={SLowerThird} />
{/* Send to default if nothing found */} {/* Send to default if nothing found */}
<Route component={SSpeaker} /> <Route component={SPresenter} />
</Switch> </Switch>
</Suspense> </Suspense>
</ErrorBoundary> </ErrorBoundary>
Binary file not shown.
+21 -6
View File
@@ -1,11 +1,13 @@
import PropTypes from "prop-types";
import { Link, Redirect } from 'react-router-dom'; import { Link, Redirect } from 'react-router-dom';
import { Image } from '@chakra-ui/react'; import { Image } from '@chakra-ui/react';
import { AnimatePresence, motion } from 'framer-motion'; import { AnimatePresence, motion } from 'framer-motion';
import { useState, useEffect, useCallback } from 'react'; import { useState, useEffect, useCallback } from 'react';
import navlogo from 'assets/images/logos/LOGO-72.png'; 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 [showNav, setShowNav] = useState(false);
const handleClick = () => { const handleClick = () => {
@@ -30,9 +32,10 @@ export default function NavLogo() {
}; };
}, [handleKeyPress]); }, [handleKeyPress]);
const baseOpacity = (isHidden) ? 0 : 0.5
return ( return (
<motion.div <motion.div
initial={{ opacity: 0.5 }} initial={{ opacity: baseOpacity }}
whileHover={{ opacity: 1 }} whileHover={{ opacity: 1 }}
className={style.navContainer} className={style.navContainer}
> >
@@ -51,12 +54,12 @@ export default function NavLogo() {
className={showNav ? style.nav : style.navHidden} className={showNav ? style.nav : style.navHidden}
> >
<Link <Link
to='/speaker' to='/presenter'
className={style.navItem} className={style.navItem}
tabIndex={1} tabIndex={1}
onKeyDownCapture={() => <Redirect push to='/speaker' />} onKeyDownCapture={() => <Redirect push to='/presenter' />}
> >
Speaker Presenter
</Link> </Link>
<Link <Link
to='/sm' to='/sm'
@@ -90,9 +93,21 @@ export default function NavLogo() {
> >
PIP PIP
</Link> </Link>
<Link
to='/studio'
className={style.navItem}
tabIndex={5}
onKeyDownCapture={() => <Redirect push to='/studio' />}
>
Studio Clock
</Link>
</motion.div> </motion.div>
)} )}
</AnimatePresence> </AnimatePresence>
</motion.div> </motion.div>
); );
} }
NavLogo.propTypes = {
isHidden: PropTypes.bool,
}
@@ -1,6 +1,9 @@
$nav-color: #fff;
.navContainer { .navContainer {
position: absolute; position: absolute;
right: 2vw; right: 2vw;
top: 1vw;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
align-items: flex-end; align-items: flex-end;
@@ -23,4 +26,5 @@
background-color: rgba(0, 0, 0, 0.7); background-color: rgba(0, 0, 0, 0.7);
padding: 0.5vh 1vw; padding: 0.5vh 1vw;
border-radius: 4px; border-radius: 4px;
color: $nav-color;
} }
@@ -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);
});
})
+2 -4
View File
@@ -40,11 +40,11 @@ export const stringFromMillis = (
* another go at simpler string formatting (counters) * another go at simpler string formatting (counters)
* @description Converts seconds to string representing time * @description Converts seconds to string representing time
* @param {number} seconds - time in seconds * @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 * @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 // add an extra 0 if necessary
const format = (val) => `0${Math.floor(val)}`.slice(-2); const format = (val) => `0${Math.floor(val)}`.slice(-2);
@@ -59,7 +59,6 @@ export function formatDisplay(seconds, hideZero) {
/** /**
* @description Converts milliseconds to seconds * @description Converts milliseconds to seconds
* @param {number} millis - time in seconds * @param {number} millis - time in seconds
* @param {boolean} hideZero - wether to show hours in case its 00
* @returns {number} Amount in seconds * @returns {number} Amount in seconds
*/ */
@@ -71,7 +70,6 @@ export const millisToSeconds = (millis) => {
/** /**
* @description Converts milliseconds to seconds * @description Converts milliseconds to seconds
* @param {number} millis - time in seconds * @param {number} millis - time in seconds
* @param {boolean} hideZero - wether to show hours in case its 00
* @returns {number} Amount in seconds * @returns {number} Amount in seconds
*/ */
+84
View File
@@ -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 <limit> 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;
};
+63 -37
View File
@@ -1,15 +1,16 @@
import { Editable, EditableInput, EditablePreview } from '@chakra-ui/editable'; import {Editable, EditableInput, EditablePreview} from '@chakra-ui/editable';
import { useEffect, useState } from 'react'; import {Switch} from "@chakra-ui/react";
import { useSocket } from 'app/context/socketContext'; import {useEffect, useState} from 'react';
import {useSocket} from 'app/context/socketContext';
import VisibleIconBtn from 'common/components/buttons/VisibleIconBtn'; import VisibleIconBtn from 'common/components/buttons/VisibleIconBtn';
import style from './MessageControl.module.css'; import style from './MessageControl.module.scss';
const inputProps = { const inputProps = {
size: 'sm', size: 'sm',
}; };
const InputRow = (props) => { const InputRow = (props) => {
const { label, placeholder, text, visible } = props; const {label, placeholder, text, visible} = props;
return ( return (
<> <>
@@ -22,8 +23,8 @@ const InputRow = (props) => {
className={style.inline} className={style.inline}
color={text === '' ? '#666' : 'inherit'} color={text === '' ? '#666' : 'inherit'}
> >
<EditablePreview className={style.padleft} /> <EditablePreview className={style.padleft}/>
<EditableInput className={style.padleft} /> <EditableInput className={style.padleft}/>
</Editable> </Editable>
<VisibleIconBtn <VisibleIconBtn
active={visible || undefined} active={visible || undefined}
@@ -49,33 +50,42 @@ export default function MessageControl() {
text: '', text: '',
visible: false, visible: false,
}); });
const [onAir, setonAir] = useState(false);
useEffect(() => { useEffect(() => {
if (socket == null) return; if (socket == null) return;
// Handle presenter messages // Handle presenter messages
socket.on('messages-presenter', (data) => { socket.on('messages-presenter', (data) => {
setPres({ ...data }); setPres({...data});
}); });
// Handle public messages // Handle public messages
socket.on('messages-public', (data) => { socket.on('messages-public', (data) => {
setPubl({ ...data }); setPubl({...data});
}); });
// Handle lower third messages // Handle lower third messages
socket.on('messages-lower', (data) => { 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 // Ask for up to date data
socket.emit('get-messages'); socket.emit('get-messages');
// Ask for onAir state
socket.emit('get-onAir');
// Clear listeners // Clear listeners
return () => { return () => {
socket.off('messages-public'); socket.off('messages-public');
socket.off('messages-presenter'); socket.off('messages-presenter');
socket.off('messages-lower'); socket.off('messages-lower');
socket.off('onAir');
}; };
}, [socket]); }, [socket]);
@@ -99,38 +109,54 @@ export default function MessageControl() {
case 'toggle-lower-visible': case 'toggle-lower-visible':
socket.emit('set-lower-visible', !lower.visible); socket.emit('set-lower-visible', !lower.visible);
break; break;
case 'toggle-onAir':
socket.emit('set-onAir', !onAir);
break;
default: default:
break; break;
} }
}; };
return ( return (
<div className={style.messageContainer}> <>
<InputRow <div className={style.messageContainer}>
label='Presenter screen message' <InputRow
placeholder='only the presenter screens see this' label='Presenter screen message'
text={pres.text} placeholder='only the presenter screens see this'
visible={pres.visible} text={pres.text}
changeHandler={(event) => messageControl('pres-text', event)} visible={pres.visible}
actionHandler={() => messageControl('toggle-pres-visible')} changeHandler={(event) => messageControl('pres-text', event)}
/> actionHandler={() => messageControl('toggle-pres-visible')}
<InputRow />
label='Public screen message' <InputRow
placeholder='public screens will render this' label='Public screen message'
text={publ.text} placeholder='public screens will render this'
visible={publ.visible} text={publ.text}
changeHandler={(event) => messageControl('publ-text', event)} visible={publ.visible}
actionHandler={() => messageControl('toggle-publ-visible')} changeHandler={(event) => messageControl('publ-text', event)}
/> actionHandler={() => messageControl('toggle-publ-visible')}
<InputRow />
label='Lower third message' <InputRow
placeholder='visible in lower third screen' label='Lower third message'
text={lower.text} placeholder='visible in lower third screen'
visible={lower.visible} text={lower.text}
changeHandler={(event) => messageControl('lower-text', event)} visible={lower.visible}
actionHandler={() => messageControl('toggle-lower-visible')} changeHandler={(event) => messageControl('lower-text', event)}
/> actionHandler={() => messageControl('toggle-lower-visible')}
</div> />
</div>
<div className={style.onAirToggle}>
<Switch
colorScheme='green'
size='md'
isChecked={onAir}
onChange={() => messageControl('toggle-onAir')}>
On Air?
</Switch>
<span className={style.oscLabel}>
{`/ontime/offAir << OSC >> /ontime/onAir`}
</span>
</div>
</>
); );
} }
@@ -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;
}
@@ -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;
}
}
+15 -4
View File
@@ -10,13 +10,9 @@ const withSocket = (Component) => {
const WrappedComponent = (props) => { const WrappedComponent = (props) => {
const { const {
data: eventsData, data: eventsData,
status: eventsDataStatus,
isError: eventsDataIsError,
} = useFetch(EVENTS_TABLE, fetchAllEvents); } = useFetch(EVENTS_TABLE, fetchAllEvents);
const { const {
data: genData, data: genData,
status: genDataStatus,
isError: genDataIsError,
} = useFetch(EVENT_TABLE, fetchEvent); } = useFetch(EVENT_TABLE, fetchEvent);
const [publicEvents, setPublicEvents] = useState([]); const [publicEvents, setPublicEvents] = useState([]);
@@ -58,6 +54,7 @@ const withSocket = (Component) => {
presenterNext: '', presenterNext: '',
}); });
const [selectedId, setSelectedId] = useState(null); const [selectedId, setSelectedId] = useState(null);
const [nextId, setNextId] = useState(null);
const [publicSelectedId, setPublicSelectedId] = useState(null); const [publicSelectedId, setPublicSelectedId] = useState(null);
const [general, setGeneral] = useState({ const [general, setGeneral] = useState({
title: '', title: '',
@@ -67,6 +64,7 @@ const withSocket = (Component) => {
endMessage: '', endMessage: '',
}); });
const [playback, setPlayback] = useState(null); const [playback, setPlayback] = useState(null);
const [onAir, setOnAir] = useState(false);
// Ask for update on load // Ask for update on load
useEffect(() => { useEffect(() => {
@@ -96,6 +94,9 @@ const withSocket = (Component) => {
socket.on('playstate', (data) => { socket.on('playstate', (data) => {
setPlayback(data); setPlayback(data);
}); });
socket.on('onAir', (data) => {
setOnAir(data);
});
// Handle titles // Handle titles
socket.on('titles', (data) => { socket.on('titles', (data) => {
@@ -112,6 +113,9 @@ const withSocket = (Component) => {
socket.on('publicselected-id', (data) => { socket.on('publicselected-id', (data) => {
setPublicSelectedId(data); setPublicSelectedId(data);
}); });
socket.on('next-id', (data) => {
setNextId(data);
});
// Ask for up to date data // Ask for up to date data
socket.emit('get-messages'); socket.emit('get-messages');
@@ -124,6 +128,7 @@ const withSocket = (Component) => {
// ask for playstate // ask for playstate
socket.emit('get-playstate'); socket.emit('get-playstate');
socket.emit('get-onAir')
// Ask for up titles // Ask for up titles
socket.emit('get-titles'); socket.emit('get-titles');
@@ -131,6 +136,7 @@ const withSocket = (Component) => {
// Ask for up selected // Ask for up selected
socket.emit('get-selected-id'); socket.emit('get-selected-id');
socket.emit('get-next-id');
// Clear listeners // Clear listeners
return () => { return () => {
@@ -139,9 +145,11 @@ const withSocket = (Component) => {
socket.off('messages-lower'); socket.off('messages-lower');
socket.off('timer'); socket.off('timer');
socket.off('playstate'); socket.off('playstate');
socket.off('onAir');
socket.off('titles'); socket.off('titles');
socket.off('publictitles'); socket.off('publictitles');
socket.off('selected-id'); socket.off('selected-id');
socket.emit('next-id');
}; };
}, [socket]); }, [socket]);
@@ -224,6 +232,7 @@ const withSocket = (Component) => {
...timer, ...timer,
finished: playback === 'start' && timer.running <= 0 && timer.startedAt, finished: playback === 'start' && timer.running <= 0 && timer.startedAt,
clock: stringFromMillis(timer.clock), clock: stringFromMillis(timer.clock),
clockNoSeconds: stringFromMillis(timer.clock, false),
playstate: playback, playstate: playback,
}; };
@@ -240,7 +249,9 @@ const withSocket = (Component) => {
backstageEvents={backstageEvents} backstageEvents={backstageEvents}
selectedId={selectedId} selectedId={selectedId}
publicSelectedId={publicSelectedId} publicSelectedId={publicSelectedId}
nextId={nextId}
general={general} general={general}
onAir={onAir}
/> />
); );
}; };
@@ -6,6 +6,7 @@ import NavLogo from 'common/components/nav/NavLogo';
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
import { AnimatePresence, motion } from 'framer-motion'; import { AnimatePresence, motion } from 'framer-motion';
import TitleSide from 'common/components/views/TitleSide'; import TitleSide from 'common/components/views/TitleSide';
import {getEventsWithDelay} from "../../../common/utils/eventsManager";
export default function StageManager(props) { export default function StageManager(props) {
const { publ, title, time, backstageEvents, selectedId, general } = props; const { publ, title, time, backstageEvents, selectedId, general } = props;
@@ -20,23 +21,9 @@ export default function StageManager(props) {
useEffect(() => { useEffect(() => {
if (backstageEvents == null) return; if (backstageEvents == null) return;
let events = [...backstageEvents]; setFilteredEvents(getEventsWithDelay(backstageEvents));
// Add running delay console.log('hhh', getEventsWithDelay(backstageEvents))
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);
}, [backstageEvents]); }, [backstageEvents]);
// Format messages // Format messages
@@ -11,7 +11,7 @@ export default function PresenterView(props) {
// Set window title // Set window title
useEffect(() => { useEffect(() => {
document.title = 'ontime - Speaker Screen'; document.title = 'ontime - Presenter Screen';
}, []); }, []);
const showOverlay = pres.text !== '' && pres.visible; const showOverlay = pres.text !== '' && pres.visible;
@@ -1,6 +1,7 @@
import { AnimatePresence, motion } from 'framer-motion'; import { AnimatePresence, motion } from 'framer-motion';
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
import style from './LowerClean.module.css'; import style from './LowerClean.module.css';
import NavLogo from "../../../../common/components/nav/NavLogo";
export default function LowerClean(props) { export default function LowerClean(props) {
const { lower, title, options } = props; const { lower, title, options } = props;
@@ -86,6 +87,9 @@ export default function LowerClean(props) {
fontSize: `${sizeMultiplier}vh`, fontSize: `${sizeMultiplier}vh`,
}} }}
> >
<NavLogo isHidden />
<AnimatePresence> <AnimatePresence>
{showLower && ( {showLower && (
<motion.div <motion.div
@@ -1,6 +1,7 @@
import { AnimatePresence, motion } from 'framer-motion'; import { AnimatePresence, motion } from 'framer-motion';
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
import style from './LowerLines.module.css'; import style from './LowerLines.module.css';
import NavLogo from "../../../../common/components/nav/NavLogo";
export default function LowerLines(props) { export default function LowerLines(props) {
const { lower, title, options } = props; const { lower, title, options } = props;
@@ -126,6 +127,9 @@ export default function LowerLines(props) {
fontSize: `${sizeMultiplier}vh`, fontSize: `${sizeMultiplier}vh`,
}} }}
> >
<NavLogo isHidden />
<AnimatePresence> <AnimatePresence>
{showLower && ( {showLower && (
<motion.div <motion.div
@@ -0,0 +1,87 @@
import style from "./StudioClock.module.scss";
import useFitText from "use-fit-text";
import NavLogo from "../../../common/components/nav/NavLogo";
import {useEffect, useState} from "react";
import {formatDisplay} from "../../../common/utils/dateConfig";
import {formatEventList, trimEventlist} from "../../../common/utils/eventsManager";
export default function StudioClock(props) {
const { title, time, backstageEvents, selectedId, nextId, onAir } = props;
const { fontSize, ref } = useFitText({maxFontSize:500});
const [hoursNow, minutesNow] = time.clockNoSeconds.split(':');
const [schedule, setSchedule] = useState([]);
const hoursIndicators = [...Array(12).keys()];
const minutesIndicators = [...Array(60).keys()];
const MAX_TITLES = 8;
// Set window title
useEffect(() => {
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 (
<div className={style.container}>
<NavLogo />
<div className={style.clockContainer}>
<div className={style.time}>{time.clockNoSeconds}</div>
<div
ref={ref}
className={style.nextTitle}
style={{fontSize, height:'100px', width:'100%', maxWidth:'680px'}}
>
{title.titleNext}
</div>
<div className={time.running > 0 ? style.nextCountdown: style.nextCountdown__overtime}>
{selectedId != null && formatDisplay(time.running)}
</div>
<div className={style.indicators}>
{hoursIndicators.map(i => (
<div
key = {i}
className={i <= hoursNow ? style.hours__active : style.hours}
style={{
transform: `rotate(${360/12*i-90}deg) translateX(380px)`
}}/>
)
)}
{minutesIndicators.map(i => (
<div
key={i}
className={i <= minutesNow ? style.min__active : style.min}
style={{
transform: `rotate(${360/60*i-90}deg) translateX(415px)`
}}/>
)
)}
</div>
</div>
<div className={style.scheduleContainer}>
<div className={onAir ? style.onAir : style.onAir__idle}>ON AIR</div>
<div className={style.schedule}>
<ul>
{schedule.map((s) => (
<li key={s.id} className={s.isNow ? style.now : s.isNext ? style.next : ''}>
<div className={s.isNow ? style.decorator__active : s.isNext ? style.decorator__next : style.decorator}/>{`${s.time} ${s.title}`}
</li>
))
}
</ul>
</div>
</div>
</div>
);
}
@@ -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;
}
}
+57 -2
View File
@@ -3073,6 +3073,14 @@ anymatch@^3.0.3, anymatch@~3.1.1:
normalize-path "^3.0.0" normalize-path "^3.0.0"
picomatch "^2.0.4" 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: aproba@^1.1.1:
version "1.2.0" version "1.2.0"
resolved "https://registry.yarnpkg.com/aproba/-/aproba-1.2.0.tgz#6802e6264efd18c790a1b0d517f0f2627bf2c94a" 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" resolved "https://registry.yarnpkg.com/check-types/-/check-types-11.1.2.tgz#86a7c12bf5539f6324eb0e70ca8896c0e38f3e2f"
integrity sha512-tzWzvgePgLORb9/3a0YenggReLKAIb2owL03H2Xdoe5pKcUyWRSEQ8xfCar8t2SIAuEDwtmx2da1YB52YuHQMQ== 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: chokidar@^2.1.8:
version "2.1.8" version "2.1.8"
resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-2.1.8.tgz#804b3a7b6a99358c3c5c61e71d8728f041cff917" resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-2.1.8.tgz#804b3a7b6a99358c3c5c61e71d8728f041cff917"
@@ -6054,7 +6077,7 @@ fsevents@^1.2.7:
bindings "^1.5.0" bindings "^1.5.0"
nan "^2.12.1" 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" version "2.3.2"
resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.3.2.tgz#8a526f78b8fdf4623b709e0b975c52c24c02fd1a" resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.3.2.tgz#8a526f78b8fdf4623b709e0b975c52c24c02fd1a"
integrity sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA== integrity sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==
@@ -6137,7 +6160,7 @@ glob-parent@^3.1.0:
is-glob "^3.1.0" is-glob "^3.1.0"
path-dirname "^1.0.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" version "5.1.2"
resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-5.1.2.tgz#869832c58034fe68a4093c17dc15e8340d8401c4" resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-5.1.2.tgz#869832c58034fe68a4093c17dc15e8340d8401c4"
integrity sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow== 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" resolved "https://registry.yarnpkg.com/immer/-/immer-8.0.1.tgz#9c73db683e2b3975c424fb0572af5889877ae656"
integrity sha512-aqXhGP7//Gui2+UrEtvxZxSquQVXTpZ7KDxfCcKAF3Vysvw0CViVaW9RZ1j1xlIYqaaaipBoqdqeibkc18PNvA== 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: import-cwd@^2.0.0:
version "2.1.0" version "2.1.0"
resolved "https://registry.yarnpkg.com/import-cwd/-/import-cwd-2.1.0.tgz#aa6cf36e722761285cb371ec6519f53e2435b0a9" resolved "https://registry.yarnpkg.com/import-cwd/-/import-cwd-2.1.0.tgz#aa6cf36e722761285cb371ec6519f53e2435b0a9"
@@ -10366,6 +10394,13 @@ readdirp@~3.5.0:
dependencies: dependencies:
picomatch "^2.2.1" 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: recursive-readdir@2.2.2:
version "2.2.2" version "2.2.2"
resolved "https://registry.yarnpkg.com/recursive-readdir/-/recursive-readdir-2.2.2.tgz#9946fb3274e1628de6e36b2f6714953b4845094f" 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" resolved "https://registry.yarnpkg.com/requires-port/-/requires-port-1.0.0.tgz#925d2601d39ac485e091cf0da5c6e694dc3dcaff"
integrity sha1-kl0mAdOaxIXgkc8NpcbmlNw9yv8= 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: resolve-cwd@^2.0.0:
version "2.0.0" version "2.0.0"
resolved "https://registry.yarnpkg.com/resolve-cwd/-/resolve-cwd-2.0.0.tgz#00a9f7387556e27038eae232caa372a6a59b665a" 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" schema-utils "^3.0.0"
semver "^7.3.2" 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: sax@~1.2.4:
version "1.2.4" version "1.2.4"
resolved "https://registry.yarnpkg.com/sax/-/sax-1.2.4.tgz#2816234e2378bddc4e5354fab5caa895df7100d9" 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" resolved "https://registry.yarnpkg.com/use-callback-ref/-/use-callback-ref-1.2.5.tgz#6115ed242cfbaed5915499c0a9842ca2912f38a5"
integrity sha512-gN3vgMISAgacF7sqsLPByqoePooY3n2emTH59Ur5d/M8eg4WTWu1xp8i8DHjohftIyEx0S08RiYxbffr4j8Peg== 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: use-memo-one@^1.1.1:
version "1.1.2" version "1.1.2"
resolved "https://registry.yarnpkg.com/use-memo-one/-/use-memo-one-1.1.2.tgz#0c8203a329f76e040047a35a1197defe342fab20" resolved "https://registry.yarnpkg.com/use-memo-one/-/use-memo-one-1.1.2.tgz#0c8203a329f76e040047a35a1197defe342fab20"
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "ontime", "name": "ontime",
"version": "0.4.1", "version": "0.4.2",
"author": "Carlos Valente", "author": "Carlos Valente",
"description": "Time keeping for live events", "description": "Time keeping for live events",
"repository": "https://github.com/cpvalente/ontime", "repository": "https://github.com/cpvalente/ontime",
+24 -5
View File
@@ -63,6 +63,7 @@ export class EventTimer extends Timer {
nextPublicEventId = null; nextPublicEventId = null;
numEvents = null; numEvents = null;
_eventlist = null; _eventlist = null;
onAir = false;
constructor(httpServer, oscClient, config) { constructor(httpServer, oscClient, config) {
// call super constructor // call super constructor
@@ -199,7 +200,7 @@ export class EventTimer extends Timer {
// send current timer // send current timer
broadcastTimer() { broadcastTimer() {
// through websockets // through websockets
this.io.emit('timer', this.getObject()); this.io.emit('timer', this.getTimes());
// through OSC, only if running // through OSC, only if running
if (this.state === 'start' || this.state === 'roll') { if (this.state === 'start' || this.state === 'roll') {
@@ -211,7 +212,7 @@ export class EventTimer extends Timer {
// broadcast state // broadcast state
broadcastState(update = true) { 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('playstate', this.state);
this.io.emit('selected', { this.io.emit('selected', {
id: this.selectedEventId, id: this.selectedEventId,
@@ -224,6 +225,7 @@ export class EventTimer extends Timer {
this.io.emit('publicnext-id', this.nextPublicEventId); this.io.emit('publicnext-id', this.nextPublicEventId);
this.io.emit('titles', this.titles); this.io.emit('titles', this.titles);
this.io.emit('publictitles', this.titlesPublic); this.io.emit('publictitles', this.titlesPublic);
this.io.emit('onAir', this.onAir);
} }
// broadcast message // broadcast message
@@ -359,7 +361,7 @@ export class EventTimer extends Timer {
); );
// send state // send state
socket.emit('timer', this.getObject()); socket.emit('timer', this.getTimes());
socket.emit('playstate', this.state); socket.emit('playstate', this.state);
socket.emit('selected-id', this.selectedEventId); socket.emit('selected-id', this.selectedEventId);
socket.emit('next-id', this.nextEventId); socket.emit('next-id', this.nextEventId);
@@ -385,7 +387,7 @@ export class EventTimer extends Timer {
/*******************************************/ /*******************************************/
// general playback state // general playback state
socket.on('get-state', () => { socket.on('get-state', () => {
socket.emit('timer', this.getObject()); socket.emit('timer', this.getTimes());
socket.emit('playstate', this.state); socket.emit('playstate', this.state);
socket.emit('selected-id', this.selectedEventId); socket.emit('selected-id', this.selectedEventId);
socket.emit('next-id', this.nextEventId); socket.emit('next-id', this.nextEventId);
@@ -400,7 +402,7 @@ export class EventTimer extends Timer {
}); });
socket.on('get-timer', () => { socket.on('get-timer', () => {
socket.emit('timer', this.getObject()); socket.emit('timer', this.getTimes());
}); });
socket.on('increment-timer', (data) => { socket.on('increment-timer', (data) => {
@@ -419,6 +421,15 @@ export class EventTimer extends Timer {
socket.emit('playstate', this.state); 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 // selection data
socket.on('get-selected', () => { 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() { start() {
// if there is nothing selected, no nothing // if there is nothing selected, no nothing
if (this.selectedEventId == null) return; if (this.selectedEventId == null) return;
+2 -2
View File
@@ -139,8 +139,8 @@ export class Timer {
return this.duration - this.current; return this.duration - this.current;
} }
// getObject // get time object
getObject(update = true) { getTimes(update = true) {
// update timer // update timer
if (update) this.update(); if (update) this.update();
+6
View File
@@ -38,6 +38,12 @@ export const initiateOSC = (config) => {
// get second part (command) // get second part (command)
switch (path.toLowerCase()) { switch (path.toLowerCase()) {
case 'onAir':
global.timer.onAir = true;
break;
case 'offAir':
global.timer.onAir = false;
break;
case 'start': case 'start':
case 'play': case 'play':
console.log('calling play'); console.log('calling play');
@@ -4,6 +4,22 @@ export const pbGet = async (req, res) => {
res.send(global.timer.playState); 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' // Create controller for GET request to '/playback/start'
// Starts timer object // Starts timer object
export const pbStart = async (req, res) => { export const pbStart = async (req, res) => {
+6
View File
@@ -7,6 +7,12 @@ const playbackController = require('../controllers/playbackController');
// create route between controller and '/playback/' endpoint // create route between controller and '/playback/' endpoint
router.get('/', playbackController.pbGet); 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 // create route between controller and '/playback/start' endpoint
router.get('/start', playbackController.pbStart); router.get('/start', playbackController.pbStart);