wip: operator data and structure

This commit is contained in:
cv
2023-07-24 22:26:35 +02:00
parent 75b69055a0
commit c49c8fbd27
11 changed files with 218 additions and 89 deletions
@@ -201,3 +201,31 @@ export const STUDIO_CLOCK_OPTIONS: ParamField[] = [
type: 'boolean', type: 'boolean',
}, },
]; ];
export const OPERATOR_OPTIONS: ParamField[] = [
TIME_FORMAT_OPTION,
{
id: 'hidepast',
title: 'Hide Past Events',
description: 'Whether to events that have passed',
type: 'boolean',
},
{
id: 'subscribe',
title: 'Highlight Field',
description: 'Choose a field to highlight',
type: 'option',
values: {
user0: 'user0',
user1: 'user1',
user2: 'user2',
user3: 'user3',
user4: 'user4',
user5: 'user5',
user6: 'user6',
user7: 'user7',
user8: 'user8',
user9: 'user9',
},
},
];
@@ -4,6 +4,14 @@ import { deepCompare, useRuntimeStore } from '../stores/runtime';
import { socketSendJson } from '../utils/socket'; import { socketSendJson } from '../utils/socket';
export const useRundownEditor = () => { export const useRundownEditor = () => {
const featureSelector = (state: RuntimeStore) => ({
selectedEventId: state.loaded.selectedEventId,
});
return useRuntimeStore(featureSelector, deepCompare);
};
export const useOperator = () => {
const featureSelector = (state: RuntimeStore) => ({ const featureSelector = (state: RuntimeStore) => ({
playback: state.playback, playback: state.playback,
selectedEventId: state.loaded.selectedEventId, selectedEventId: state.loaded.selectedEventId,
@@ -1,12 +1,9 @@
// remember we are targeting phones and tablets
@use '../../../src/theme/v2Styles' as *; @use '../../../src/theme/v2Styles' as *;
@use '../../../src/theme/ontimeColours' as *; @use '../../../src/theme/ontimeColours' as *;
.operatorContainer { .operatorContainer {
width: 100vw; width: 100vw;
height: 100vh; height: 100vh;
padding: 2rem;
font-size: 0.8rem; font-size: 0.8rem;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
@@ -20,29 +17,14 @@
} }
@mixin event-block() { @mixin event-block() {
padding: 0.2rem; height: 40px;
padding: 0.5rem;
display: flex; display: flex;
gap: 0.75rem; gap: 0.75rem;
align-items: center; align-items: center;
margin: 0.1rem; margin: 0.1rem;
} }
.scheduledEvent {
@include event-block();
background-color: $gray-1300;
align-items: start;
}
.activeEvent {
border-top: 1.5px white;
border-style: solid;
}
.runningTimer {
border-top: 0.5px white;
border-style: solid;
background-color: $green-700;
}
.block { .block {
@include event-block(); @include event-block();
+32 -24
View File
@@ -1,25 +1,30 @@
import React from 'react'; import { UIEvent, useState } from 'react';
import { SupportedEvent } from 'ontime-types'; import { useSearchParams } from 'react-router-dom';
import { SupportedEvent, UserFields } from 'ontime-types';
import NavigationMenu from '../.././common/components/navigation-menu/NavigationMenu'; import NavigationMenu from '../../common/components/navigation-menu/NavigationMenu';
import { OPERATOR_OPTIONS } from '../../common/components/view-params-editor/constants';
import ViewParamsEditor from '../../common/components/view-params-editor/ViewParamsEditor';
import { useOperator } from '../../common/hooks/useSocket';
import useRundown from '../../common/hooks-query/useRundown'; import useRundown from '../../common/hooks-query/useRundown';
import FocusBlock from './focus-block/focus-block'; import FocusBlock from './focus-block/focus-block';
import OpBlock from './op-block/OpBlock'; import OpBlock from './op-block/OpBlock';
import OpEvent from './op-event/OpEvent'; import OpEvent from './op-event/OpEvent';
import TimeBlock from './time-block/TimeBlock'; import TimeBlock from './time-block/TimeBlock';
import { getRundownEnd } from './operator.utils';
import style from './Operator.module.scss'; import style from './Operator.module.scss';
export default function Operator() { export default function Operator() {
// this is the data that you need, the status flag should give you possibility to create a loading state
// for debugging data use the react query dev tools (flower thing in the bottom left corner)
const { data, status } = useRundown(); const { data, status } = useRundown();
const [showChild, setShowChild] = React.useState(false); const featureData = useOperator();
const [showChild, setShowChild] = useState(false);
const [searchParams] = useSearchParams();
const handleScroll = (event: any) => { const handleScroll = (event: UIEvent<HTMLElement>) => {
const scrollThreshold = 50; // Set the scroll threshold here const scrollThreshold = 50;
const scrollPosition = event.target.scrollTop; const scrollPosition = (event.target as HTMLElement).scrollTop;
setShowChild(scrollPosition > scrollThreshold); setShowChild(scrollPosition > scrollThreshold);
}; };
@@ -28,32 +33,35 @@ export default function Operator() {
return <>loading</>; return <>loading</>;
} }
const subscribe = searchParams.get('subscribe') as keyof UserFields | null;
const lastEvent = getRundownEnd(data);
let eventIndex = 0;
return ( return (
<div className={style.operatorContainer}> <div className={style.operatorContainer}>
<NavigationMenu /> <NavigationMenu />
<ViewParamsEditor paramFields={OPERATOR_OPTIONS} />
<div className={style.operatorEvents} onScroll={handleScroll}> <div className={style.operatorEvents} onScroll={handleScroll}>
{data.map((entry, i) => { {data.map((entry) => {
// there are three types of events, you a filter them by using the type property
// for this view, we do not show the delay event
// this is a scheduled event
if (entry.type === SupportedEvent.Event) { if (entry.type === SupportedEvent.Event) {
eventIndex += 1;
return ( return (
<div <OpEvent
key={entry.id} key={entry.id}
className={`${style.scheduledEvent} ${i % 3 == 0 ? style.activeEvent : ''} ${ index={eventIndex}
i == 4 ? style.runningTimer : '' data={entry}
}`} isSelected={featureData.selectedEventId === entry.id}
> subscribed={subscribe || undefined}
<OpEvent id={i} data={entry} /> />
</div>
); );
} }
// this is a block entry (like a section title)
if (entry.type === SupportedEvent.Block) { if (entry.type === SupportedEvent.Block) {
return ( return (
// @arihanv
// we prefer moving these wrapper divs to the block
// so that the styles and all attributes can be self-contained
<div key={entry.id} className={style.block}> <div key={entry.id} className={style.block}>
<OpBlock data={entry} /> <OpBlock data={entry} />
</div> </div>
@@ -63,7 +71,7 @@ export default function Operator() {
})} })}
{showChild && <FocusBlock />} {showChild && <FocusBlock />}
</div> </div>
<TimeBlock /> <TimeBlock playback={featureData.playback} lastEvent={lastEvent} selectedEventId={featureData.selectedEventId} />
</div> </div>
); );
} }
@@ -1,14 +1,17 @@
import { BiTargetLock } from '@react-icons/all-files/bi/BiTargetLock'; import { IoLocate } from '@react-icons/all-files/io5/IoLocate';
import style from './focusBlock.module.scss'; import style from './focusBlock.module.scss';
export default function FocusBlock() { export default function FocusBlock() {
const handleClick = () => console.log('click follow');
// @arihavn, can we find a way to have a single button here, no need for the div
return ( return (
<button className={style.focusBlock}> <div className={style.focusBlock}>
<div className={style.focusButton}> <button className={style.focusButton} onClick={handleClick}>
<BiTargetLock size={20} /> <IoLocate size={16} />
Follow Follow
</div> </button>
</button> </div>
); );
} }
@@ -1,25 +1,22 @@
@use '.../../../src/theme/ontimeColours' as *; @use '../../../../src/theme/ontimeColours' as *;
.focusBlock { .focusBlock {
bottom: 0; bottom: 0;
left: 0; left: 0;
right: 0; right: 0;
position: -webkit-sticky;
position: sticky; position: sticky;
display: flex; display: flex;
justify-content: center; justify-content: center;
width: 100%; width: 100%;
margin-top: -53px;
} }
.focusButton { .focusButton {
margin: 10px; margin: 10px;
background-color: $blue-500; background-color: $blue-500;
width: fit-content; padding: 0.25rem 1rem;
padding: 3px 15px 3px 15px; border-radius: 99px;
border-radius: 10px;
display: flex; display: flex;
align-items: center; align-items: center;
gap: 5px; gap: 0.5rem;
font-size: medium; font-size: 1rem;
} }
@@ -1,13 +1,31 @@
.alias { @use '../../../../src/theme/ontimeColours' as *;
background-color: red; @import '../Operator.module.scss';
width: fit-content;
height: fit-content; .scheduledEvent {
@include event-block();
background-color: $gray-1300;
}
.runningTimer {
background-color: $green-700;
}
.cue {
font-size: 0.75rem;
background-color: $gray-1050; // to override inline
padding: 0.2rem; padding: 0.2rem;
align-items: center; min-width: 4em; // we do not want these to resize
text-align: center;
border-radius: 2px;
&:after {
content: '\200b';
}
} }
.title { .title {
font-size: 1rem; font-size: 1rem;
letter-spacing: 0.02px;
} }
.event { .event {
@@ -21,7 +39,10 @@
} }
.fields { .fields {
display: block;
font-weight: 700; font-weight: 700;
color: $orange-500;
letter-spacing: 0.02px;
} }
.time { .time {
@@ -43,10 +64,3 @@
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
} }
.chevron {
display: flex;
align-items: center;
color: yellow;
font-size: 16px;
}
@@ -1,22 +1,38 @@
import { IoChevronUp } from '@react-icons/all-files/io5/IoChevronUp'; import { OntimeEvent, UserFields } from 'ontime-types';
import { OntimeEvent } from 'ontime-types';
import DelayIndicator from '../../../common/components/delay-indicator/DelayIndicator';
import { useTimer } from '../../../common/hooks/useSocket';
import { getAccessibleColour } from '../../../common/utils/styleUtils';
import { formatTime } from '../../../common/utils/time'; import { formatTime } from '../../../common/utils/time';
import style from './OpEvent.module.scss'; import style from './OpEvent.module.scss';
type OpEventProps = { type OpEventProps = {
data: OntimeEvent; data: OntimeEvent;
id: number; index: number;
isSelected: boolean;
subscribed?: keyof UserFields;
}; };
export default function OpEvent({ data, id }: OpEventProps) { function RollingTime() {
const timer = useTimer();
return <>{formatTime(timer.current, { showSeconds: true, format: 'hh:mm:ss' })}</>;
}
export default function OpEvent({ data, index, isSelected, subscribed }: OpEventProps) {
const start = formatTime(data.timeStart, { format: 'hh:mm' }); const start = formatTime(data.timeStart, { format: 'hh:mm' });
const end = formatTime(data.timeEnd, { format: 'hh:mm' }); const end = formatTime(data.timeEnd, { format: 'hh:mm' });
const cueColours = data.colour && getAccessibleColour(data.colour);
const subscribedData = (subscribed ? data?.[subscribed] : undefined) || '';
// @arihanv when selected, the whole row should become green
return ( return (
<> <div className={`${isSelected ? style.runningTimer : undefined}`}>
<div className={style.alias}>{data.note}</div> <div className={style.scheduledEvent}>
<div className={style.block}> <div className={style.cue} style={{ ...cueColours }}>
{index}
</div>
<div className={style.event}> <div className={style.event}>
<div className={style.title}> <div className={style.title}>
{data.title} - {data.subtitle} {data.title} - {data.subtitle}
@@ -26,15 +42,14 @@ export default function OpEvent({ data, id }: OpEventProps) {
{start} - {end} {start} - {end}
</div> </div>
<div className={style.indicator}> <div className={style.indicator}>
<div className={style.chevron}> <DelayIndicator delayValue={data.delay} />
<IoChevronUp /> {isSelected ? <RollingTime /> : formatTime(data.duration, { showSeconds: true, format: 'hh:mm:ss' })}
</div>
--:--:--
</div> </div>
</div> </div>
</div> </div>
{id % 3 == 0 && <div className={style.fields}>CAM 5 Slow pan to SL</div>}
</div> </div>
</> {/** @arihanv we likely want to animate the height of the fields div */}
<div className={style.fields}>{subscribedData}</div>
</div>
); );
} }
@@ -0,0 +1,15 @@
import { OntimeEvent, OntimeRundown, SupportedEvent } from 'ontime-types';
export function getRundownEnd(rundown: OntimeRundown): OntimeEvent | null {
if (rundown.length < 1) {
return null;
}
for (let i = rundown.length - 1; i > 0; i--) {
if (rundown[i].type === SupportedEvent.Event) {
return rundown[i] as OntimeEvent;
}
}
return null;
}
@@ -2,6 +2,15 @@
padding: 0.65rem; padding: 0.65rem;
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
border-top: 1px solid rgba(255, 255, 255, 0.1);
box-shadow: rgba(0, 0, 0, 0.35) 0 3px 6px 6px
}
.column {
display: flex;
flex-direction: column;
align-items: center;
} }
.clock { .clock {
@@ -9,3 +18,8 @@
gap: 1.5rem; gap: 1.5rem;
margin-right: 1rem; margin-right: 1rem;
} }
.label {
font-size: 0.75rem;
color: gray;
}
@@ -1,22 +1,67 @@
import { IoPlay } from '@react-icons/all-files/io5/IoPlay'; import { OntimeEvent, Playback } from 'ontime-types';
import PlaybackIcon from '../../../common/components/playback-icon/PlaybackIcon';
import { useTimer } from '../../../common/hooks/useSocket'; import { useTimer } from '../../../common/hooks/useSocket';
import { formatTime } from '../../../common/utils/time'; import { formatTime } from '../../../common/utils/time';
import styles from './TimeBlock.module.scss'; import styles from './TimeBlock.module.scss';
export default function TimeBlock() { export default function TimeBlock({
playback,
lastEvent,
selectedEventId,
}: {
playback: Playback;
lastEvent: OntimeEvent | null;
selectedEventId: string | null;
}) {
const timer = useTimer(); const timer = useTimer();
const getTimeEnd = () => {
if (lastEvent === null) {
return '...';
}
const timeEnd = lastEvent.id === selectedEventId ? timer.expectedFinish : lastEvent.timeEnd;
return formatTime(timeEnd, { showSeconds: true, format: 'hh:mm:ss' });
};
// TODO: format should be user defined
const timeNow = formatTime(timer.clock, { const timeNow = formatTime(timer.clock, {
showSeconds: true, showSeconds: true,
format: 'hh:mm:ss a', format: 'hh:mm:ss',
});
const runningTime = formatTime(timer.current, {
showSeconds: true,
format: 'hh:mm:ss',
});
const elapsedTime = formatTime(timer.elapsed, {
showSeconds: true,
format: 'hh:mm:ss',
}); });
return ( return (
<div className={styles.TimeBlock}> <div className={styles.TimeBlock}>
<IoPlay size={17} /> <PlaybackIcon state={playback} />
<div className={styles.clock}> <div className={styles.clock}>
<span className={styles.timer}>{timeNow}</span> <span className={styles.timer}>00:10:00</span> <div className={styles.column}>
<span className={styles.label}>Time now</span>
<span className={styles.timer}>{timeNow}</span>
</div>
<div className={styles.column}>
<span className={styles.label}>Time to end</span>
<span className={styles.timer}>{getTimeEnd()}</span>
</div>
<div className={styles.column}>
<span className={styles.label}>Elapsed time</span>
<span className={styles.timer}>{elapsedTime}</span>
</div>
<div className={styles.column}>
<span className={styles.label}>Running timer</span>
<span className={styles.timer}>{runningTime}</span>
</div>
</div> </div>
</div> </div>
); );