mirror of
https://github.com/cpvalente/ontime.git
synced 2026-08-03 06:28:01 +00:00
Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 52f02f153c | |||
| 3582ce18e1 | |||
| 80c2e9a8ba |
@@ -18,17 +18,18 @@ Once installed and running, ontime starts a background server that is the heart
|
||||
The app, is used to add / edit your running order in the event list, and running the timers using the Playback Control function.
|
||||
|
||||
From here, any device in the same network with a browser is able to render the views as described. This is done by reaching the ontime server at the _default port 4001_ eg: `localhost:4001` or `192.168.1.3:4001`
|
||||
You can then use the the ontime logo on the top right corner to select the desired view.
|
||||
You can then use the ontime logo in the top right corner to select the desired view (event in the lower thirds view, where it is hidden).
|
||||
|
||||
In case of unnatended machines or automations, it is possible to use different URL to recall individual views
|
||||
In case of unattended machines or automations, it is possible to use different URL to recall individual views
|
||||
|
||||
```
|
||||
IP.ADDRESS:4001 > Web server default to stage timer view
|
||||
IP.ADDRESS:4001/speaker > Speaker / Stage timer view
|
||||
IP.ADDRESS:4001 > Web server default to presenter timer view
|
||||
IP.ADDRESS:4001/preseter > Presenter / Stage timer view
|
||||
IP.ADDRESS:4001/sm > Stage Manager / Backstage view
|
||||
IP.ADDRESS:4001/public > Public / Foyer view
|
||||
IP.ADDRESS:4001/pip > Picture in Picture view
|
||||
IP.ADDRESS:4001/lower > Lower Thirds
|
||||
IP.ADDRESS:4001/studio > Studio Clock
|
||||
```
|
||||
|
||||
More documentation available [here](https://cpvalente.gitbook.io/ontime/)
|
||||
@@ -43,43 +44,54 @@ More documentation available [here](https://cpvalente.gitbook.io/ontime/)
|
||||
- [x] Send live messages to different screen types
|
||||
- [x] Ability to differentiate between backstage and public data
|
||||
- [x] Manage delays workflow
|
||||
- [x] OSC Control and Feedback
|
||||
- [x] Open Sound Control (OSC) Control and Feedback
|
||||
- [x] Roll mode: run independently using the system clock
|
||||
- [x] Import event list from Excel
|
||||
|
||||
## Unopinionated
|
||||
We are not interested in forcing workflows and have made ontime so it is flexible to whichever way you would like to work.
|
||||
We are not interested in forcing workflows and have made ontime, so it is flexible to whichever way you would like to work.
|
||||
|
||||
|
||||
- [x] You do not need an order list to use the timer. Create an empty event and the OSC API works just the same
|
||||
- [x] If you want just the info screens, no need to use the timer!
|
||||
- [x] Dont have or care for a schedule?
|
||||
- [x] Don't have or care for a schedule?
|
||||
- [x] a single event with no data is enough to use the OSC API and get going
|
||||
- [x] use the order list to create a set of quick timers by setting the beggining and start times to 00:00 and 00:10 (**BAM**! 10 minute timer). You can quick recall this with OSC as always
|
||||
- [x] use the order list to create a set of quick timers by setting the beginning and start times to 00:00 and 00:10 (**BAM**! 10 minute timer). You can quickly recall this with OSC as always
|
||||
|
||||
## Integrations and Workflow
|
||||
The app is being currently developed to a wide user base, from broadcast to entertainment and conference halls.
|
||||
|
||||
Taking advantage of the integrations in Ontime, we currently use Ontime with:
|
||||
- `disguise`: trigger ontime from d3's timeline using the **OSC API**, **render views** using d3's webmodule
|
||||
- `OBS`: **render views** using the Browser Module
|
||||
- `QLab`: trigger ontime using **OSC API**
|
||||
- `Companion`: trigger ontime and manipulate timer using **OSC API**
|
||||
|
||||
|
||||
## Roadmap
|
||||
### For version 1
|
||||
Almost reaching a feature set that we can call v1. Before that:
|
||||
- [ ] Mac OS version
|
||||
- [ ] Finish Documentation
|
||||
### Continuing
|
||||
### Continued development
|
||||
There are several features planned in the roadmap.
|
||||
These will be implemented in a development friendly order unless there is user demand to bump any of them.
|
||||
- [ ] Linux version
|
||||
- [ ] Headless version (run server only anywhere, configure from a browser locally)
|
||||
- [ ] Companion integration
|
||||
- [ ] Companion module
|
||||
- [ ] Lower Third Manager
|
||||
- [ ] Note only event
|
||||
- [ ] URL Aliases (define configurable aliases to ease onsite setup)
|
||||
- [ ] Logging view
|
||||
- [ ] Reach Schedule: way to speedup timer to meet a deadline
|
||||
- [ ] Excel Import
|
||||
- [ ] vMix integration
|
||||
|
||||
### For version 1
|
||||
Almost reaching a feature set that we can call v1. Before that:
|
||||
- [ ] Mac OS version
|
||||
|
||||
### Issues
|
||||
The app is still in pre-release and there are a few issues, mainly concerning style.
|
||||
This will be receiving attention as we near v1 release
|
||||
|
||||
#### Style
|
||||
- [ ] App appears visually broken: Please ensure that windows settings have no display zoom (it is 125% by default)
|
||||
- [ ] app needs improvement on handling zoomed interfaces
|
||||
- [ ] App needs improvement on handling zoomed interfaces: Please ensure that windows settings have no display zoom (it is 125% by default)
|
||||
- [ ] Very long titles might cause interface to shift
|
||||
|
||||
# Help
|
||||
|
||||
+4
-1
@@ -24,6 +24,7 @@
|
||||
"react-scripts": "4.0.3",
|
||||
"socket.io-client": "^4.3.2",
|
||||
"typeface-open-sans": "^1.1.13",
|
||||
"use-fit-text": "^2.4.0",
|
||||
"web-vitals": "^1.0.1"
|
||||
},
|
||||
"scripts": {
|
||||
@@ -50,5 +51,7 @@
|
||||
"last 1 safari version"
|
||||
]
|
||||
},
|
||||
"devDependencies": {}
|
||||
"devDependencies": {
|
||||
"sass": "^1.44.0"
|
||||
}
|
||||
}
|
||||
|
||||
+12
-7
@@ -22,6 +22,8 @@ const Lower = lazy(() =>
|
||||
);
|
||||
const Pip = lazy(() => import('features/viewers/production/Pip'));
|
||||
|
||||
const StudioClock = lazy(() => import('features/viewers/studio/StudioClock'));
|
||||
|
||||
const queryClient = new QueryClient();
|
||||
// Seemed to cause issues
|
||||
// broadcastQueryClient({
|
||||
@@ -29,12 +31,13 @@ const queryClient = new QueryClient();
|
||||
// broadcastChannel: 'ontime',
|
||||
// });
|
||||
|
||||
const SSpeaker = withSocket(PresenterView);
|
||||
const SSpeakerSimple = withSocket(PresenterSimple);
|
||||
const SPresenter = withSocket(PresenterView);
|
||||
const SPresenterSimple = withSocket(PresenterSimple);
|
||||
const SStageManager = withSocket(StageManager);
|
||||
const SPublic = withSocket(Public);
|
||||
const SLowerThird = withSocket(Lower);
|
||||
const SPip = withSocket(Pip);
|
||||
const SStudio = withSocket(StudioClock);
|
||||
|
||||
function App() {
|
||||
// Handle keyboard shortcuts
|
||||
@@ -69,18 +72,20 @@ function App() {
|
||||
<ErrorBoundary>
|
||||
<Suspense fallback={null}>
|
||||
<Switch>
|
||||
<Route exact path='/' component={SSpeaker} />
|
||||
<Route exact path='/' component={SPresenter} />
|
||||
<Route exact path='/sm' component={SStageManager} />
|
||||
<Route exact path='/speaker' component={SSpeaker} />
|
||||
<Route exact path='/stage' component={SSpeaker} />
|
||||
<Route exact path='/speakersimple' component={SSpeakerSimple} />
|
||||
<Route exact path='/speaker' component={SPresenter} />
|
||||
<Route exact path='/presenter' component={SPresenter} />
|
||||
<Route exact path='/stage' component={SPresenter} />
|
||||
<Route exact path='/presentersimple' component={SPresenterSimple} />
|
||||
<Route exact path='/editor' component={Editor} />
|
||||
<Route exact path='/public' component={SPublic} />
|
||||
<Route exact path='/pip' component={SPip} />
|
||||
<Route exact path='/studio' component={SStudio} />
|
||||
{/* Lower cannot have fallback */}
|
||||
<Route exact path='/lower' component={SLowerThird} />
|
||||
{/* Send to default if nothing found */}
|
||||
<Route component={SSpeaker} />
|
||||
<Route component={SPresenter} />
|
||||
</Switch>
|
||||
</Suspense>
|
||||
</ErrorBoundary>
|
||||
|
||||
Binary file not shown.
@@ -1,11 +1,13 @@
|
||||
import PropTypes from "prop-types";
|
||||
import { Link, Redirect } from 'react-router-dom';
|
||||
import { Image } from '@chakra-ui/react';
|
||||
import { AnimatePresence, motion } from 'framer-motion';
|
||||
import { useState, useEffect, useCallback } from 'react';
|
||||
import navlogo from 'assets/images/logos/LOGO-72.png';
|
||||
import style from './NavLogo.module.css';
|
||||
import style from './NavLogo.module.scss';
|
||||
|
||||
export default function NavLogo() {
|
||||
export default function NavLogo(props) {
|
||||
const {isHidden} = props;
|
||||
const [showNav, setShowNav] = useState(false);
|
||||
|
||||
const handleClick = () => {
|
||||
@@ -30,9 +32,10 @@ export default function NavLogo() {
|
||||
};
|
||||
}, [handleKeyPress]);
|
||||
|
||||
const baseOpacity = (isHidden) ? 0 : 0.5
|
||||
return (
|
||||
<motion.div
|
||||
initial={{ opacity: 0.5 }}
|
||||
initial={{ opacity: baseOpacity }}
|
||||
whileHover={{ opacity: 1 }}
|
||||
className={style.navContainer}
|
||||
>
|
||||
@@ -51,12 +54,12 @@ export default function NavLogo() {
|
||||
className={showNav ? style.nav : style.navHidden}
|
||||
>
|
||||
<Link
|
||||
to='/speaker'
|
||||
to='/presenter'
|
||||
className={style.navItem}
|
||||
tabIndex={1}
|
||||
onKeyDownCapture={() => <Redirect push to='/speaker' />}
|
||||
onKeyDownCapture={() => <Redirect push to='/presenter' />}
|
||||
>
|
||||
Speaker
|
||||
Presenter
|
||||
</Link>
|
||||
<Link
|
||||
to='/sm'
|
||||
@@ -90,9 +93,21 @@ export default function NavLogo() {
|
||||
>
|
||||
PIP
|
||||
</Link>
|
||||
<Link
|
||||
to='/studio'
|
||||
className={style.navItem}
|
||||
tabIndex={5}
|
||||
onKeyDownCapture={() => <Redirect push to='/studio' />}
|
||||
>
|
||||
Studio Clock
|
||||
</Link>
|
||||
</motion.div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
</motion.div>
|
||||
);
|
||||
}
|
||||
|
||||
NavLogo.propTypes = {
|
||||
isHidden: PropTypes.bool,
|
||||
}
|
||||
|
||||
+4
@@ -1,6 +1,9 @@
|
||||
$nav-color: #fff;
|
||||
|
||||
.navContainer {
|
||||
position: absolute;
|
||||
right: 2vw;
|
||||
top: 1vw;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-end;
|
||||
@@ -23,4 +26,5 @@
|
||||
background-color: rgba(0, 0, 0, 0.7);
|
||||
padding: 0.5vh 1vw;
|
||||
border-radius: 4px;
|
||||
color: $nav-color;
|
||||
}
|
||||
@@ -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)
|
||||
* @description Converts seconds to string representing time
|
||||
* @param {number} seconds - time in seconds
|
||||
* @param {boolean} hideZero - wether to show hours in case its 00
|
||||
* @param {boolean} [hideZero] - whether to show hours in case its 00
|
||||
* @returns {string} String representing absolute time 00:12:02
|
||||
*/
|
||||
|
||||
export function formatDisplay(seconds, hideZero) {
|
||||
export function formatDisplay(seconds, hideZero=false) {
|
||||
// add an extra 0 if necessary
|
||||
const format = (val) => `0${Math.floor(val)}`.slice(-2);
|
||||
|
||||
@@ -59,7 +59,6 @@ export function formatDisplay(seconds, hideZero) {
|
||||
/**
|
||||
* @description Converts milliseconds to seconds
|
||||
* @param {number} millis - time in seconds
|
||||
* @param {boolean} hideZero - wether to show hours in case its 00
|
||||
* @returns {number} Amount in seconds
|
||||
*/
|
||||
|
||||
@@ -71,7 +70,6 @@ export const millisToSeconds = (millis) => {
|
||||
/**
|
||||
* @description Converts milliseconds to seconds
|
||||
* @param {number} millis - time in seconds
|
||||
* @param {boolean} hideZero - wether to show hours in case its 00
|
||||
* @returns {number} Amount in seconds
|
||||
*/
|
||||
|
||||
|
||||
@@ -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 { useEffect, useState } from 'react';
|
||||
import { useSocket } from 'app/context/socketContext';
|
||||
import {Editable, EditableInput, EditablePreview} from '@chakra-ui/editable';
|
||||
import {Switch} from "@chakra-ui/react";
|
||||
import {useEffect, useState} from 'react';
|
||||
import {useSocket} from 'app/context/socketContext';
|
||||
import VisibleIconBtn from 'common/components/buttons/VisibleIconBtn';
|
||||
import style from './MessageControl.module.css';
|
||||
import style from './MessageControl.module.scss';
|
||||
|
||||
const inputProps = {
|
||||
size: 'sm',
|
||||
};
|
||||
|
||||
const InputRow = (props) => {
|
||||
const { label, placeholder, text, visible } = props;
|
||||
const {label, placeholder, text, visible} = props;
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -22,8 +23,8 @@ const InputRow = (props) => {
|
||||
className={style.inline}
|
||||
color={text === '' ? '#666' : 'inherit'}
|
||||
>
|
||||
<EditablePreview className={style.padleft} />
|
||||
<EditableInput className={style.padleft} />
|
||||
<EditablePreview className={style.padleft}/>
|
||||
<EditableInput className={style.padleft}/>
|
||||
</Editable>
|
||||
<VisibleIconBtn
|
||||
active={visible || undefined}
|
||||
@@ -49,33 +50,42 @@ export default function MessageControl() {
|
||||
text: '',
|
||||
visible: false,
|
||||
});
|
||||
const [onAir, setonAir] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (socket == null) return;
|
||||
|
||||
// Handle presenter messages
|
||||
socket.on('messages-presenter', (data) => {
|
||||
setPres({ ...data });
|
||||
setPres({...data});
|
||||
});
|
||||
|
||||
// Handle public messages
|
||||
socket.on('messages-public', (data) => {
|
||||
setPubl({ ...data });
|
||||
setPubl({...data});
|
||||
});
|
||||
|
||||
// Handle lower third messages
|
||||
socket.on('messages-lower', (data) => {
|
||||
setLower({ ...data });
|
||||
setLower({...data});
|
||||
});
|
||||
|
||||
// Handle lower third messages
|
||||
socket.on('onAir', (data) => {
|
||||
setonAir(data);
|
||||
});
|
||||
|
||||
// Ask for up to date data
|
||||
socket.emit('get-messages');
|
||||
// Ask for onAir state
|
||||
socket.emit('get-onAir');
|
||||
|
||||
// Clear listeners
|
||||
return () => {
|
||||
socket.off('messages-public');
|
||||
socket.off('messages-presenter');
|
||||
socket.off('messages-lower');
|
||||
socket.off('onAir');
|
||||
};
|
||||
}, [socket]);
|
||||
|
||||
@@ -99,38 +109,54 @@ export default function MessageControl() {
|
||||
case 'toggle-lower-visible':
|
||||
socket.emit('set-lower-visible', !lower.visible);
|
||||
break;
|
||||
|
||||
case 'toggle-onAir':
|
||||
socket.emit('set-onAir', !onAir);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={style.messageContainer}>
|
||||
<InputRow
|
||||
label='Presenter screen message'
|
||||
placeholder='only the presenter screens see this'
|
||||
text={pres.text}
|
||||
visible={pres.visible}
|
||||
changeHandler={(event) => messageControl('pres-text', event)}
|
||||
actionHandler={() => messageControl('toggle-pres-visible')}
|
||||
/>
|
||||
<InputRow
|
||||
label='Public screen message'
|
||||
placeholder='public screens will render this'
|
||||
text={publ.text}
|
||||
visible={publ.visible}
|
||||
changeHandler={(event) => messageControl('publ-text', event)}
|
||||
actionHandler={() => messageControl('toggle-publ-visible')}
|
||||
/>
|
||||
<InputRow
|
||||
label='Lower third message'
|
||||
placeholder='visible in lower third screen'
|
||||
text={lower.text}
|
||||
visible={lower.visible}
|
||||
changeHandler={(event) => messageControl('lower-text', event)}
|
||||
actionHandler={() => messageControl('toggle-lower-visible')}
|
||||
/>
|
||||
</div>
|
||||
<>
|
||||
<div className={style.messageContainer}>
|
||||
<InputRow
|
||||
label='Presenter screen message'
|
||||
placeholder='only the presenter screens see this'
|
||||
text={pres.text}
|
||||
visible={pres.visible}
|
||||
changeHandler={(event) => messageControl('pres-text', event)}
|
||||
actionHandler={() => messageControl('toggle-pres-visible')}
|
||||
/>
|
||||
<InputRow
|
||||
label='Public screen message'
|
||||
placeholder='public screens will render this'
|
||||
text={publ.text}
|
||||
visible={publ.visible}
|
||||
changeHandler={(event) => messageControl('publ-text', event)}
|
||||
actionHandler={() => messageControl('toggle-publ-visible')}
|
||||
/>
|
||||
<InputRow
|
||||
label='Lower third message'
|
||||
placeholder='visible in lower third screen'
|
||||
text={lower.text}
|
||||
visible={lower.visible}
|
||||
changeHandler={(event) => messageControl('lower-text', event)}
|
||||
actionHandler={() => messageControl('toggle-lower-visible')}
|
||||
/>
|
||||
</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;
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
import { memo } from 'react';
|
||||
import style from './PlaybackControl.module.css';
|
||||
import style from './PlaybackControl.module.scss';
|
||||
import StartIconBtn from 'common/components/buttons/StartIconBtn';
|
||||
import PauseIconBtn from 'common/components/buttons/PauseIconBtn';
|
||||
import PrevIconBtn from 'common/components/buttons/PrevIconBtn';
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import style from './PlaybackControl.module.css';
|
||||
import style from './PlaybackControl.module.scss';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useSocket } from 'app/context/socketContext';
|
||||
import PlaybackButtons from './PlaybackButtons';
|
||||
|
||||
+4
@@ -97,6 +97,10 @@
|
||||
grid-area: fin;
|
||||
}
|
||||
|
||||
.roll {
|
||||
grid-area: 2 / 2 / 2 / 4 ;
|
||||
}
|
||||
|
||||
.time {
|
||||
color: #ccc;
|
||||
font-size: 1.1em;
|
||||
@@ -1,9 +1,9 @@
|
||||
import style from './PlaybackControl.module.css';
|
||||
import style from './PlaybackControl.module.scss';
|
||||
import Countdown from 'common/components/countdown/Countdown';
|
||||
import { stringFromMillis } from 'common/utils/dateConfig';
|
||||
import { Tooltip } from '@chakra-ui/react';
|
||||
import { Button } from '@chakra-ui/button';
|
||||
import { memo } from 'react';
|
||||
import {stringFromMillis} from 'common/utils/dateConfig';
|
||||
import {Tooltip} from '@chakra-ui/react';
|
||||
import {Button} from '@chakra-ui/button';
|
||||
import {memo} from 'react';
|
||||
|
||||
const areEqual = (prevProps, nextProps) => {
|
||||
return (
|
||||
@@ -16,7 +16,7 @@ const areEqual = (prevProps, nextProps) => {
|
||||
};
|
||||
|
||||
const PlaybackTimer = (props) => {
|
||||
const { timer, playback, handleIncrement } = props;
|
||||
const {timer, playback, handleIncrement} = props;
|
||||
const started = stringFromMillis(timer.startedAt, true);
|
||||
const finish = stringFromMillis(timer.expectedFinish, true);
|
||||
const isNegative = timer.running < 0;
|
||||
@@ -28,7 +28,7 @@ const PlaybackTimer = (props) => {
|
||||
width: '2.9em',
|
||||
colorScheme: 'whiteAlpha',
|
||||
variant: 'outline',
|
||||
_focus: { boxShadow: 'none' },
|
||||
_focus: {boxShadow: 'none'},
|
||||
};
|
||||
|
||||
return (
|
||||
@@ -36,12 +36,12 @@ const PlaybackTimer = (props) => {
|
||||
<div className={style.timeContainer}>
|
||||
<div className={style.indicators}>
|
||||
<Tooltip label='Roll mode active'>
|
||||
<div className={isRolling ? style.indRollActive : style.indRoll} />
|
||||
<div className={isRolling ? style.indRollActive : style.indRoll}/>
|
||||
</Tooltip>
|
||||
<div
|
||||
className={isNegative ? style.indNegativeActive : style.indNegative}
|
||||
/>
|
||||
<div className={style.indDelay} />
|
||||
<div className={style.indDelay}/>
|
||||
</div>
|
||||
<div className={style.timer}>
|
||||
<Countdown
|
||||
@@ -51,7 +51,7 @@ const PlaybackTimer = (props) => {
|
||||
/>
|
||||
</div>
|
||||
{isWaiting ? (
|
||||
<div className={style.start}>
|
||||
<div className={style.roll}>
|
||||
<span className={style.rolltag}>Roll: Countdown to start</span>
|
||||
<span className={style.time}>{''}</span>
|
||||
</div>
|
||||
|
||||
@@ -10,13 +10,9 @@ const withSocket = (Component) => {
|
||||
const WrappedComponent = (props) => {
|
||||
const {
|
||||
data: eventsData,
|
||||
status: eventsDataStatus,
|
||||
isError: eventsDataIsError,
|
||||
} = useFetch(EVENTS_TABLE, fetchAllEvents);
|
||||
const {
|
||||
data: genData,
|
||||
status: genDataStatus,
|
||||
isError: genDataIsError,
|
||||
} = useFetch(EVENT_TABLE, fetchEvent);
|
||||
|
||||
const [publicEvents, setPublicEvents] = useState([]);
|
||||
@@ -58,6 +54,7 @@ const withSocket = (Component) => {
|
||||
presenterNext: '',
|
||||
});
|
||||
const [selectedId, setSelectedId] = useState(null);
|
||||
const [nextId, setNextId] = useState(null);
|
||||
const [publicSelectedId, setPublicSelectedId] = useState(null);
|
||||
const [general, setGeneral] = useState({
|
||||
title: '',
|
||||
@@ -67,6 +64,7 @@ const withSocket = (Component) => {
|
||||
endMessage: '',
|
||||
});
|
||||
const [playback, setPlayback] = useState(null);
|
||||
const [onAir, setOnAir] = useState(false);
|
||||
|
||||
// Ask for update on load
|
||||
useEffect(() => {
|
||||
@@ -96,6 +94,9 @@ const withSocket = (Component) => {
|
||||
socket.on('playstate', (data) => {
|
||||
setPlayback(data);
|
||||
});
|
||||
socket.on('onAir', (data) => {
|
||||
setOnAir(data);
|
||||
});
|
||||
|
||||
// Handle titles
|
||||
socket.on('titles', (data) => {
|
||||
@@ -112,6 +113,9 @@ const withSocket = (Component) => {
|
||||
socket.on('publicselected-id', (data) => {
|
||||
setPublicSelectedId(data);
|
||||
});
|
||||
socket.on('next-id', (data) => {
|
||||
setNextId(data);
|
||||
});
|
||||
|
||||
// Ask for up to date data
|
||||
socket.emit('get-messages');
|
||||
@@ -124,6 +128,7 @@ const withSocket = (Component) => {
|
||||
|
||||
// ask for playstate
|
||||
socket.emit('get-playstate');
|
||||
socket.emit('get-onAir')
|
||||
|
||||
// Ask for up titles
|
||||
socket.emit('get-titles');
|
||||
@@ -131,6 +136,7 @@ const withSocket = (Component) => {
|
||||
|
||||
// Ask for up selected
|
||||
socket.emit('get-selected-id');
|
||||
socket.emit('get-next-id');
|
||||
|
||||
// Clear listeners
|
||||
return () => {
|
||||
@@ -139,9 +145,11 @@ const withSocket = (Component) => {
|
||||
socket.off('messages-lower');
|
||||
socket.off('timer');
|
||||
socket.off('playstate');
|
||||
socket.off('onAir');
|
||||
socket.off('titles');
|
||||
socket.off('publictitles');
|
||||
socket.off('selected-id');
|
||||
socket.emit('next-id');
|
||||
};
|
||||
}, [socket]);
|
||||
|
||||
@@ -224,6 +232,7 @@ const withSocket = (Component) => {
|
||||
...timer,
|
||||
finished: playback === 'start' && timer.running <= 0 && timer.startedAt,
|
||||
clock: stringFromMillis(timer.clock),
|
||||
clockNoSeconds: stringFromMillis(timer.clock, false),
|
||||
playstate: playback,
|
||||
};
|
||||
|
||||
@@ -240,7 +249,9 @@ const withSocket = (Component) => {
|
||||
backstageEvents={backstageEvents}
|
||||
selectedId={selectedId}
|
||||
publicSelectedId={publicSelectedId}
|
||||
nextId={nextId}
|
||||
general={general}
|
||||
onAir={onAir}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -6,6 +6,7 @@ import NavLogo from 'common/components/nav/NavLogo';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { AnimatePresence, motion } from 'framer-motion';
|
||||
import TitleSide from 'common/components/views/TitleSide';
|
||||
import {getEventsWithDelay} from "../../../common/utils/eventsManager";
|
||||
|
||||
export default function StageManager(props) {
|
||||
const { publ, title, time, backstageEvents, selectedId, general } = props;
|
||||
@@ -20,23 +21,9 @@ export default function StageManager(props) {
|
||||
useEffect(() => {
|
||||
if (backstageEvents == null) return;
|
||||
|
||||
let events = [...backstageEvents];
|
||||
setFilteredEvents(getEventsWithDelay(backstageEvents));
|
||||
|
||||
// Add running delay
|
||||
let delay = 0;
|
||||
for (const e of events) {
|
||||
if (e.type === 'block') delay = 0;
|
||||
else if (e.type === 'delay') delay = delay + e.duration;
|
||||
else if (e.type === 'event' && delay > 0) {
|
||||
e.timeStart += delay;
|
||||
e.timeEnd += delay;
|
||||
}
|
||||
}
|
||||
|
||||
// filter just events
|
||||
let filtered = events.filter((e) => e.type === 'event');
|
||||
|
||||
setFilteredEvents(filtered);
|
||||
console.log('hhh', getEventsWithDelay(backstageEvents))
|
||||
}, [backstageEvents]);
|
||||
|
||||
// Format messages
|
||||
|
||||
@@ -11,7 +11,7 @@ export default function PresenterView(props) {
|
||||
|
||||
// Set window title
|
||||
useEffect(() => {
|
||||
document.title = 'ontime - Speaker Screen';
|
||||
document.title = 'ontime - Presenter Screen';
|
||||
}, []);
|
||||
|
||||
const showOverlay = pres.text !== '' && pres.visible;
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { AnimatePresence, motion } from 'framer-motion';
|
||||
import { useEffect, useState } from 'react';
|
||||
import style from './LowerClean.module.css';
|
||||
import NavLogo from "../../../../common/components/nav/NavLogo";
|
||||
|
||||
export default function LowerClean(props) {
|
||||
const { lower, title, options } = props;
|
||||
@@ -86,6 +87,9 @@ export default function LowerClean(props) {
|
||||
fontSize: `${sizeMultiplier}vh`,
|
||||
}}
|
||||
>
|
||||
|
||||
<NavLogo isHidden />
|
||||
|
||||
<AnimatePresence>
|
||||
{showLower && (
|
||||
<motion.div
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { AnimatePresence, motion } from 'framer-motion';
|
||||
import { useEffect, useState } from 'react';
|
||||
import style from './LowerLines.module.css';
|
||||
import NavLogo from "../../../../common/components/nav/NavLogo";
|
||||
|
||||
export default function LowerLines(props) {
|
||||
const { lower, title, options } = props;
|
||||
@@ -126,6 +127,9 @@ export default function LowerLines(props) {
|
||||
fontSize: `${sizeMultiplier}vh`,
|
||||
}}
|
||||
>
|
||||
|
||||
<NavLogo isHidden />
|
||||
|
||||
<AnimatePresence>
|
||||
{showLower && (
|
||||
<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 [, , secondsNow] = time.clock.split(':');
|
||||
const [schedule, setSchedule] = useState([]);
|
||||
|
||||
const activeIndicators = [...Array(12).keys()];
|
||||
const secondsIndicators = [...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}>
|
||||
{activeIndicators.map(i => (
|
||||
<div
|
||||
key={i}
|
||||
className={style.hours__active}
|
||||
style={{
|
||||
transform: `rotate(${360 / 12 * i - 90}deg) translateX(380px)`
|
||||
}}/>
|
||||
)
|
||||
)}
|
||||
{secondsIndicators.map(i => (
|
||||
<div
|
||||
key={i}
|
||||
className={i <= secondsNow ? 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 : ''}>
|
||||
{`${s.time} ${s.title}`}
|
||||
</li>
|
||||
))
|
||||
}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,167 @@
|
||||
@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: #000;
|
||||
|
||||
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: #300000;
|
||||
$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: 300px;
|
||||
line-height: 0.8em;
|
||||
}
|
||||
.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: 170px;
|
||||
line-height: 0.9em;
|
||||
}
|
||||
.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: $red-active;
|
||||
}
|
||||
.next {
|
||||
color: $cyan-active;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@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"
|
||||
picomatch "^2.0.4"
|
||||
|
||||
anymatch@~3.1.2:
|
||||
version "3.1.2"
|
||||
resolved "https://registry.yarnpkg.com/anymatch/-/anymatch-3.1.2.tgz#c0557c096af32f106198f4f4e2a383537e378716"
|
||||
integrity sha512-P43ePfOAIupkguHUycrc4qJ9kz8ZiuOUijaETwX7THt0Y/GNK7v0aa8rY816xWjZ7rJdA5XdMcpVFTKMq+RvWg==
|
||||
dependencies:
|
||||
normalize-path "^3.0.0"
|
||||
picomatch "^2.0.4"
|
||||
|
||||
aproba@^1.1.1:
|
||||
version "1.2.0"
|
||||
resolved "https://registry.yarnpkg.com/aproba/-/aproba-1.2.0.tgz#6802e6264efd18c790a1b0d517f0f2627bf2c94a"
|
||||
@@ -3990,6 +3998,21 @@ check-types@^11.1.1:
|
||||
resolved "https://registry.yarnpkg.com/check-types/-/check-types-11.1.2.tgz#86a7c12bf5539f6324eb0e70ca8896c0e38f3e2f"
|
||||
integrity sha512-tzWzvgePgLORb9/3a0YenggReLKAIb2owL03H2Xdoe5pKcUyWRSEQ8xfCar8t2SIAuEDwtmx2da1YB52YuHQMQ==
|
||||
|
||||
"chokidar@>=3.0.0 <4.0.0":
|
||||
version "3.5.2"
|
||||
resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-3.5.2.tgz#dba3976fcadb016f66fd365021d91600d01c1e75"
|
||||
integrity sha512-ekGhOnNVPgT77r4K/U3GDhu+FQ2S8TnK/s2KbIGXi0SZWuwkZ2QNyfWdZW+TVfn84DpEP7rLeCt2UI6bJ8GwbQ==
|
||||
dependencies:
|
||||
anymatch "~3.1.2"
|
||||
braces "~3.0.2"
|
||||
glob-parent "~5.1.2"
|
||||
is-binary-path "~2.1.0"
|
||||
is-glob "~4.0.1"
|
||||
normalize-path "~3.0.0"
|
||||
readdirp "~3.6.0"
|
||||
optionalDependencies:
|
||||
fsevents "~2.3.2"
|
||||
|
||||
chokidar@^2.1.8:
|
||||
version "2.1.8"
|
||||
resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-2.1.8.tgz#804b3a7b6a99358c3c5c61e71d8728f041cff917"
|
||||
@@ -6054,7 +6077,7 @@ fsevents@^1.2.7:
|
||||
bindings "^1.5.0"
|
||||
nan "^2.12.1"
|
||||
|
||||
fsevents@^2.1.2, fsevents@^2.1.3, fsevents@~2.3.1:
|
||||
fsevents@^2.1.2, fsevents@^2.1.3, fsevents@~2.3.1, fsevents@~2.3.2:
|
||||
version "2.3.2"
|
||||
resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.3.2.tgz#8a526f78b8fdf4623b709e0b975c52c24c02fd1a"
|
||||
integrity sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==
|
||||
@@ -6137,7 +6160,7 @@ glob-parent@^3.1.0:
|
||||
is-glob "^3.1.0"
|
||||
path-dirname "^1.0.0"
|
||||
|
||||
glob-parent@^5.0.0, glob-parent@^5.1.0, glob-parent@~5.1.0:
|
||||
glob-parent@^5.0.0, glob-parent@^5.1.0, glob-parent@~5.1.0, glob-parent@~5.1.2:
|
||||
version "5.1.2"
|
||||
resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-5.1.2.tgz#869832c58034fe68a4093c17dc15e8340d8401c4"
|
||||
integrity sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==
|
||||
@@ -6608,6 +6631,11 @@ immer@8.0.1:
|
||||
resolved "https://registry.yarnpkg.com/immer/-/immer-8.0.1.tgz#9c73db683e2b3975c424fb0572af5889877ae656"
|
||||
integrity sha512-aqXhGP7//Gui2+UrEtvxZxSquQVXTpZ7KDxfCcKAF3Vysvw0CViVaW9RZ1j1xlIYqaaaipBoqdqeibkc18PNvA==
|
||||
|
||||
immutable@^4.0.0:
|
||||
version "4.0.0"
|
||||
resolved "https://registry.yarnpkg.com/immutable/-/immutable-4.0.0.tgz#b86f78de6adef3608395efb269a91462797e2c23"
|
||||
integrity sha512-zIE9hX70qew5qTUjSS7wi1iwj/l7+m54KWU247nhM3v806UdGj1yDndXj+IOYxxtW9zyLI+xqFNZjTuDaLUqFw==
|
||||
|
||||
import-cwd@^2.0.0:
|
||||
version "2.1.0"
|
||||
resolved "https://registry.yarnpkg.com/import-cwd/-/import-cwd-2.1.0.tgz#aa6cf36e722761285cb371ec6519f53e2435b0a9"
|
||||
@@ -10366,6 +10394,13 @@ readdirp@~3.5.0:
|
||||
dependencies:
|
||||
picomatch "^2.2.1"
|
||||
|
||||
readdirp@~3.6.0:
|
||||
version "3.6.0"
|
||||
resolved "https://registry.yarnpkg.com/readdirp/-/readdirp-3.6.0.tgz#74a370bd857116e245b29cc97340cd431a02a6c7"
|
||||
integrity sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==
|
||||
dependencies:
|
||||
picomatch "^2.2.1"
|
||||
|
||||
recursive-readdir@2.2.2:
|
||||
version "2.2.2"
|
||||
resolved "https://registry.yarnpkg.com/recursive-readdir/-/recursive-readdir-2.2.2.tgz#9946fb3274e1628de6e36b2f6714953b4845094f"
|
||||
@@ -10565,6 +10600,11 @@ requires-port@^1.0.0:
|
||||
resolved "https://registry.yarnpkg.com/requires-port/-/requires-port-1.0.0.tgz#925d2601d39ac485e091cf0da5c6e694dc3dcaff"
|
||||
integrity sha1-kl0mAdOaxIXgkc8NpcbmlNw9yv8=
|
||||
|
||||
resize-observer-polyfill@^1.5.1:
|
||||
version "1.5.1"
|
||||
resolved "https://registry.yarnpkg.com/resize-observer-polyfill/-/resize-observer-polyfill-1.5.1.tgz#0e9020dd3d21024458d4ebd27e23e40269810464"
|
||||
integrity sha512-LwZrotdHOo12nQuZlHEmtuXdqGoOD0OhaxopaNFxWzInpEgaLWoVuAMbTzixuosCx2nEG58ngzW3vxdWoxIgdg==
|
||||
|
||||
resolve-cwd@^2.0.0:
|
||||
version "2.0.0"
|
||||
resolved "https://registry.yarnpkg.com/resolve-cwd/-/resolve-cwd-2.0.0.tgz#00a9f7387556e27038eae232caa372a6a59b665a"
|
||||
@@ -10811,6 +10851,14 @@ sass-loader@^10.0.5:
|
||||
schema-utils "^3.0.0"
|
||||
semver "^7.3.2"
|
||||
|
||||
sass@^1.44.0:
|
||||
version "1.44.0"
|
||||
resolved "https://registry.yarnpkg.com/sass/-/sass-1.44.0.tgz#619aa0a2275c097f9af5e6b8fe8a95e3056430fb"
|
||||
integrity sha512-0hLREbHFXGQqls/K8X+koeP+ogFRPF4ZqetVB19b7Cst9Er8cOR0rc6RU7MaI4W1JmUShd1BPgPoeqmmgMMYFw==
|
||||
dependencies:
|
||||
chokidar ">=3.0.0 <4.0.0"
|
||||
immutable "^4.0.0"
|
||||
|
||||
sax@~1.2.4:
|
||||
version "1.2.4"
|
||||
resolved "https://registry.yarnpkg.com/sax/-/sax-1.2.4.tgz#2816234e2378bddc4e5354fab5caa895df7100d9"
|
||||
@@ -12131,6 +12179,13 @@ use-callback-ref@^1.2.3, use-callback-ref@^1.2.5:
|
||||
resolved "https://registry.yarnpkg.com/use-callback-ref/-/use-callback-ref-1.2.5.tgz#6115ed242cfbaed5915499c0a9842ca2912f38a5"
|
||||
integrity sha512-gN3vgMISAgacF7sqsLPByqoePooY3n2emTH59Ur5d/M8eg4WTWu1xp8i8DHjohftIyEx0S08RiYxbffr4j8Peg==
|
||||
|
||||
use-fit-text@^2.4.0:
|
||||
version "2.4.0"
|
||||
resolved "https://registry.yarnpkg.com/use-fit-text/-/use-fit-text-2.4.0.tgz#d3d1cd72f6d29cfb2233ec0e0b38c6f25d319f67"
|
||||
integrity sha512-Iy4LMrXcdxWlyZ5phntMpJMgyXGB1p3tV73y2r0QrZ6f/thPh+/QU3ie6RCXmjF8tHMs20FKMPskXeDYIla/Ww==
|
||||
dependencies:
|
||||
resize-observer-polyfill "^1.5.1"
|
||||
|
||||
use-memo-one@^1.1.1:
|
||||
version "1.1.2"
|
||||
resolved "https://registry.yarnpkg.com/use-memo-one/-/use-memo-one-1.1.2.tgz#0c8203a329f76e040047a35a1197defe342fab20"
|
||||
|
||||
+2
-2
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "ontime",
|
||||
"version": "0.4.1",
|
||||
"version": "0.4.3",
|
||||
"author": "Carlos Valente",
|
||||
"description": "Time keeping for live events",
|
||||
"repository": "https://github.com/cpvalente/ontime",
|
||||
@@ -27,7 +27,7 @@
|
||||
"nodestart": "NODE_ENV=development node src/app.js",
|
||||
"setdb": "cp data/db.json src/data/db.json",
|
||||
"clean": "rm -rf ../client/build/ && rm -rf ../client/node_modules && rm -rf src/node_modules && rm -rf ./node_modules && rm -rf ./dist",
|
||||
"prep": "yarn clean && yarn prep",
|
||||
"prep": "yarn clean && yarn setdb",
|
||||
"test": "node --experimental-vm-modules node_modules/jest/bin/jest.js",
|
||||
"start": "NODE_ENV=development electron .",
|
||||
"pack": "electron-builder --dir",
|
||||
|
||||
+4
-2
@@ -57,6 +57,7 @@ await db.write();
|
||||
import { router as eventsRouter } from './routes/eventsRouter.js';
|
||||
import { router as eventRouter } from './routes/eventRouter.js';
|
||||
import { router as ontimeRouter } from './routes/ontimeRouter.js';
|
||||
import { router as playbackRouter } from './routes/playbackRouter.js';
|
||||
|
||||
// Global Objects
|
||||
import { EventTimer } from './classes/EventTimer.js';
|
||||
@@ -81,11 +82,12 @@ app.use('/uploads', express.static('uploads'));
|
||||
app.use('/events', eventsRouter);
|
||||
app.use('/event', eventRouter);
|
||||
app.use('/ontime', ontimeRouter);
|
||||
app.use('/playback', playbackRouter);
|
||||
|
||||
// serve react
|
||||
app.use(
|
||||
express.static(
|
||||
path.join(__dirname, env == 'prod' ? '../' : '../../', 'client/build')
|
||||
path.join(__dirname, env === 'prod' ? '../' : '../../', 'client/build')
|
||||
)
|
||||
);
|
||||
|
||||
@@ -93,7 +95,7 @@ app.get('*', (req, res) => {
|
||||
res.sendFile(
|
||||
path.resolve(
|
||||
__dirname,
|
||||
env == 'prod' ? '../' : '../../',
|
||||
env === 'prod' ? '../' : '../../',
|
||||
'client',
|
||||
'build',
|
||||
'index.html'
|
||||
|
||||
@@ -63,6 +63,7 @@ export class EventTimer extends Timer {
|
||||
nextPublicEventId = null;
|
||||
numEvents = null;
|
||||
_eventlist = null;
|
||||
onAir = false;
|
||||
|
||||
constructor(httpServer, oscClient, config) {
|
||||
// call super constructor
|
||||
@@ -199,7 +200,7 @@ export class EventTimer extends Timer {
|
||||
// send current timer
|
||||
broadcastTimer() {
|
||||
// through websockets
|
||||
this.io.emit('timer', this.getObject());
|
||||
this.io.emit('timer', this.getTimes());
|
||||
|
||||
// through OSC, only if running
|
||||
if (this.state === 'start' || this.state === 'roll') {
|
||||
@@ -211,7 +212,7 @@ export class EventTimer extends Timer {
|
||||
|
||||
// broadcast state
|
||||
broadcastState(update = true) {
|
||||
this.io.emit('timer', this.getObject(update));
|
||||
this.io.emit('timer', this.getTimes(update));
|
||||
this.io.emit('playstate', this.state);
|
||||
this.io.emit('selected', {
|
||||
id: this.selectedEventId,
|
||||
@@ -224,6 +225,7 @@ export class EventTimer extends Timer {
|
||||
this.io.emit('publicnext-id', this.nextPublicEventId);
|
||||
this.io.emit('titles', this.titles);
|
||||
this.io.emit('publictitles', this.titlesPublic);
|
||||
this.io.emit('onAir', this.onAir);
|
||||
}
|
||||
|
||||
// broadcast message
|
||||
@@ -232,10 +234,22 @@ export class EventTimer extends Timer {
|
||||
}
|
||||
|
||||
update() {
|
||||
// if there is nothing selected, do nothing
|
||||
if (this.selectedEventId == null && this.state !== 'roll') return;
|
||||
const now = this._getCurrentTime();
|
||||
|
||||
// if there is nothing selected, update only clock
|
||||
if (this.selectedEventId == null && this.state !== 'roll') {
|
||||
this.clock = now;
|
||||
this.broadcastThis('timer', {
|
||||
clock: now,
|
||||
running: Timer.toSeconds(this.current),
|
||||
secondary: Timer.toSeconds(this.secondaryTimer),
|
||||
durationSeconds: Timer.toSeconds(this.duration),
|
||||
expectedFinish: this._getExpectedFinish(),
|
||||
startedAt: this._startedAt,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// only implement roll here
|
||||
if (this.state !== 'roll') {
|
||||
super.update();
|
||||
@@ -359,7 +373,7 @@ export class EventTimer extends Timer {
|
||||
);
|
||||
|
||||
// send state
|
||||
socket.emit('timer', this.getObject());
|
||||
socket.emit('timer', this.getTimes());
|
||||
socket.emit('playstate', this.state);
|
||||
socket.emit('selected-id', this.selectedEventId);
|
||||
socket.emit('next-id', this.nextEventId);
|
||||
@@ -385,7 +399,7 @@ export class EventTimer extends Timer {
|
||||
/*******************************************/
|
||||
// general playback state
|
||||
socket.on('get-state', () => {
|
||||
socket.emit('timer', this.getObject());
|
||||
socket.emit('timer', this.getTimes());
|
||||
socket.emit('playstate', this.state);
|
||||
socket.emit('selected-id', this.selectedEventId);
|
||||
socket.emit('next-id', this.nextEventId);
|
||||
@@ -400,7 +414,7 @@ export class EventTimer extends Timer {
|
||||
});
|
||||
|
||||
socket.on('get-timer', () => {
|
||||
socket.emit('timer', this.getObject());
|
||||
socket.emit('timer', this.getTimes());
|
||||
});
|
||||
|
||||
socket.on('increment-timer', (data) => {
|
||||
@@ -419,6 +433,15 @@ export class EventTimer extends Timer {
|
||||
socket.emit('playstate', this.state);
|
||||
});
|
||||
|
||||
socket.on('set-onAir', (data) => {
|
||||
this.onAir = data;
|
||||
this.broadcastThis('onAir', this.onAir);
|
||||
});
|
||||
|
||||
socket.on('get-onAir', () => {
|
||||
socket.emit('onAir', this.onAir);
|
||||
});
|
||||
|
||||
/*******************************************/
|
||||
// selection data
|
||||
socket.on('get-selected', () => {
|
||||
@@ -949,6 +972,16 @@ export class EventTimer extends Timer {
|
||||
`;
|
||||
}
|
||||
|
||||
/**
|
||||
* @description Set onAir property of timer
|
||||
* @param {boolean} onAir - whether flag is active
|
||||
*/
|
||||
setonAir(onAir) {
|
||||
this.onAir = onAir;
|
||||
// broadcast change
|
||||
this.broadcastThis('onAir', onAir);
|
||||
}
|
||||
|
||||
start() {
|
||||
// if there is nothing selected, no nothing
|
||||
if (this.selectedEventId == null) return;
|
||||
|
||||
@@ -139,8 +139,8 @@ export class Timer {
|
||||
return this.duration - this.current;
|
||||
}
|
||||
|
||||
// getObject
|
||||
getObject(update = true) {
|
||||
// get time object
|
||||
getTimes(update = true) {
|
||||
// update timer
|
||||
if (update) this.update();
|
||||
|
||||
|
||||
@@ -38,6 +38,12 @@ export const initiateOSC = (config) => {
|
||||
|
||||
// get second part (command)
|
||||
switch (path.toLowerCase()) {
|
||||
case 'onair':
|
||||
global.timer.setonAir(true);
|
||||
break;
|
||||
case 'offair':
|
||||
global.timer.setonAir(false);
|
||||
break;
|
||||
case 'start':
|
||||
case 'play':
|
||||
console.log('calling play');
|
||||
|
||||
@@ -4,6 +4,22 @@ export const pbGet = async (req, res) => {
|
||||
res.send(global.timer.playState);
|
||||
};
|
||||
|
||||
// Create controller for GET request to '/playback/onAir'
|
||||
// Turns onAir flag to true
|
||||
export const onAir = async (req, res) => {
|
||||
console.log('Setting onAir to true');
|
||||
global.timer.setonAir(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.setonAir(false);
|
||||
res.sendStatus(200);
|
||||
};
|
||||
|
||||
// Create controller for GET request to '/playback/start'
|
||||
// Starts timer object
|
||||
export const pbStart = async (req, res) => {
|
||||
|
||||
@@ -1,32 +1,51 @@
|
||||
import express from 'express';
|
||||
export const router = express.Router();
|
||||
|
||||
// import event controller
|
||||
const playbackController = require('../controllers/playbackController');
|
||||
// import playback controllers
|
||||
import {
|
||||
pbGet,
|
||||
onAir,
|
||||
offAir,
|
||||
pbStart,
|
||||
pbPause,
|
||||
pbStop,
|
||||
pbRoll,
|
||||
pbPrevious,
|
||||
pbNext,
|
||||
pbUnload,
|
||||
pbReload
|
||||
} from '../controllers/playbackController.js';
|
||||
|
||||
// create route between controller and '/playback/' endpoint
|
||||
router.get('/', playbackController.pbGet);
|
||||
router.get('/', pbGet);
|
||||
|
||||
// create route between controller and '/playback/onAir' endpoint
|
||||
router.get('/onAir', onAir);
|
||||
|
||||
// create route between controller and '/playback/offAir' endpoint
|
||||
router.get('/offAir', offAir);
|
||||
|
||||
// create route between controller and '/playback/start' endpoint
|
||||
router.get('/start', playbackController.pbStart);
|
||||
router.get('/start', pbStart);
|
||||
router.get('/play', pbStart);
|
||||
|
||||
// create route between controller and '/playback/pause' endpoint
|
||||
router.get('/pause', playbackController.pbPause);
|
||||
router.get('/pause', pbPause);
|
||||
|
||||
// create route between controller and '/playback/stop' endpoint
|
||||
router.get('/stop', playbackController.pbStop);
|
||||
router.get('/stop', pbStop);
|
||||
|
||||
// create route between controller and '/playback/roll' endpoint
|
||||
router.get('/roll', playbackController.pbRoll);
|
||||
router.get('/roll', pbRoll);
|
||||
|
||||
// create route between controller and '/playback/previous' endpoint
|
||||
router.get('/previous', playbackController.pbPrevious);
|
||||
router.get('/previous', pbPrevious);
|
||||
|
||||
// create route between controller and '/playback/next' endpoint
|
||||
router.get('/next', playbackController.pbNext);
|
||||
router.get('/next', pbNext);
|
||||
|
||||
// create route between controller and '/playback/unload' endpoint
|
||||
router.get('/unload', playbackController.pbUnload);
|
||||
router.get('/unload', pbUnload);
|
||||
|
||||
// create route between controller and '/playback/reload' endpoint
|
||||
router.get('/reload', playbackController.pbReload);
|
||||
router.get('/reload', pbReload);
|
||||
|
||||
Reference in New Issue
Block a user