mirror of
https://github.com/cpvalente/ontime.git
synced 2026-08-19 14:14:17 +00:00
Time until (#1497)
* 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> * fix: account for running day * refactor: improve composition * add partial test --------- Co-authored-by: Alex Christoffer Rasmussen <ac@omnivox.dk>
This commit is contained in:
@@ -7,6 +7,7 @@ import {
|
||||
isOntimeEvent,
|
||||
isPlayableEvent,
|
||||
MaybeString,
|
||||
OntimeEvent,
|
||||
PlayableEvent,
|
||||
Playback,
|
||||
RundownCached,
|
||||
@@ -269,8 +270,9 @@ 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;
|
||||
|
||||
let currentDay = 0;
|
||||
return (
|
||||
<div className={style.rundownContainer} ref={scrollRef} data-testid='rundown'>
|
||||
<DndContext onDragEnd={handleOnDragEnd} sensors={sensors} collisionDetection={closestCenter}>
|
||||
@@ -297,6 +299,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;
|
||||
@@ -310,6 +313,7 @@ export default function Rundown({ data }: RundownProps) {
|
||||
const hasCursor = entry.id === cursor;
|
||||
if (isLoaded) {
|
||||
isPast = false;
|
||||
currentDay = (entry as OntimeEvent).dayOffset;
|
||||
}
|
||||
|
||||
return (
|
||||
@@ -331,6 +335,8 @@ export default function Rundown({ data }: RundownProps) {
|
||||
playback={isLoaded ? featureData.playback : undefined}
|
||||
isRolling={featureData.playback === Playback.Roll}
|
||||
isNextDay={isNextDay}
|
||||
totalGap={totalGap}
|
||||
currentDay={currentDay}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -37,6 +37,8 @@ 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;
|
||||
currentDay: number;
|
||||
}
|
||||
|
||||
export default function RundownEntry(props: RundownEntryProps) {
|
||||
@@ -52,6 +54,8 @@ export default function RundownEntry(props: RundownEntryProps) {
|
||||
isRolling,
|
||||
eventIndex,
|
||||
isNextDay,
|
||||
totalGap,
|
||||
currentDay,
|
||||
} = props;
|
||||
const { emitError } = useEmitLog();
|
||||
const { addEvent, updateEvent, batchUpdateEvents, deleteEvent, swapEvents } = useEventAction();
|
||||
@@ -173,6 +177,8 @@ export default function RundownEntry(props: RundownEntryProps) {
|
||||
isRolling={isRolling}
|
||||
gap={data.gap}
|
||||
isNextDay={isNextDay}
|
||||
dayOffset={data.dayOffset - currentDay}
|
||||
totalGap={totalGap}
|
||||
actionHandler={actionHandler}
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -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 && (
|
||||
<EventBlockChip
|
||||
className={style.chipSection}
|
||||
id={eventId}
|
||||
trueTimeStart={timeStart + dayOffset * dayInMs}
|
||||
isPast={isPast}
|
||||
isLoaded={loaded}
|
||||
totalGap={totalGap}
|
||||
isLinkedAndNext={isNext && linkStart !== null}
|
||||
/>
|
||||
)}
|
||||
<div className={style.statusElements} id='block-status' data-ispublic={isPublic}>
|
||||
<span className={style.eventNote}>{note}</span>
|
||||
<div className={loaded ? style.progressBg : `${style.progressBg} ${style.hidden}`}>
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
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 (
|
||||
<Tooltip label='Expected time until start' openDelay={tooltipDelayFast}>
|
||||
<div className={className}>
|
||||
<EventUntil trueTimeStart={trueTimeStart} totalGap={totalGap} isLinkedAndNext={isLinkedAndNext} />
|
||||
</div>
|
||||
</Tooltip>
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
interface EventUntilProps {
|
||||
trueTimeStart: number;
|
||||
totalGap: number;
|
||||
isLinkedAndNext: boolean;
|
||||
}
|
||||
|
||||
function EventUntil(props: EventUntilProps) {
|
||||
const { trueTimeStart, totalGap, isLinkedAndNext } = props;
|
||||
const { clock, offset } = useTimelineStatus();
|
||||
|
||||
const consumedOffset = isLinkedAndNext ? offset : Math.min(offset + totalGap, 0);
|
||||
const offsetTimestart = trueTimeStart - consumedOffset;
|
||||
const timeUntil = offsetTimestart - clock;
|
||||
const isDue = timeUntil < MILLIS_PER_SECOND;
|
||||
|
||||
const timeUntilString = isDue ? 'DUE' : `${formatDuration(Math.abs(timeUntil), timeUntil > 2 * MILLIS_PER_MINUTE)}`;
|
||||
|
||||
return <div className={cx([style.chip, isDue && style.due])}>{timeUntilString}</div>;
|
||||
}
|
||||
Reference in New Issue
Block a user