From 32f18d8d23742aaf76a457adaa5111ebd034dd90 Mon Sep 17 00:00:00 2001 From: cv <34649812+cpvalente@users.noreply.github.com> Date: Mon, 28 Aug 2023 21:42:27 +0200 Subject: [PATCH] feat: operator view --- .../src/common/components/state/Empty.tsx | 1 + .../ViewParamsEditor.module.scss | 2 +- .../view-params-editor/constants.ts | 62 ++++++++++------- .../src/common/context/useMediaQuery.ts | 32 +++++++++ apps/client/src/common/utils/styleUtils.ts | 6 +- .../src/features/editors/Editor.module.scss | 4 +- .../features/operator/Operator.module.scss | 33 +++++---- .../client/src/features/operator/Operator.tsx | 57 +++++++++------- .../operator/focus-block/focus-block.tsx | 17 ----- .../focus-block/focusBlock.module.scss | 22 ------ .../follow-button/FollowButton.module.scss | 36 ++++++++++ .../operator/follow-button/FollowButton.tsx | 23 +++++++ .../operator/op-block/OpBlock.module.scss | 3 - .../features/operator/op-block/OpBlock.tsx | 11 --- .../operator/op-event/OpEvent.module.scss | 66 ------------------ .../features/operator/op-event/OpEvent.tsx | 55 --------------- .../operator-block/OperatorBlock.module.scss | 8 +++ .../operator/operator-block/OperatorBlock.tsx | 9 +++ .../operator-event/OperatorEvent.module.scss | 67 +++++++++++++++++++ .../operator/operator-event/OperatorEvent.tsx | 63 +++++++++++++++++ .../src/features/operator/operator.utils.ts | 15 ----- .../operator/status-bar/StatusBar.module.scss | 34 ++++++++++ .../StatusBar.tsx} | 23 +++---- .../operator/time-block/TimeBlock.module.scss | 25 ------- .../src/features/rundown/_blockMixins.scss | 2 +- apps/client/src/theme/_ontimeColours.scss | 4 +- apps/client/src/theme/_v2Styles.scss | 6 +- apps/client/src/theme/ontimeButton.ts | 4 +- apps/client/src/theme/ontimeRadio.ts | 6 +- apps/client/src/theme/ontimeTextInputs.ts | 2 +- packages/utils/index.ts | 2 +- .../utils/src/rundown-utils/rundownUtils.ts | 15 +++++ 32 files changed, 399 insertions(+), 316 deletions(-) create mode 100644 apps/client/src/common/context/useMediaQuery.ts delete mode 100644 apps/client/src/features/operator/focus-block/focus-block.tsx delete mode 100644 apps/client/src/features/operator/focus-block/focusBlock.module.scss create mode 100644 apps/client/src/features/operator/follow-button/FollowButton.module.scss create mode 100644 apps/client/src/features/operator/follow-button/FollowButton.tsx delete mode 100644 apps/client/src/features/operator/op-block/OpBlock.module.scss delete mode 100644 apps/client/src/features/operator/op-block/OpBlock.tsx delete mode 100644 apps/client/src/features/operator/op-event/OpEvent.module.scss delete mode 100644 apps/client/src/features/operator/op-event/OpEvent.tsx create mode 100644 apps/client/src/features/operator/operator-block/OperatorBlock.module.scss create mode 100644 apps/client/src/features/operator/operator-block/OperatorBlock.tsx create mode 100644 apps/client/src/features/operator/operator-event/OperatorEvent.module.scss create mode 100644 apps/client/src/features/operator/operator-event/OperatorEvent.tsx delete mode 100644 apps/client/src/features/operator/operator.utils.ts create mode 100644 apps/client/src/features/operator/status-bar/StatusBar.module.scss rename apps/client/src/features/operator/{time-block/TimeBlock.tsx => status-bar/StatusBar.tsx} (76%) delete mode 100644 apps/client/src/features/operator/time-block/TimeBlock.module.scss diff --git a/apps/client/src/common/components/state/Empty.tsx b/apps/client/src/common/components/state/Empty.tsx index 776f92b4a..db43ae49d 100644 --- a/apps/client/src/common/components/state/Empty.tsx +++ b/apps/client/src/common/components/state/Empty.tsx @@ -1,4 +1,5 @@ import { CSSProperties } from 'react'; + import { ReactComponent as Emptyimage } from '@/assets/images/empty.svg'; import style from './Empty.module.scss'; diff --git a/apps/client/src/common/components/view-params-editor/ViewParamsEditor.module.scss b/apps/client/src/common/components/view-params-editor/ViewParamsEditor.module.scss index 38c18e674..7bf970ecf 100644 --- a/apps/client/src/common/components/view-params-editor/ViewParamsEditor.module.scss +++ b/apps/client/src/common/components/view-params-editor/ViewParamsEditor.module.scss @@ -2,7 +2,7 @@ @use '../../../theme/ontimeColours' as *; .drawerContent { - background-color: $gray-1200; + background-color: $gray-1250; } .drawerHeader { 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 c9492faff..2407369cf 100644 --- a/apps/client/src/common/components/view-params-editor/constants.ts +++ b/apps/client/src/common/components/view-params-editor/constants.ts @@ -1,3 +1,5 @@ +import { UserFields } from 'ontime-types'; + import { ParamField } from './types'; export const TIME_FORMAT_OPTION: ParamField = { @@ -202,30 +204,38 @@ export const STUDIO_CLOCK_OPTIONS: ParamField[] = [ }, ]; -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', +export const getOperatorOptions = (userFields: UserFields): ParamField[] => { + return [ + TIME_FORMAT_OPTION, + { + id: 'showseconds', + title: 'Show seconds', + description: 'Schedule shows hh:mm:ss', + type: 'boolean', }, - }, -]; + { + 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: userFields.user0 || 'user0', + user1: userFields.user1 || 'user1', + user2: userFields.user2 || 'user2', + user3: userFields.user3 || 'user3', + user4: userFields.user4 || 'user4', + user5: userFields.user5 || 'user5', + user6: userFields.user6 || 'user6', + user7: userFields.user7 || 'user7', + user8: userFields.user8 || 'user8', + user9: userFields.user9 || 'user9', + }, + }, + ]; +}; diff --git a/apps/client/src/common/context/useMediaQuery.ts b/apps/client/src/common/context/useMediaQuery.ts new file mode 100644 index 000000000..e2d282529 --- /dev/null +++ b/apps/client/src/common/context/useMediaQuery.ts @@ -0,0 +1,32 @@ +// roughly from https://github.com/juliencrn/usehooks-ts/blob/master/packages/usehooks-ts/src/useMediaQuery/useMediaQuery.ts + +import { useCallback, useEffect, useState } from 'react'; + +function getMatches(query: string): boolean { + return window.matchMedia(query).matches; +} + +// TODO: debounce handleChange +export default function useMediaQuery(query: string): boolean { + const [matches, setMatches] = useState(getMatches(query)); + + const handleChange = useCallback(() => { + setMatches(getMatches(query)); + }, [query]); + + useEffect(() => { + const matchMedia = window.matchMedia(query); + + // Triggered at the first client-side load and if query changes + handleChange(); + + // Listen matchMedia + matchMedia.addEventListener('change', handleChange); + + return () => { + matchMedia.removeEventListener('change', handleChange); + }; + }, [handleChange, query]); + + return matches; +} diff --git a/apps/client/src/common/utils/styleUtils.ts b/apps/client/src/common/utils/styleUtils.ts index 2be4ff166..f0e2a05e6 100644 --- a/apps/client/src/common/utils/styleUtils.ts +++ b/apps/client/src/common/utils/styleUtils.ts @@ -3,7 +3,7 @@ import Color from 'color'; type ColourCombination = { backgroundColor: string; color: string; -} +}; /** * @description Selects text colour to maintain accessible contrast @@ -19,11 +19,11 @@ export const getAccessibleColour = (bgColour: string): ColourCombination => { console.log(`Unable to parse colour: ${bgColour}`); } } - return { backgroundColor: '#000', color: "#fffffa" }; + return { backgroundColor: '#000', color: '#fffffa' }; }; /** * @description Creates a list of classnames from array of css module conditions * @param classNames - css modules objects */ -export const cx = (classNames: any[]) => classNames.filter(Boolean).join(" "); +export const cx = (classNames: any[]) => classNames.filter(Boolean).join(' '); diff --git a/apps/client/src/features/editors/Editor.module.scss b/apps/client/src/features/editors/Editor.module.scss index acdd490c0..3d569eb5a 100644 --- a/apps/client/src/features/editors/Editor.module.scss +++ b/apps/client/src/features/editors/Editor.module.scss @@ -144,7 +144,7 @@ $playback-width: 26rem; .eventEditor { border-radius: 8px 8px 0 0; background-color: $bg-container-l2; - box-shadow: rgba(0, 0, 0, 0.35) 0 3px 6px 6px; + box-shadow: $large-bottom-drawer-shadow; border-top: 1px solid $white-20; position: absolute; bottom: 0; @@ -164,7 +164,7 @@ $playback-width: 26rem; } .header { - background-color: $gray-1200; + background-color: $gray-1250; padding: 0.5rem; border-left: 1px solid $white-10; border-radius: 0 8px 0 0; diff --git a/apps/client/src/features/operator/Operator.module.scss b/apps/client/src/features/operator/Operator.module.scss index 53327b71e..62ba30e1f 100644 --- a/apps/client/src/features/operator/Operator.module.scss +++ b/apps/client/src/features/operator/Operator.module.scss @@ -4,29 +4,26 @@ .operatorContainer { width: 100vw; height: 100vh; - font-size: 0.8rem; - display: flex; - flex-direction: column; - gap: 2px; - color: white; + + color: $ui-white; } .operatorEvents { - flex: 1; + width: 100vw; + height: 100vh; + overflow: auto; -} - -@mixin event-block() { - height: 40px; - padding: 0.5rem; display: flex; - gap: 0.75rem; - align-items: center; - margin: 0.1rem; + flex-direction: column; + gap: 2px; } +@mixin block() { + width: 100%; + padding: 0.25rem 0.5rem; -.block { - @include event-block(); - background-color: $gray-1350; -} \ No newline at end of file + // tablet + @media (min-width: $min-tablet) { + padding: 0.25rem 1rem; + } +} diff --git a/apps/client/src/features/operator/Operator.tsx b/apps/client/src/features/operator/Operator.tsx index 8fcce1d3a..24163faa4 100644 --- a/apps/client/src/features/operator/Operator.tsx +++ b/apps/client/src/features/operator/Operator.tsx @@ -1,27 +1,36 @@ -import { UIEvent, useState } from 'react'; +import { UIEvent, useEffect, useState } from 'react'; import { useSearchParams } from 'react-router-dom'; import { SupportedEvent, UserFields } from 'ontime-types'; +import { getLastEvent } from 'ontime-utils'; import NavigationMenu from '../../common/components/navigation-menu/NavigationMenu'; -import { OPERATOR_OPTIONS } from '../../common/components/view-params-editor/constants'; +import Empty from '../../common/components/state/Empty'; +import { getOperatorOptions } 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 useUserFields from '../../common/hooks-query/useUserFields'; -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 FollowButton from './follow-button/FollowButton'; +import OperatorBlock from './operator-block/OperatorBlock'; +import OperatorEvent from './operator-event/OperatorEvent'; +import StatusBar from './status-bar/StatusBar'; import style from './Operator.module.scss'; +import { isStringBoolean } from '../../common/utils/viewUtils'; export default function Operator() { const { data, status } = useRundown(); + const { data: userFields, status: userFieldsStatus } = useUserFields(); const featureData = useOperator(); const [showChild, setShowChild] = useState(false); const [searchParams] = useSearchParams(); + // Set window title + useEffect(() => { + document.title = 'ontime - Operator'; + }, []); + const handleScroll = (event: UIEvent) => { const scrollThreshold = 50; const scrollPosition = (event.target as HTMLElement).scrollTop; @@ -29,49 +38,45 @@ export default function Operator() { setShowChild(scrollPosition > scrollThreshold); }; - if (!data || status === 'loading') { - return <>loading; + if (!data || status === 'loading' || !userFields || userFieldsStatus === 'loading') { + return ; } + // get fields which the user subscribed to const subscribe = searchParams.get('subscribe') as keyof UserFields | null; - const lastEvent = getRundownEnd(data); - let eventIndex = 0; + const showSeconds = isStringBoolean(searchParams.get('showseconds')); + const lastEvent = getLastEvent(data); + + const operatorOptions = getOperatorOptions(userFields); return (
- +
{data.map((entry) => { if (entry.type === SupportedEvent.Event) { - eventIndex += 1; return ( - ); } 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 -
- -
- ); + return ; } return null; })} - {showChild && }
- + undefined} /> +
); } diff --git a/apps/client/src/features/operator/focus-block/focus-block.tsx b/apps/client/src/features/operator/focus-block/focus-block.tsx deleted file mode 100644 index 18d8fa350..000000000 --- a/apps/client/src/features/operator/focus-block/focus-block.tsx +++ /dev/null @@ -1,17 +0,0 @@ -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 deleted file mode 100644 index 18994a1f9..000000000 --- a/apps/client/src/features/operator/focus-block/focusBlock.module.scss +++ /dev/null @@ -1,22 +0,0 @@ -@use '../../../../src/theme/ontimeColours' as *; - -.focusBlock { - bottom: 0; - left: 0; - right: 0; - position: sticky; - display: flex; - justify-content: center; - width: 100%; -} - -.focusButton { - margin: 10px; - background-color: $blue-500; - padding: 0.25rem 1rem; - border-radius: 99px; - display: flex; - align-items: center; - gap: 0.5rem; - font-size: 1rem; -} diff --git a/apps/client/src/features/operator/follow-button/FollowButton.module.scss b/apps/client/src/features/operator/follow-button/FollowButton.module.scss new file mode 100644 index 000000000..44c08c58d --- /dev/null +++ b/apps/client/src/features/operator/follow-button/FollowButton.module.scss @@ -0,0 +1,36 @@ +@use '../../../../src/theme/ontimeColours' as *; +@use '../../../../src/theme/v2Styles' as *; + +.followButton { + position: relative; + bottom: 17.5vh; + margin: 0 auto; + z-index: 1; + + display: flex; + align-items: center; + gap: 0.5rem; + padding: 0.25rem 1rem; + background-color: $blue-700; + color: #fff; + font-size: 1rem; + border-radius: 99px; + transition: bottom 1s; + + &:active { + transition: background-color $transition-time-action; + background-color: $blue-900; + } +} + +.hidden { + transition: bottom 1s; + bottom: -0; +} + +// tablet +@media (min-width: $min-tablet) { + .followButton { + bottom: 10vh; + } +} \ No newline at end of file diff --git a/apps/client/src/features/operator/follow-button/FollowButton.tsx b/apps/client/src/features/operator/follow-button/FollowButton.tsx new file mode 100644 index 000000000..8a4875e6b --- /dev/null +++ b/apps/client/src/features/operator/follow-button/FollowButton.tsx @@ -0,0 +1,23 @@ +import { IoLocate } from '@react-icons/all-files/io5/IoLocate'; + +import { cx } from '../../../common/utils/styleUtils'; + +import style from './FollowButton.module.scss'; + +interface FollowButtonProps { + isVisible: boolean; + onClickHandler: () => void; +} + +export default function FollowButton(props: FollowButtonProps) { + const { isVisible, onClickHandler } = props; + + const classes = cx([style.followButton, !isVisible && style.hidden]); + + return ( + + ); +} diff --git a/apps/client/src/features/operator/op-block/OpBlock.module.scss b/apps/client/src/features/operator/op-block/OpBlock.module.scss deleted file mode 100644 index 4c9f4bb7f..000000000 --- a/apps/client/src/features/operator/op-block/OpBlock.module.scss +++ /dev/null @@ -1,3 +0,0 @@ -.OpBlock { - font-size: 1rem; -} diff --git a/apps/client/src/features/operator/op-block/OpBlock.tsx b/apps/client/src/features/operator/op-block/OpBlock.tsx deleted file mode 100644 index dfed4ab15..000000000 --- a/apps/client/src/features/operator/op-block/OpBlock.tsx +++ /dev/null @@ -1,11 +0,0 @@ -import { OntimeBlock } from 'ontime-types'; - -import style from './OpBlock.module.scss'; - -interface OpBlockProps { - data: OntimeBlock; -} - -export default function OpBlock({ data }: OpBlockProps) { - return
{data.title}
; -} diff --git a/apps/client/src/features/operator/op-event/OpEvent.module.scss b/apps/client/src/features/operator/op-event/OpEvent.module.scss deleted file mode 100644 index 756bb4819..000000000 --- a/apps/client/src/features/operator/op-event/OpEvent.module.scss +++ /dev/null @@ -1,66 +0,0 @@ -@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; - 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 { - display: flex; - gap: 6px; - width: 100%; -} - -.block { - width: 100%; -} - -.fields { - display: block; - font-weight: 700; - color: $orange-500; - letter-spacing: 0.02px; -} - -.time { - display: flex; - flex-direction: column; - font-size: 0.65rem; - text-align: right; -} - -.time > div { - margin-bottom: -4px; /* Adjust the value as needed */ -} - -.title { - flex: 1; -} - -.indicator { - display: flex; - justify-content: space-between; -} diff --git a/apps/client/src/features/operator/op-event/OpEvent.tsx b/apps/client/src/features/operator/op-event/OpEvent.tsx deleted file mode 100644 index 5e3a721bf..000000000 --- a/apps/client/src/features/operator/op-event/OpEvent.tsx +++ /dev/null @@ -1,55 +0,0 @@ -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; - index: number; - isSelected: boolean; - subscribed?: keyof UserFields; -}; - -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 ( -
-
-
- {index} -
-
-
- {data.title} - {data.subtitle} -
-
-
- {start} - {end} -
-
- - {isSelected ? : formatTime(data.duration, { showSeconds: true, format: 'hh:mm:ss' })} -
-
-
-
- {/** @arihanv we likely want to animate the height of the fields div */} -
{subscribedData}
-
- ); -} diff --git a/apps/client/src/features/operator/operator-block/OperatorBlock.module.scss b/apps/client/src/features/operator/operator-block/OperatorBlock.module.scss new file mode 100644 index 000000000..b49494ca6 --- /dev/null +++ b/apps/client/src/features/operator/operator-block/OperatorBlock.module.scss @@ -0,0 +1,8 @@ +@use '../../../../src/theme/ontimeColours' as *; +@import '../Operator.module.scss'; + +.block { + @include block(); + background-color: $gray-1350; + font-size: 1.25rem; +} diff --git a/apps/client/src/features/operator/operator-block/OperatorBlock.tsx b/apps/client/src/features/operator/operator-block/OperatorBlock.tsx new file mode 100644 index 000000000..ecf14b8fc --- /dev/null +++ b/apps/client/src/features/operator/operator-block/OperatorBlock.tsx @@ -0,0 +1,9 @@ +import style from './OperatorBlock.module.scss'; + +interface OperatorBlockProps { + title: string; +} + +export default function OperatorBlock({ title }: OperatorBlockProps) { + return
{title}
; +} diff --git a/apps/client/src/features/operator/operator-event/OperatorEvent.module.scss b/apps/client/src/features/operator/operator-event/OperatorEvent.module.scss new file mode 100644 index 000000000..e0b90604b --- /dev/null +++ b/apps/client/src/features/operator/operator-event/OperatorEvent.module.scss @@ -0,0 +1,67 @@ +@use '../../../../src/theme/v2Styles' as *; +@use '../../../../src/theme/ontimeColours' as *; +@import '../Operator.module.scss'; + +.event { + @include block(); + background-color: $gray-1300; + + &.subscribed { + background-color: $gray-1250; + } + + &.running { + background-color: $red-700; + } +} + +.titles { + display: flex; + align-items: center; + justify-content: space-between; + + .title { + font-size: 1.5rem; + letter-spacing: 0.02px; + } + + .cue { + font-size: 1rem; + background-color: $gray-1050; // to override inline + padding: 0 0.5rem; + text-align: center; + border-radius: $component-border-radius-sm; + } +} + +.times { + display: flex; + align-items: center; + justify-content: space-between; + font-size: calc(1rem - 1px); + + .runtime { + display: flex; + align-items: center; + gap: 0.25rem; + } +} + +.fields { + font-size: 1.25rem; + font-weight: 500; + color: $orange-500; + + letter-spacing: 0.02px; + height: fit-content; + + transition-property: height; + transition-duration: $transition-time-feedback; +} + +// tablet +@media (min-width: $min-tablet) { + .times { + font-size: 1rem; + } +} \ No newline at end of file diff --git a/apps/client/src/features/operator/operator-event/OperatorEvent.tsx b/apps/client/src/features/operator/operator-event/OperatorEvent.tsx new file mode 100644 index 000000000..4da6a4491 --- /dev/null +++ b/apps/client/src/features/operator/operator-event/OperatorEvent.tsx @@ -0,0 +1,63 @@ +import { OntimeEvent, UserFields } from 'ontime-types'; + +import DelayIndicator from '../../../common/components/delay-indicator/DelayIndicator'; +import { useTimer } from '../../../common/hooks/useSocket'; +import { cx, getAccessibleColour } from '../../../common/utils/styleUtils'; +import { formatTime } from '../../../common/utils/time'; + +import style from './OperatorEvent.module.scss'; + +interface OperatorEventProps { + data: OntimeEvent; + cue: string; + isSelected: boolean; + subscribed: keyof UserFields | null; + showSeconds: boolean; +} + +// extract this to contain re-renders +function RollingTime() { + const timer = useTimer(); + return <>{formatTime(timer.current, { showSeconds: true, format: 'hh:mm:ss' })}; +} + +export default function OperatorEvent(props: OperatorEventProps) { + const { data, cue, isSelected, subscribed, showSeconds } = props; + + const start = formatTime(data.timeStart, { showSeconds }); + const end = formatTime(data.timeEnd, { showSeconds }); + + const cueColours = data.colour && getAccessibleColour(data.colour); + const subscribedData = (subscribed ? data?.[subscribed] : undefined) || ''; + + const operatorClasses = cx([ + style.event, + isSelected ? style.running : null, + subscribedData ? style.subscribed : null, + ]); + + return ( +
+
+ + {data.title} - {data.subtitle} + + + {cue} + +
+ +
+ + {start} - {end} + + + + {isSelected ? : formatTime(data.duration, { showSeconds: true, format: 'hh:mm:ss' })} + +
+ +
{subscribedData}
+
+ ); +} diff --git a/apps/client/src/features/operator/operator.utils.ts b/apps/client/src/features/operator/operator.utils.ts deleted file mode 100644 index 5e84f7ccb..000000000 --- a/apps/client/src/features/operator/operator.utils.ts +++ /dev/null @@ -1,15 +0,0 @@ -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/status-bar/StatusBar.module.scss b/apps/client/src/features/operator/status-bar/StatusBar.module.scss new file mode 100644 index 000000000..f0e5a2be3 --- /dev/null +++ b/apps/client/src/features/operator/status-bar/StatusBar.module.scss @@ -0,0 +1,34 @@ +@use '../../../../src/theme/ontimeColours' as *; +@use '../../../../src/theme/v2Styles' as *; + +.statusBar { + position: absolute; + bottom: 0; + width: 100%; + background-color: $gray-1350; + z-index: 2; + + padding: 0.5rem; + display: flex; + justify-content: space-between; + border-top: 1px solid $white-10; + box-shadow: $large-bottom-drawer-shadow; +} + +.column { + display: flex; + flex-direction: column; + align-items: center; + text-align: center; +} + +.clock { + display: flex; + 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/status-bar/StatusBar.tsx similarity index 76% rename from apps/client/src/features/operator/time-block/TimeBlock.tsx rename to apps/client/src/features/operator/status-bar/StatusBar.tsx index 0b199d4e4..7d0ac0ffd 100644 --- a/apps/client/src/features/operator/time-block/TimeBlock.tsx +++ b/apps/client/src/features/operator/status-bar/StatusBar.tsx @@ -1,12 +1,13 @@ import { OntimeEvent, Playback } from 'ontime-types'; +import { millisToString } from 'ontime-utils'; 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'; +import styles from './StatusBar.module.scss'; -export default function TimeBlock({ +export default function StatusBar({ playback, lastEvent, selectedEventId, @@ -23,27 +24,19 @@ export default function TimeBlock({ } const timeEnd = lastEvent.id === selectedEventId ? timer.expectedFinish : lastEvent.timeEnd; - return formatTime(timeEnd, { showSeconds: true, format: 'hh:mm:ss' }); + return millisToString(timeEnd); }; - // TODO: format should be user defined + // use user defined format const timeNow = formatTime(timer.clock, { showSeconds: true, - 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', - }); + const runningTime = millisToString(timer.current); + const elapsedTime = millisToString(timer.elapsed); return ( -
+
diff --git a/apps/client/src/features/operator/time-block/TimeBlock.module.scss b/apps/client/src/features/operator/time-block/TimeBlock.module.scss deleted file mode 100644 index 43da2e062..000000000 --- a/apps/client/src/features/operator/time-block/TimeBlock.module.scss +++ /dev/null @@ -1,25 +0,0 @@ -.TimeBlock { - 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 { - display: flex; - gap: 1.5rem; - margin-right: 1rem; -} - -.label { - font-size: 0.75rem; - color: gray; -} diff --git a/apps/client/src/features/rundown/_blockMixins.scss b/apps/client/src/features/rundown/_blockMixins.scss index 7e7ab0bfc..73d712581 100644 --- a/apps/client/src/features/rundown/_blockMixins.scss +++ b/apps/client/src/features/rundown/_blockMixins.scss @@ -7,7 +7,7 @@ $block-binder-width: 2rem; $block-clearance: 0.5rem; $block-border-radius: 0.5rem; $block-text-color: $gray-50; -$block-bg: $gray-1200; +$block-bg: $gray-1250; $block-bg2: $gray-1050; // for delay and blocks $block-box-shadow: rgba(0, 0, 0, 0.5) 0 0 3px 2px; $secondary-block-height: 2.5rem; diff --git a/apps/client/src/theme/_ontimeColours.scss b/apps/client/src/theme/_ontimeColours.scss index a9db62269..c24ff85f6 100644 --- a/apps/client/src/theme/_ontimeColours.scss +++ b/apps/client/src/theme/_ontimeColours.scss @@ -24,8 +24,8 @@ $gray-900: #4c4c4c; $gray-1000: #404040; $gray-1050: #303030; $gray-1100: #2d2d2d; -$gray-1250: #262626; -$gray-1200: #202020; +$gray-1200: #262626; +$gray-1250: #202020; $gray-1300: #1a1a1a; $gray-1350: #101010; $pure-white: #fff; diff --git a/apps/client/src/theme/_v2Styles.scss b/apps/client/src/theme/_v2Styles.scss index c299fb8e8..b812ae78c 100644 --- a/apps/client/src/theme/_v2Styles.scss +++ b/apps/client/src/theme/_v2Styles.scss @@ -36,6 +36,7 @@ $bg-container-onlight: $gray-100; $box-shadow-l1: rgba(0, 0, 0, 0.15) 0 3px 3px 0; $box-shadow-l2: rgba(0, 0, 0, 0.15) 0 3px 3px 0; +$large-bottom-drawer-shadow: rgba(0, 0, 0, 0.35) 0 3px 6px 6px; $modal-note-color: $gray-700; @@ -54,6 +55,9 @@ $section-white: $ui-white; $inner-section-text-size: calc(1rem - 2px); $text-body-size: calc(1rem - 1px); +// media queries +$min-tablet: 500px; + .blink { animation: blink $blinking-time linear infinite; } @@ -62,4 +66,4 @@ $text-body-size: calc(1rem - 1px); 50% { opacity: 20%; } -} \ No newline at end of file +} diff --git a/apps/client/src/theme/ontimeButton.ts b/apps/client/src/theme/ontimeButton.ts index ab50a341b..50442c6a8 100644 --- a/apps/client/src/theme/ontimeButton.ts +++ b/apps/client/src/theme/ontimeButton.ts @@ -27,7 +27,7 @@ export const ontimeButtonOutlined = { }, _active: { backgroundColor: '#2d2d2d', // $gray-1100 - borderColor: '#202020', // $gray-12000 + borderColor: '#202020', // $gray-1250 }, }; @@ -43,7 +43,7 @@ export const ontimeButtonSubtle = { }, _active: { backgroundColor: '#2d2d2d', // $gray-1100 - borderColor: '#202020', // $gray-12000 + borderColor: '#202020', // $gray-1250 }, }; diff --git a/apps/client/src/theme/ontimeRadio.ts b/apps/client/src/theme/ontimeRadio.ts index 9a56d8d3b..bad8ba9c6 100644 --- a/apps/client/src/theme/ontimeRadio.ts +++ b/apps/client/src/theme/ontimeRadio.ts @@ -1,9 +1,9 @@ export const ontimeBlockRadio = { control: { - borderColor: '#262626', // $gray-1250 - backgroundColor: '#262626', // $gray-1250 + borderColor: '#262626', // $gray-1200 + backgroundColor: '#262626', // $gray-1200 _checked: { - borderColor: '#262626', // $gray-1250 + borderColor: '#262626', // $gray-1200 color: '#3182ce', // $action-blue backgroundColor: '#3182ce', // $action-blue }, diff --git a/apps/client/src/theme/ontimeTextInputs.ts b/apps/client/src/theme/ontimeTextInputs.ts index c135d589e..e64381c52 100644 --- a/apps/client/src/theme/ontimeTextInputs.ts +++ b/apps/client/src/theme/ontimeTextInputs.ts @@ -1,6 +1,6 @@ const commonStyles = { fontWeight: '400', - backgroundColor: '#262626', // $gray-1250 + backgroundColor: '#262626', // $gray-1200 color: '#e2e2e2', // $gray-200 border: '1px solid transparent', _hover: { diff --git a/packages/utils/index.ts b/packages/utils/index.ts index 02a936e71..6dc3c2ca8 100644 --- a/packages/utils/index.ts +++ b/packages/utils/index.ts @@ -1,5 +1,5 @@ // runtime utils -export { getFirst, getNext, getPrevious } from './src/rundown-utils/rundownUtils.js'; +export { getFirst, getLastEvent, getNext, getPrevious } from './src/rundown-utils/rundownUtils.js'; export { validatePlayback } from './src/validate-action/validatePlayback.js'; // rundown utils diff --git a/packages/utils/src/rundown-utils/rundownUtils.ts b/packages/utils/src/rundown-utils/rundownUtils.ts index c1efeb095..685a8ddf9 100644 --- a/packages/utils/src/rundown-utils/rundownUtils.ts +++ b/packages/utils/src/rundown-utils/rundownUtils.ts @@ -26,6 +26,21 @@ export function getFirstEvent(rundown: OntimeRundownEntry[]) { return null; } +export function getLastEvent(rundown: OntimeRundown): OntimeEvent | null { + if (rundown.length < 1) { + return null; + } + + for (let i = rundown.length - 1; i > 0; i--) { + const event = rundown.at(i); + if (isOntimeEvent(event)) { + return event; + } + } + + return null; +} + /** * Gets next event in rundown, if it exists * @param {OntimeRundownEntry[]} rundown