Feat/studio clock (#60)

This commit is contained in:
Carlos Valente
2021-12-13 21:29:56 +01:00
committed by GitHub
parent 627cce7ba6
commit 80c2e9a8ba
26 changed files with 1099 additions and 135 deletions
+12 -7
View File
@@ -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.
+21 -6
View File
@@ -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,
}
@@ -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);
});
})
+2 -4
View File
@@ -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
*/
+84
View File
@@ -0,0 +1,84 @@
/**
* @description From a list of events, returns only events of type event with calculated delays
* @param {Object[]} events - given events
* @returns {Object[]} Filtered events with calculated delays
*/
import {stringFromMillis} from "./dateConfig";
export const getEventsWithDelay = (events) => {
if (events == null) return [];
const unfilteredEvents = [...events];
// Add running delay
let delay = 0;
for (const e of unfilteredEvents) {
if (e.type === 'block') delay = 0;
else if (e.type === 'delay') delay = delay + e.duration;
else if (e.type === 'event' && delay > 0) {
e.timeStart += delay;
e.timeEnd += delay;
}
}
// filter just events
return unfilteredEvents.filter((e) => e.type === 'event');
};
/**
* @description Returns trimmed event list array
* @param {Object[]} events - given events
* @param {string} selectedId - id of currently selected event
* @param {number} limit - max number of events to return
* @returns {Object[]} Event list with maximum <limit> objects
*/
export const trimEventlist = (events, selectedId, limit) => {
if (events == null) return [];
const BEFORE = 2;
const trimmedEvents = [...events];
// limit events length if necessary
if (limit != null) {
while (trimmedEvents.length > limit) {
const idx = trimmedEvents.findIndex((e) => e.id === selectedId);
if (idx <= BEFORE) { trimmedEvents.pop(); }
else { trimmedEvents.shift(); }
}
}
return trimmedEvents;
};
/**
* @description Returns list of events formatted to be displayed
* @param {Object[]} events - given events
* @param {string} selectedId - id of currently selected event
* @param {string} nextId - id of next event
* @param {boolean} [showEnd] - whether to show the end time
* @returns {Object[]} Formatted list of events [{time: -, title: -, isNow, isNext}]
*/
export const formatEventList = (events, selectedId, nextId, showEnd = false) => {
if (events == null) return [];
const givenEvents = [...events];
// format list
let formattedEvents = [];
for (const g of givenEvents) {
const start = stringFromMillis(g.timeStart, false);
const end = stringFromMillis(g.timeEnd, false);
formattedEvents.push({
id: g.id,
time: showEnd ? `${start} - ${end}` : start,
title: g.title,
isNow: g.id === selectedId,
isNext: g.id === nextId,
});
}
return formattedEvents;
};
+63 -37
View File
@@ -1,15 +1,16 @@
import { Editable, EditableInput, EditablePreview } from '@chakra-ui/editable';
import { 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;
}
}
+15 -4
View File
@@ -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 [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;
}
}