mirror of
https://github.com/cpvalente/ontime.git
synced 2026-09-10 16:49:41 +00:00
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>
This commit is contained in:
committed by
GitHub
parent
5d5b48aca6
commit
221ff7b3af
@@ -250,3 +250,11 @@ export const useIsOnline = () => {
|
|||||||
|
|
||||||
return useRuntimeStore(featureSelector);
|
return useRuntimeStore(featureSelector);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const usePlayback = () => {
|
||||||
|
const featureSelector = (state: RuntimeStore) => ({
|
||||||
|
playback: state.timer.playback,
|
||||||
|
});
|
||||||
|
|
||||||
|
return useRuntimeStore(featureSelector);
|
||||||
|
};
|
||||||
|
|||||||
@@ -269,6 +269,7 @@ export default function Rundown({ data }: RundownProps) {
|
|||||||
// all events before the current selected are in the past
|
// all events before the current selected are in the past
|
||||||
let isPast = Boolean(featureData?.selectedEventId);
|
let isPast = Boolean(featureData?.selectedEventId);
|
||||||
let isNextDay = false;
|
let isNextDay = false;
|
||||||
|
let totalGap = 0;
|
||||||
const isEditMode = appMode === AppMode.Edit;
|
const isEditMode = appMode === AppMode.Edit;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -297,6 +298,7 @@ export default function Rundown({ data }: RundownProps) {
|
|||||||
|
|
||||||
if (isPlayableEvent(entry)) {
|
if (isPlayableEvent(entry)) {
|
||||||
isNextDay = checkIsNextDay(entry, lastEvent);
|
isNextDay = checkIsNextDay(entry, lastEvent);
|
||||||
|
totalGap += !isPast ? entry.gap : 0;
|
||||||
if (isNewLatest(entry, lastEvent)) {
|
if (isNewLatest(entry, lastEvent)) {
|
||||||
// populate previous entry
|
// populate previous entry
|
||||||
thisEvent = entry;
|
thisEvent = entry;
|
||||||
@@ -331,6 +333,7 @@ export default function Rundown({ data }: RundownProps) {
|
|||||||
playback={isLoaded ? featureData.playback : undefined}
|
playback={isLoaded ? featureData.playback : undefined}
|
||||||
isRolling={featureData.playback === Playback.Roll}
|
isRolling={featureData.playback === Playback.Roll}
|
||||||
isNextDay={isNextDay}
|
isNextDay={isNextDay}
|
||||||
|
totalGap={totalGap}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -37,6 +37,7 @@ interface RundownEntryProps {
|
|||||||
previousEventId?: string;
|
previousEventId?: string;
|
||||||
playback?: Playback; // we only care about this if this event is playing
|
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
|
isRolling: boolean; // we need to know even if not related to this event
|
||||||
|
totalGap: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function RundownEntry(props: RundownEntryProps) {
|
export default function RundownEntry(props: RundownEntryProps) {
|
||||||
@@ -52,6 +53,7 @@ export default function RundownEntry(props: RundownEntryProps) {
|
|||||||
isRolling,
|
isRolling,
|
||||||
eventIndex,
|
eventIndex,
|
||||||
isNextDay,
|
isNextDay,
|
||||||
|
totalGap,
|
||||||
} = props;
|
} = props;
|
||||||
const { emitError } = useEmitLog();
|
const { emitError } = useEmitLog();
|
||||||
const { addEvent, updateEvent, batchUpdateEvents, deleteEvent, swapEvents } = useEventAction();
|
const { addEvent, updateEvent, batchUpdateEvents, deleteEvent, swapEvents } = useEventAction();
|
||||||
@@ -173,6 +175,8 @@ export default function RundownEntry(props: RundownEntryProps) {
|
|||||||
isRolling={isRolling}
|
isRolling={isRolling}
|
||||||
gap={data.gap}
|
gap={data.gap}
|
||||||
isNextDay={isNextDay}
|
isNextDay={isNextDay}
|
||||||
|
dayOffset={data.dayOffset}
|
||||||
|
totalGap={totalGap}
|
||||||
actionHandler={actionHandler}
|
actionHandler={actionHandler}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ $skip-opacity: 0.2;
|
|||||||
display: grid;
|
display: grid;
|
||||||
grid-template-areas:
|
grid-template-areas:
|
||||||
'binder ... ... ...'
|
'binder ... ... ...'
|
||||||
'binder pb-actions times ...'
|
'binder pb-actions times chip'
|
||||||
'binder pb-actions title title'
|
'binder pb-actions title title'
|
||||||
'binder pb-actions estatus estatus'
|
'binder pb-actions estatus estatus'
|
||||||
'binder ... ... ...';
|
'binder ... ... ...';
|
||||||
@@ -131,6 +131,10 @@ $skip-opacity: 0.2;
|
|||||||
height: 100%;
|
height: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.chipSection {
|
||||||
|
grid-area: chip;
|
||||||
|
}
|
||||||
|
|
||||||
.titleSection {
|
.titleSection {
|
||||||
grid-area: title;
|
grid-area: title;
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|||||||
@@ -49,6 +49,8 @@ interface EventBlockProps {
|
|||||||
isRolling: boolean;
|
isRolling: boolean;
|
||||||
gap: number;
|
gap: number;
|
||||||
isNextDay: boolean;
|
isNextDay: boolean;
|
||||||
|
dayOffset: number;
|
||||||
|
totalGap: number;
|
||||||
actionHandler: (
|
actionHandler: (
|
||||||
action: EventItemActions,
|
action: EventItemActions,
|
||||||
payload?:
|
payload?:
|
||||||
@@ -87,6 +89,8 @@ export default function EventBlock(props: EventBlockProps) {
|
|||||||
isRolling,
|
isRolling,
|
||||||
gap,
|
gap,
|
||||||
isNextDay,
|
isNextDay,
|
||||||
|
dayOffset,
|
||||||
|
totalGap,
|
||||||
actionHandler,
|
actionHandler,
|
||||||
} = props;
|
} = props;
|
||||||
const { selectedEventId, setSelectedEventId, clearSelectedEventId } = useEventIdSwapping();
|
const { selectedEventId, setSelectedEventId, clearSelectedEventId } = useEventIdSwapping();
|
||||||
@@ -303,6 +307,9 @@ export default function EventBlock(props: EventBlockProps) {
|
|||||||
loaded={loaded}
|
loaded={loaded}
|
||||||
playback={playback}
|
playback={playback}
|
||||||
isRolling={isRolling}
|
isRolling={isRolling}
|
||||||
|
dayOffset={dayOffset}
|
||||||
|
isPast={isPast}
|
||||||
|
totalGap={totalGap}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -11,12 +11,14 @@ import { IoPlaySkipForward } from '@react-icons/all-files/io5/IoPlaySkipForward'
|
|||||||
import { IoStop } from '@react-icons/all-files/io5/IoStop';
|
import { IoStop } from '@react-icons/all-files/io5/IoStop';
|
||||||
import { IoTime } from '@react-icons/all-files/io5/IoTime';
|
import { IoTime } from '@react-icons/all-files/io5/IoTime';
|
||||||
import { EndAction, MaybeString, Playback, TimerType, TimeStrategy } from 'ontime-types';
|
import { EndAction, MaybeString, Playback, TimerType, TimeStrategy } from 'ontime-types';
|
||||||
|
import { dayInMs } from 'ontime-utils';
|
||||||
|
|
||||||
import { cx } from '../../../common/utils/styleUtils';
|
import { cx } from '../../../common/utils/styleUtils';
|
||||||
import { tooltipDelayMid } from '../../../ontimeConfig';
|
import { tooltipDelayMid } from '../../../ontimeConfig';
|
||||||
import EditableBlockTitle from '../common/EditableBlockTitle';
|
import EditableBlockTitle from '../common/EditableBlockTitle';
|
||||||
import TimeInputFlow from '../time-input-flow/TimeInputFlow';
|
import TimeInputFlow from '../time-input-flow/TimeInputFlow';
|
||||||
|
|
||||||
|
import EventBlockChip from './composite/EventBlockChip';
|
||||||
import EventBlockPlayback from './composite/EventBlockPlayback';
|
import EventBlockPlayback from './composite/EventBlockPlayback';
|
||||||
import EventBlockProgressBar from './composite/EventBlockProgressBar';
|
import EventBlockProgressBar from './composite/EventBlockProgressBar';
|
||||||
|
|
||||||
@@ -42,6 +44,9 @@ interface EventBlockInnerProps {
|
|||||||
loaded: boolean;
|
loaded: boolean;
|
||||||
playback?: Playback;
|
playback?: Playback;
|
||||||
isRolling: boolean;
|
isRolling: boolean;
|
||||||
|
dayOffset: number;
|
||||||
|
isPast: boolean;
|
||||||
|
totalGap: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
function EventBlockInner(props: EventBlockInnerProps) {
|
function EventBlockInner(props: EventBlockInnerProps) {
|
||||||
@@ -64,6 +69,9 @@ function EventBlockInner(props: EventBlockInnerProps) {
|
|||||||
loaded,
|
loaded,
|
||||||
playback,
|
playback,
|
||||||
isRolling,
|
isRolling,
|
||||||
|
dayOffset,
|
||||||
|
isPast,
|
||||||
|
totalGap,
|
||||||
} = props;
|
} = props;
|
||||||
|
|
||||||
const [renderInner, setRenderInner] = useState(false);
|
const [renderInner, setRenderInner] = useState(false);
|
||||||
@@ -108,6 +116,17 @@ function EventBlockInner(props: EventBlockInnerProps) {
|
|||||||
loaded={loaded}
|
loaded={loaded}
|
||||||
disablePlayback={skip || isRolling}
|
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}>
|
<div className={style.statusElements} id='block-status' data-ispublic={isPublic}>
|
||||||
<span className={style.eventNote}>{note}</span>
|
<span className={style.eventNote}>{note}</span>
|
||||||
<div className={loaded ? style.progressBg : `${style.progressBg} ${style.hidden}`}>
|
<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,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 (
|
||||||
|
<EventUntil
|
||||||
|
className={className}
|
||||||
|
trueTimeStart={trueTimeStart}
|
||||||
|
totalGap={totalGap}
|
||||||
|
isLinkedAndNext={isLinkedAndNext}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
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 (
|
||||||
|
<Tooltip label='Expected time until start' openDelay={tooltipDelayFast}>
|
||||||
|
<div className={cx([style.chip, isDue ? style.due : null, className])}>
|
||||||
|
<div>{timeUntilString}</div>
|
||||||
|
</div>
|
||||||
|
</Tooltip>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -25,6 +25,7 @@ $ontime-delay-text: #E69056;
|
|||||||
$ontime-paused: #c05621;
|
$ontime-paused: #c05621;
|
||||||
$ontime-stop: #E4281E;
|
$ontime-stop: #E4281E;
|
||||||
$playback-negative: $red-500;
|
$playback-negative: $red-500;
|
||||||
|
$playback-ahead: $green-500;
|
||||||
$active-indicator: #8bb33d;
|
$active-indicator: #8bb33d;
|
||||||
$text-black: $gray-1350;
|
$text-black: $gray-1350;
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user