mirror of
https://github.com/cpvalente/ontime.git
synced 2026-08-21 06:59:09 +00:00
Feat/studio clock (#60)
This commit is contained in:
@@ -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
@@ -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
@@ -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.
@@ -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,
|
||||||
|
}
|
||||||
|
|||||||
+4
@@ -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);
|
||||||
|
});
|
||||||
|
})
|
||||||
@@ -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
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
|||||||
@@ -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;
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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
@@ -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
@@ -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",
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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();
|
||||||
|
|
||||||
|
|||||||
@@ -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) => {
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user