From 221ff7b3af285c9917e0d38b1b7bc5c9599808ba Mon Sep 17 00:00:00 2001 From: Alex Christoffer Rasmussen Date: Mon, 27 Jan 2025 16:39:32 +0100 Subject: [PATCH] Timeuntil in UI (#1461) * cherry pick EventBlockChip * pull data down throug components * add comment * cleanup css Co-authored-by: Carlos Valente <34649812+cpvalente@users.noreply.github.com> --------- Co-authored-by: Carlos Valente <34649812+cpvalente@users.noreply.github.com> --- apps/client/src/common/hooks/useSocket.ts | 8 ++ apps/client/src/features/rundown/Rundown.tsx | 3 + .../src/features/rundown/RundownEntry.tsx | 4 + .../event-block/EventBlock.module.scss | 6 +- .../rundown/event-block/EventBlock.tsx | 7 ++ .../rundown/event-block/EventBlockInner.tsx | 19 +++++ .../composite/EventBlockChip.module.scss | 21 +++++ .../event-block/composite/EventBlockChip.tsx | 77 +++++++++++++++++++ apps/client/src/theme/_ontimeStyles.scss | 1 + 9 files changed, 145 insertions(+), 1 deletion(-) create mode 100644 apps/client/src/features/rundown/event-block/composite/EventBlockChip.module.scss create mode 100644 apps/client/src/features/rundown/event-block/composite/EventBlockChip.tsx diff --git a/apps/client/src/common/hooks/useSocket.ts b/apps/client/src/common/hooks/useSocket.ts index 06948c53d..1ccbec87a 100644 --- a/apps/client/src/common/hooks/useSocket.ts +++ b/apps/client/src/common/hooks/useSocket.ts @@ -250,3 +250,11 @@ export const useIsOnline = () => { return useRuntimeStore(featureSelector); }; + +export const usePlayback = () => { + const featureSelector = (state: RuntimeStore) => ({ + playback: state.timer.playback, + }); + + return useRuntimeStore(featureSelector); +}; diff --git a/apps/client/src/features/rundown/Rundown.tsx b/apps/client/src/features/rundown/Rundown.tsx index 7099ab0f1..6106900cd 100644 --- a/apps/client/src/features/rundown/Rundown.tsx +++ b/apps/client/src/features/rundown/Rundown.tsx @@ -269,6 +269,7 @@ export default function Rundown({ data }: RundownProps) { // all events before the current selected are in the past let isPast = Boolean(featureData?.selectedEventId); let isNextDay = false; + let totalGap = 0; const isEditMode = appMode === AppMode.Edit; return ( @@ -297,6 +298,7 @@ export default function Rundown({ data }: RundownProps) { if (isPlayableEvent(entry)) { isNextDay = checkIsNextDay(entry, lastEvent); + totalGap += !isPast ? entry.gap : 0; if (isNewLatest(entry, lastEvent)) { // populate previous entry thisEvent = entry; @@ -331,6 +333,7 @@ export default function Rundown({ data }: RundownProps) { playback={isLoaded ? featureData.playback : undefined} isRolling={featureData.playback === Playback.Roll} isNextDay={isNextDay} + totalGap={totalGap} /> diff --git a/apps/client/src/features/rundown/RundownEntry.tsx b/apps/client/src/features/rundown/RundownEntry.tsx index d7fb2ce7d..386950aa5 100644 --- a/apps/client/src/features/rundown/RundownEntry.tsx +++ b/apps/client/src/features/rundown/RundownEntry.tsx @@ -37,6 +37,7 @@ interface RundownEntryProps { previousEventId?: string; playback?: Playback; // we only care about this if this event is playing isRolling: boolean; // we need to know even if not related to this event + totalGap: number; } export default function RundownEntry(props: RundownEntryProps) { @@ -52,6 +53,7 @@ export default function RundownEntry(props: RundownEntryProps) { isRolling, eventIndex, isNextDay, + totalGap, } = props; const { emitError } = useEmitLog(); const { addEvent, updateEvent, batchUpdateEvents, deleteEvent, swapEvents } = useEventAction(); @@ -173,6 +175,8 @@ export default function RundownEntry(props: RundownEntryProps) { isRolling={isRolling} gap={data.gap} isNextDay={isNextDay} + dayOffset={data.dayOffset} + totalGap={totalGap} actionHandler={actionHandler} /> ); diff --git a/apps/client/src/features/rundown/event-block/EventBlock.module.scss b/apps/client/src/features/rundown/event-block/EventBlock.module.scss index 2328b52e0..1ee1bb917 100644 --- a/apps/client/src/features/rundown/event-block/EventBlock.module.scss +++ b/apps/client/src/features/rundown/event-block/EventBlock.module.scss @@ -9,7 +9,7 @@ $skip-opacity: 0.2; display: grid; grid-template-areas: 'binder ... ... ...' - 'binder pb-actions times ...' + 'binder pb-actions times chip' 'binder pb-actions title title' 'binder pb-actions estatus estatus' 'binder ... ... ...'; @@ -131,6 +131,10 @@ $skip-opacity: 0.2; height: 100%; } +.chipSection { + grid-area: chip; +} + .titleSection { grid-area: title; display: flex; diff --git a/apps/client/src/features/rundown/event-block/EventBlock.tsx b/apps/client/src/features/rundown/event-block/EventBlock.tsx index 1f1de2ef3..8fd2fc4ea 100644 --- a/apps/client/src/features/rundown/event-block/EventBlock.tsx +++ b/apps/client/src/features/rundown/event-block/EventBlock.tsx @@ -49,6 +49,8 @@ interface EventBlockProps { isRolling: boolean; gap: number; isNextDay: boolean; + dayOffset: number; + totalGap: number; actionHandler: ( action: EventItemActions, payload?: @@ -87,6 +89,8 @@ export default function EventBlock(props: EventBlockProps) { isRolling, gap, isNextDay, + dayOffset, + totalGap, actionHandler, } = props; const { selectedEventId, setSelectedEventId, clearSelectedEventId } = useEventIdSwapping(); @@ -303,6 +307,9 @@ export default function EventBlock(props: EventBlockProps) { loaded={loaded} playback={playback} isRolling={isRolling} + dayOffset={dayOffset} + isPast={isPast} + totalGap={totalGap} /> )} diff --git a/apps/client/src/features/rundown/event-block/EventBlockInner.tsx b/apps/client/src/features/rundown/event-block/EventBlockInner.tsx index 6d64376fa..a59450486 100644 --- a/apps/client/src/features/rundown/event-block/EventBlockInner.tsx +++ b/apps/client/src/features/rundown/event-block/EventBlockInner.tsx @@ -11,12 +11,14 @@ import { IoPlaySkipForward } from '@react-icons/all-files/io5/IoPlaySkipForward' import { IoStop } from '@react-icons/all-files/io5/IoStop'; import { IoTime } from '@react-icons/all-files/io5/IoTime'; import { EndAction, MaybeString, Playback, TimerType, TimeStrategy } from 'ontime-types'; +import { dayInMs } from 'ontime-utils'; import { cx } from '../../../common/utils/styleUtils'; import { tooltipDelayMid } from '../../../ontimeConfig'; import EditableBlockTitle from '../common/EditableBlockTitle'; import TimeInputFlow from '../time-input-flow/TimeInputFlow'; +import EventBlockChip from './composite/EventBlockChip'; import EventBlockPlayback from './composite/EventBlockPlayback'; import EventBlockProgressBar from './composite/EventBlockProgressBar'; @@ -42,6 +44,9 @@ interface EventBlockInnerProps { loaded: boolean; playback?: Playback; isRolling: boolean; + dayOffset: number; + isPast: boolean; + totalGap: number; } function EventBlockInner(props: EventBlockInnerProps) { @@ -64,6 +69,9 @@ function EventBlockInner(props: EventBlockInnerProps) { loaded, playback, isRolling, + dayOffset, + isPast, + totalGap, } = props; const [renderInner, setRenderInner] = useState(false); @@ -108,6 +116,17 @@ function EventBlockInner(props: EventBlockInnerProps) { loaded={loaded} disablePlayback={skip || isRolling} /> + {!skip && ( + + )}
{note}
diff --git a/apps/client/src/features/rundown/event-block/composite/EventBlockChip.module.scss b/apps/client/src/features/rundown/event-block/composite/EventBlockChip.module.scss new file mode 100644 index 000000000..b69696431 --- /dev/null +++ b/apps/client/src/features/rundown/event-block/composite/EventBlockChip.module.scss @@ -0,0 +1,21 @@ +.chip { + background-color: $gray-1100; + white-space: nowrap; + + font-size: calc(1rem - 3px); + color: $label-gray; + padding: 0.125rem 0.5rem; + border-radius: 2px; + + &.over { + color: $playback-negative; + } + + &.under { + color: $playback-ahead; + } + + &.due { + color: $warning-orange; + } +} diff --git a/apps/client/src/features/rundown/event-block/composite/EventBlockChip.tsx b/apps/client/src/features/rundown/event-block/composite/EventBlockChip.tsx new file mode 100644 index 000000000..7a6857cbb --- /dev/null +++ b/apps/client/src/features/rundown/event-block/composite/EventBlockChip.tsx @@ -0,0 +1,77 @@ +import { useMemo } from 'react'; +import { Tooltip } from '@chakra-ui/react'; +import { isPlaybackActive, MILLIS_PER_MINUTE, MILLIS_PER_SECOND } from 'ontime-utils'; + +import { usePlayback, useTimelineStatus } from '../../../../common/hooks/useSocket'; +import { cx } from '../../../../common/utils/styleUtils'; +import { formatDuration } from '../../../../common/utils/time'; +import { tooltipDelayFast } from '../../../../ontimeConfig'; + +import style from './EventBlockChip.module.scss'; + +interface EventBlockChipProps { + id: string; + trueTimeStart: number; + isPast: boolean; + isLoaded: boolean; + className: string; + totalGap: number; + isLinkedAndNext: boolean; +} + +export default function EventBlockChip(props: EventBlockChipProps) { + const { trueTimeStart, isPast, isLoaded, className, totalGap, isLinkedAndNext } = props; + const { playback } = usePlayback(); + + if (isLoaded) { + return null; //TODO: the is a small flash of 'DUE' on the loaded event as clock data arrives before isLoaded propagates + } + + const playbackActive = isPlaybackActive(playback); + + if (!playbackActive || isPast) { + return null; //TODO: Event report will go here + } + + if (playbackActive) { + // we extracted the component to avoid unnecessary calculations and re-renders + return ( + + ); + } + + return null; +} + +interface EventUntilProps { + className: string; + trueTimeStart: number; + totalGap: number; + isLinkedAndNext: boolean; +} + +function EventUntil(props: EventUntilProps) { + const { trueTimeStart, className, totalGap, isLinkedAndNext } = props; + const { clock, offset } = useTimelineStatus(); + + const [timeUntilString, isDue] = useMemo(() => { + const consumedOffset = isLinkedAndNext ? offset : Math.min(offset + totalGap, 0); + const offsetTimestart = trueTimeStart - consumedOffset; + const timeUntil = offsetTimestart - clock; + const isDue = timeUntil < MILLIS_PER_SECOND; + return [isDue ? 'DUE' : `${formatDuration(Math.abs(timeUntil), timeUntil > 2 * MILLIS_PER_MINUTE)}`, isDue]; + }, [totalGap, isLinkedAndNext, offset, trueTimeStart, clock]); + + return ( + +
+
{timeUntilString}
+
+
+ ); +} diff --git a/apps/client/src/theme/_ontimeStyles.scss b/apps/client/src/theme/_ontimeStyles.scss index efaea7b83..6254e8534 100644 --- a/apps/client/src/theme/_ontimeStyles.scss +++ b/apps/client/src/theme/_ontimeStyles.scss @@ -25,6 +25,7 @@ $ontime-delay-text: #E69056; $ontime-paused: #c05621; $ontime-stop: #E4281E; $playback-negative: $red-500; +$playback-ahead: $green-500; $active-indicator: #8bb33d; $text-black: $gray-1350;