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
+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;
}
}