diff --git a/apps/client/src/common/components/view-params-editor/constants.ts b/apps/client/src/common/components/view-params-editor/constants.ts index 682f4ab62..c9492faff 100644 --- a/apps/client/src/common/components/view-params-editor/constants.ts +++ b/apps/client/src/common/components/view-params-editor/constants.ts @@ -201,3 +201,31 @@ export const STUDIO_CLOCK_OPTIONS: ParamField[] = [ 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', + }, + }, +]; diff --git a/apps/client/src/common/hooks/useSocket.ts b/apps/client/src/common/hooks/useSocket.ts index 95be26d8f..17d0dc57d 100644 --- a/apps/client/src/common/hooks/useSocket.ts +++ b/apps/client/src/common/hooks/useSocket.ts @@ -4,6 +4,14 @@ import { deepCompare, useRuntimeStore } from '../stores/runtime'; import { socketSendJson } from '../utils/socket'; export const useRundownEditor = () => { + const featureSelector = (state: RuntimeStore) => ({ + selectedEventId: state.loaded.selectedEventId, + }); + + return useRuntimeStore(featureSelector, deepCompare); +}; + +export const useOperator = () => { const featureSelector = (state: RuntimeStore) => ({ playback: state.playback, selectedEventId: state.loaded.selectedEventId, diff --git a/apps/client/src/features/operator/Operator.module.scss b/apps/client/src/features/operator/Operator.module.scss index 2ffccd80d..53327b71e 100644 --- a/apps/client/src/features/operator/Operator.module.scss +++ b/apps/client/src/features/operator/Operator.module.scss @@ -1,12 +1,9 @@ -// remember we are targeting phones and tablets - @use '../../../src/theme/v2Styles' as *; @use '../../../src/theme/ontimeColours' as *; .operatorContainer { width: 100vw; height: 100vh; - padding: 2rem; font-size: 0.8rem; display: flex; flex-direction: column; @@ -20,29 +17,14 @@ } @mixin event-block() { - padding: 0.2rem; + height: 40px; + padding: 0.5rem; display: flex; gap: 0.75rem; align-items: center; 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 { @include event-block(); diff --git a/apps/client/src/features/operator/Operator.tsx b/apps/client/src/features/operator/Operator.tsx index 120b1ca17..8fcce1d3a 100644 --- a/apps/client/src/features/operator/Operator.tsx +++ b/apps/client/src/features/operator/Operator.tsx @@ -1,25 +1,30 @@ -import React from 'react'; -import { SupportedEvent } from 'ontime-types'; +import { UIEvent, useState } from 'react'; +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 FocusBlock from './focus-block/focus-block'; import OpBlock from './op-block/OpBlock'; import OpEvent from './op-event/OpEvent'; import TimeBlock from './time-block/TimeBlock'; +import { getRundownEnd } from './operator.utils'; import style from './Operator.module.scss'; 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 [showChild, setShowChild] = React.useState(false); + const featureData = useOperator(); + const [showChild, setShowChild] = useState(false); + const [searchParams] = useSearchParams(); - const handleScroll = (event: any) => { - const scrollThreshold = 50; // Set the scroll threshold here - const scrollPosition = event.target.scrollTop; + const handleScroll = (event: UIEvent) => { + const scrollThreshold = 50; + const scrollPosition = (event.target as HTMLElement).scrollTop; setShowChild(scrollPosition > scrollThreshold); }; @@ -28,32 +33,35 @@ export default function Operator() { return <>loading; } + const subscribe = searchParams.get('subscribe') as keyof UserFields | null; + const lastEvent = getRundownEnd(data); + let eventIndex = 0; + return (
+
- {data.map((entry, i) => { - // 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 + {data.map((entry) => { if (entry.type === SupportedEvent.Event) { + eventIndex += 1; return ( -
- -
+ index={eventIndex} + data={entry} + isSelected={featureData.selectedEventId === entry.id} + subscribed={subscribe || undefined} + /> ); } - // this is a block entry (like a section title) if (entry.type === SupportedEvent.Block) { return ( + // @arihanv + // we prefer moving these wrapper divs to the block + // so that the styles and all attributes can be self-contained
@@ -63,7 +71,7 @@ export default function Operator() { })} {showChild && }
- +
); -} \ No newline at end of file +} diff --git a/apps/client/src/features/operator/focus-block/focus-block.tsx b/apps/client/src/features/operator/focus-block/focus-block.tsx index 01bef7dfe..18d8fa350 100644 --- a/apps/client/src/features/operator/focus-block/focus-block.tsx +++ b/apps/client/src/features/operator/focus-block/focus-block.tsx @@ -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'; 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 ( - + + ); } diff --git a/apps/client/src/features/operator/focus-block/focusBlock.module.scss b/apps/client/src/features/operator/focus-block/focusBlock.module.scss index a574211e8..18994a1f9 100644 --- a/apps/client/src/features/operator/focus-block/focusBlock.module.scss +++ b/apps/client/src/features/operator/focus-block/focusBlock.module.scss @@ -1,25 +1,22 @@ -@use '.../../../src/theme/ontimeColours' as *; +@use '../../../../src/theme/ontimeColours' as *; .focusBlock { bottom: 0; left: 0; right: 0; - position: -webkit-sticky; position: sticky; display: flex; justify-content: center; width: 100%; - margin-top: -53px; } .focusButton { margin: 10px; background-color: $blue-500; - width: fit-content; - padding: 3px 15px 3px 15px; - border-radius: 10px; + padding: 0.25rem 1rem; + border-radius: 99px; display: flex; align-items: center; - gap: 5px; - font-size: medium; + gap: 0.5rem; + font-size: 1rem; } diff --git a/apps/client/src/features/operator/op-event/OpEvent.module.scss b/apps/client/src/features/operator/op-event/OpEvent.module.scss index 22609dabb..756bb4819 100644 --- a/apps/client/src/features/operator/op-event/OpEvent.module.scss +++ b/apps/client/src/features/operator/op-event/OpEvent.module.scss @@ -1,13 +1,31 @@ -.alias { - background-color: red; - width: fit-content; - height: fit-content; +@use '../../../../src/theme/ontimeColours' as *; +@import '../Operator.module.scss'; + +.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; - align-items: center; + min-width: 4em; // we do not want these to resize + text-align: center; + border-radius: 2px; + + &:after { + content: '\200b'; + } } .title { font-size: 1rem; + letter-spacing: 0.02px; } .event { @@ -21,7 +39,10 @@ } .fields { + display: block; font-weight: 700; + color: $orange-500; + letter-spacing: 0.02px; } .time { @@ -43,10 +64,3 @@ display: flex; justify-content: space-between; } - -.chevron { - display: flex; - align-items: center; - color: yellow; - font-size: 16px; -} \ No newline at end of file diff --git a/apps/client/src/features/operator/op-event/OpEvent.tsx b/apps/client/src/features/operator/op-event/OpEvent.tsx index 58048ccac..5e3a721bf 100644 --- a/apps/client/src/features/operator/op-event/OpEvent.tsx +++ b/apps/client/src/features/operator/op-event/OpEvent.tsx @@ -1,22 +1,38 @@ -import { IoChevronUp } from '@react-icons/all-files/io5/IoChevronUp'; -import { OntimeEvent } from 'ontime-types'; +import { OntimeEvent, UserFields } 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 style from './OpEvent.module.scss'; type OpEventProps = { 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 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 ( - <> -
{data.note}
-
+
+
+
+ {index} +
{data.title} - {data.subtitle} @@ -26,15 +42,14 @@ export default function OpEvent({ data, id }: OpEventProps) { {start} - {end}
-
- -
- --:--:-- + + {isSelected ? : formatTime(data.duration, { showSeconds: true, format: 'hh:mm:ss' })}
- {id % 3 == 0 &&
CAM 5 Slow pan to SL
}
- + {/** @arihanv we likely want to animate the height of the fields div */} +
{subscribedData}
+
); -} \ No newline at end of file +} diff --git a/apps/client/src/features/operator/operator.utils.ts b/apps/client/src/features/operator/operator.utils.ts new file mode 100644 index 000000000..5e84f7ccb --- /dev/null +++ b/apps/client/src/features/operator/operator.utils.ts @@ -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; +} diff --git a/apps/client/src/features/operator/time-block/TimeBlock.module.scss b/apps/client/src/features/operator/time-block/TimeBlock.module.scss index e9cfcbb68..43da2e062 100644 --- a/apps/client/src/features/operator/time-block/TimeBlock.module.scss +++ b/apps/client/src/features/operator/time-block/TimeBlock.module.scss @@ -2,6 +2,15 @@ padding: 0.65rem; display: flex; 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 { @@ -9,3 +18,8 @@ gap: 1.5rem; margin-right: 1rem; } + +.label { + font-size: 0.75rem; + color: gray; +} diff --git a/apps/client/src/features/operator/time-block/TimeBlock.tsx b/apps/client/src/features/operator/time-block/TimeBlock.tsx index 64da63565..0b199d4e4 100644 --- a/apps/client/src/features/operator/time-block/TimeBlock.tsx +++ b/apps/client/src/features/operator/time-block/TimeBlock.tsx @@ -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 { formatTime } from '../../../common/utils/time'; 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 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, { 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 (
- +
- {timeNow} 00:10:00 +
+ Time now + {timeNow} +
+
+ Time to end + {getTimeEnd()} +
+
+ Elapsed time + {elapsedTime} +
+
+ Running timer + {runningTime} +
);