mirror of
https://github.com/cpvalente/ontime.git
synced 2026-08-15 20:33:47 +00:00
feat: time to next flag
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
.label {
|
||||
display: block;
|
||||
margin-top: 2rem;
|
||||
margin-top: 1rem;
|
||||
font-size: $inner-section-text-size;
|
||||
color: $label-gray;
|
||||
}
|
||||
|
||||
@@ -1,19 +1,10 @@
|
||||
import { memo, PropsWithChildren, useMemo } from 'react';
|
||||
import { memo, PropsWithChildren } from 'react';
|
||||
|
||||
import { useIsMobileScreen } from '../../common/hooks/useIsMobileScreen';
|
||||
import { useRuntimeOverview } from '../../common/hooks/useSocket';
|
||||
|
||||
import {
|
||||
ClockOverview,
|
||||
CurrentBlockOverview,
|
||||
OverviewWrapper,
|
||||
RuntimeOverview,
|
||||
TimerOverview,
|
||||
} from './composite/OverviewWrapper';
|
||||
import { TimeRow } from './composite/TimeLayout';
|
||||
import { calculateEndAndDaySpan, formatedTime } from './overviewUtils';
|
||||
|
||||
import style from './Overview.module.scss';
|
||||
import { ClockOverview, MetadataTimes, RuntimeOverview, StartTimes, TimerOverview } from './composite/TimeElements';
|
||||
import TitleOverview from './composite/TitleOverview';
|
||||
import { OverviewWrapper } from './OverviewWrapper';
|
||||
|
||||
export default memo(CuesheetOverview);
|
||||
function CuesheetOverview({ children }: PropsWithChildren) {
|
||||
@@ -33,49 +24,14 @@ function CuesheetMobile({ children }: PropsWithChildren) {
|
||||
}
|
||||
|
||||
function CuesheetDesktop({ children }: PropsWithChildren) {
|
||||
const { plannedEnd, plannedStart, actualStart, expectedEnd } = useRuntimeOverview();
|
||||
|
||||
const [maybePlannedEnd, maybePlannedDaySpan] = useMemo(() => calculateEndAndDaySpan(plannedEnd), [plannedEnd]);
|
||||
const plannedEndText = formatedTime(maybePlannedEnd);
|
||||
|
||||
const [maybeExpectedEnd, maybeExpectedDaySpan] = useMemo(() => calculateEndAndDaySpan(expectedEnd), [expectedEnd]);
|
||||
const expectedEndText = formatedTime(maybeExpectedEnd);
|
||||
return (
|
||||
<OverviewWrapper navElements={children}>
|
||||
<div>
|
||||
<TimeRow
|
||||
label='Planned start'
|
||||
value={formatedTime(plannedStart)}
|
||||
className={style.start}
|
||||
muted={plannedStart === null}
|
||||
/>
|
||||
<TimeRow
|
||||
label='Actual start'
|
||||
value={formatedTime(actualStart)}
|
||||
className={style.start}
|
||||
muted={actualStart === null}
|
||||
/>
|
||||
</div>
|
||||
<TitleOverview />
|
||||
<StartTimes />
|
||||
<TimerOverview />
|
||||
<RuntimeOverview />
|
||||
<CurrentBlockOverview />
|
||||
<MetadataTimes />
|
||||
<ClockOverview />
|
||||
<div>
|
||||
<TimeRow
|
||||
label='Planned end'
|
||||
value={plannedEndText}
|
||||
className={style.end}
|
||||
daySpan={maybePlannedDaySpan}
|
||||
muted={maybePlannedEnd === null}
|
||||
/>
|
||||
<TimeRow
|
||||
label='Expected end'
|
||||
value={expectedEndText}
|
||||
className={style.end}
|
||||
daySpan={maybeExpectedDaySpan}
|
||||
muted={maybeExpectedEnd === null}
|
||||
/>
|
||||
</div>
|
||||
</OverviewWrapper>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,67 +1,19 @@
|
||||
import { memo, PropsWithChildren, useMemo } from 'react';
|
||||
import { memo, PropsWithChildren } from 'react';
|
||||
|
||||
import { useRuntimeOverview } from '../../common/hooks/useSocket';
|
||||
|
||||
import {
|
||||
ClockOverview,
|
||||
CurrentBlockOverview,
|
||||
OverviewWrapper,
|
||||
ProgressOverview,
|
||||
RuntimeOverview,
|
||||
TitlesOverview,
|
||||
} from './composite/OverviewWrapper';
|
||||
import { TimeRow } from './composite/TimeLayout';
|
||||
import { calculateEndAndDaySpan, formatedTime } from './overviewUtils';
|
||||
|
||||
import style from './Overview.module.scss';
|
||||
import { ClockOverview, MetadataTimes, ProgressOverview, RuntimeOverview, StartTimes } from './composite/TimeElements';
|
||||
import TitleOverview from './composite/TitleOverview';
|
||||
import { OverviewWrapper } from './OverviewWrapper';
|
||||
|
||||
export default memo(EditorOverview);
|
||||
function EditorOverview({ children }: PropsWithChildren) {
|
||||
const { plannedEnd, plannedStart, actualStart, expectedEnd } = useRuntimeOverview();
|
||||
|
||||
const [maybePlannedEnd, maybePlannedDaySpan] = useMemo(() => calculateEndAndDaySpan(plannedEnd), [plannedEnd]);
|
||||
const plannedEndText = formatedTime(maybePlannedEnd);
|
||||
|
||||
const [maybeExpectedEnd, maybeExpectedDaySpan] = useMemo(() => calculateEndAndDaySpan(expectedEnd), [expectedEnd]);
|
||||
const expectedEndText = formatedTime(maybeExpectedEnd);
|
||||
|
||||
return (
|
||||
<OverviewWrapper navElements={children}>
|
||||
<TitlesOverview />
|
||||
<div>
|
||||
<TimeRow
|
||||
label='Planned start'
|
||||
value={formatedTime(plannedStart)}
|
||||
className={style.start}
|
||||
muted={plannedStart === null}
|
||||
/>
|
||||
<TimeRow
|
||||
label='Actual start'
|
||||
value={formatedTime(actualStart)}
|
||||
className={style.start}
|
||||
muted={actualStart === null}
|
||||
/>
|
||||
</div>
|
||||
<TitleOverview />
|
||||
<StartTimes />
|
||||
<ProgressOverview />
|
||||
<RuntimeOverview />
|
||||
<CurrentBlockOverview />
|
||||
<MetadataTimes />
|
||||
<ClockOverview />
|
||||
<div>
|
||||
<TimeRow
|
||||
label='Planned end'
|
||||
value={plannedEndText}
|
||||
className={style.end}
|
||||
daySpan={maybePlannedDaySpan}
|
||||
muted={maybePlannedEnd === null}
|
||||
/>
|
||||
<TimeRow
|
||||
label='Expected end'
|
||||
value={expectedEndText}
|
||||
className={style.end}
|
||||
daySpan={maybeExpectedDaySpan}
|
||||
muted={maybeExpectedEnd === null}
|
||||
/>
|
||||
</div>
|
||||
</OverviewWrapper>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
grid-area: overview;
|
||||
font-size: $inner-section-text-size;
|
||||
display: flex;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.isOffline {
|
||||
@@ -23,36 +24,14 @@
|
||||
|
||||
.nav {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.info {
|
||||
flex: 1;
|
||||
padding-inline: 1rem;
|
||||
padding-left: 1rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.title {
|
||||
font-size: 1.5rem;
|
||||
@include ellipsis-overflow;
|
||||
}
|
||||
|
||||
.description {
|
||||
font-size: 1rem;
|
||||
color: $label-gray;
|
||||
@include ellipsis-overflow;
|
||||
}
|
||||
|
||||
.offset {
|
||||
color: $muted-gray;
|
||||
}
|
||||
|
||||
.ahead {
|
||||
color: $playback-ahead;
|
||||
}
|
||||
|
||||
.behind {
|
||||
color: $ontime-delay-text;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
import { PropsWithChildren, ReactNode } from 'react';
|
||||
import { ErrorBoundary } from '@sentry/react';
|
||||
|
||||
import { useIsOnline } from '../../common/hooks/useSocket';
|
||||
import { cx } from '../../common/utils/styleUtils';
|
||||
|
||||
import style from './Overview.module.scss';
|
||||
|
||||
interface OverviewWrapperProps {
|
||||
navElements: ReactNode;
|
||||
}
|
||||
|
||||
export function OverviewWrapper({ navElements, children }: PropsWithChildren<OverviewWrapperProps>) {
|
||||
const { isOnline } = useIsOnline();
|
||||
return (
|
||||
<div className={cx([style.overview, !isOnline && style.isOffline])}>
|
||||
<ErrorBoundary>
|
||||
<div className={style.nav}>{navElements}</div>
|
||||
<div className={style.info}>{children}</div>
|
||||
</ErrorBoundary>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
import { dayInMs } from 'ontime-utils';
|
||||
|
||||
import { calculateEndAndDaySpan } from '../overviewUtils';
|
||||
import { calculateEndAndDaySpan } from '../overview.utils';
|
||||
|
||||
describe('calculateEndAndDaySpan', () => {
|
||||
it('should return [null, 0] when end is null', () => {
|
||||
|
||||
@@ -1,146 +0,0 @@
|
||||
import { PropsWithChildren, ReactNode } from 'react';
|
||||
import { ErrorBoundary } from '@sentry/react';
|
||||
import { isOntimeBlock, TimerType } from 'ontime-types';
|
||||
import { isPlaybackActive, millisToString } from 'ontime-utils';
|
||||
|
||||
import Tooltip from '../../../common/components/tooltip/Tooltip';
|
||||
import {
|
||||
useClock,
|
||||
useCurrentBlockId,
|
||||
useIsOnline,
|
||||
useRuntimePlaybackOverview,
|
||||
useTimer,
|
||||
} from '../../../common/hooks/useSocket';
|
||||
import useProjectData from '../../../common/hooks-query/useProjectData';
|
||||
import { useEntry } from '../../../common/hooks-query/useRundown';
|
||||
import { cx, enDash, timerPlaceholder } from '../../../common/utils/styleUtils';
|
||||
import { formatedTime, getOffsetText } from '../overviewUtils';
|
||||
|
||||
import { TimeColumn, TimeRow } from './TimeLayout';
|
||||
|
||||
import style from '../Overview.module.scss';
|
||||
|
||||
interface OverviewWrapperProps {
|
||||
navElements: ReactNode;
|
||||
}
|
||||
|
||||
export function OverviewWrapper({ navElements, children }: PropsWithChildren<OverviewWrapperProps>) {
|
||||
const { isOnline } = useIsOnline();
|
||||
return (
|
||||
<div className={cx([style.overview, !isOnline && style.isOffline])}>
|
||||
<ErrorBoundary>
|
||||
<div className={style.nav}>{navElements}</div>
|
||||
<div className={style.info}>{children}</div>
|
||||
</ErrorBoundary>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function TitlesOverview() {
|
||||
const { data } = useProjectData();
|
||||
|
||||
if (!data.title && !data.description) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className={style.title}>{data.title}</div>
|
||||
<div className={style.description}>{data.description}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function CurrentBlockOverview() {
|
||||
const { blockStartedAt, clock, blockExpectedEnd } = useRuntimePlaybackOverview();
|
||||
const { currentBlockId } = useCurrentBlockId();
|
||||
const entry = useEntry(currentBlockId);
|
||||
|
||||
const timeInBlock = formatedTime(blockStartedAt ? clock - blockStartedAt : null, 3, TimerType.CountUp);
|
||||
const blockExpectedEndString = formatedTime(blockExpectedEnd, 3, TimerType.CountUp);
|
||||
|
||||
const remainingBlockDuration = (() => {
|
||||
if (blockStartedAt === null || !entry) return timerPlaceholder;
|
||||
if (!isOntimeBlock(entry)) return timerPlaceholder;
|
||||
return formatedTime(blockStartedAt + entry.duration - clock, 3, TimerType.CountDown);
|
||||
})();
|
||||
|
||||
const timeUntilBlockEnd = (() => {
|
||||
if (blockExpectedEnd === null) return timerPlaceholder;
|
||||
return formatedTime(blockExpectedEnd - clock, 3, TimerType.CountDown);
|
||||
})() ;
|
||||
|
||||
return (
|
||||
<>
|
||||
<div>
|
||||
<Tooltip text='How long the group has been active'>
|
||||
<TimeRow
|
||||
label='Elapsed in group'
|
||||
value={timeInBlock}
|
||||
className={style.clock}
|
||||
muted={blockStartedAt === null}
|
||||
/>
|
||||
</Tooltip>
|
||||
<Tooltip text='Remaining time until the planed group duration is up'>
|
||||
<TimeRow
|
||||
label='Remaining group duration'
|
||||
value={remainingBlockDuration}
|
||||
className={style.clock}
|
||||
muted={blockStartedAt === null}
|
||||
/>
|
||||
</Tooltip>
|
||||
</div>
|
||||
<div>
|
||||
<Tooltip text='Expected time until the group can end, if everything ends on time from now on'>
|
||||
<TimeRow
|
||||
label='Expected time until group end'
|
||||
value={timeUntilBlockEnd}
|
||||
className={style.end}
|
||||
muted={blockStartedAt === null}
|
||||
/>
|
||||
</Tooltip>
|
||||
<Tooltip text='Expected time the group will end, if everything ends on time from now on'>
|
||||
<TimeRow
|
||||
label='Expected group end'
|
||||
value={blockExpectedEndString}
|
||||
className={style.end}
|
||||
muted={blockStartedAt === null}
|
||||
/>
|
||||
</Tooltip>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export function TimerOverview() {
|
||||
const { current } = useTimer();
|
||||
|
||||
const display = millisToString(current, { fallback: timerPlaceholder });
|
||||
|
||||
return <TimeColumn label='Running timer' value={display} muted={current === null} />;
|
||||
}
|
||||
|
||||
export function ProgressOverview() {
|
||||
const { numEvents, selectedEventIndex } = useRuntimePlaybackOverview();
|
||||
|
||||
const current = selectedEventIndex !== null ? selectedEventIndex + 1 : enDash;
|
||||
const progressText = numEvents ? `${current} of ${numEvents || enDash}` : enDash;
|
||||
|
||||
return <TimeColumn label='Progress' value={progressText} />;
|
||||
}
|
||||
|
||||
export function RuntimeOverview() {
|
||||
const { offset, playback } = useRuntimePlaybackOverview();
|
||||
|
||||
const isPlaying = isPlaybackActive(playback);
|
||||
const offsetText = getOffsetText(isPlaying ? offset : null);
|
||||
const offsetClasses = cx([style.offset, isPlaying && (offset < 0 ? style.behind : style.ahead)]);
|
||||
|
||||
return <TimeColumn label='Offset' value={offsetText} className={offsetClasses} testId='offset' />;
|
||||
}
|
||||
|
||||
export function ClockOverview() {
|
||||
const { clock } = useClock();
|
||||
|
||||
return <TimeColumn label='Time now' value={formatedTime(clock)} />;
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
.column {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: end;
|
||||
}
|
||||
|
||||
.row {
|
||||
display: grid;
|
||||
grid-template-columns: 3rem 10rem 8rem;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.metadataRow {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(3rem, 10rem) 8rem 8rem;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.labelledElement {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.25rem;
|
||||
}
|
||||
|
||||
.icon {
|
||||
font-size: 1rem;
|
||||
color: $label-gray;
|
||||
}
|
||||
|
||||
.label {
|
||||
color: $label-gray;
|
||||
font-size: calc(1rem - 2px);
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.time {
|
||||
font-size: 1.25rem;
|
||||
letter-spacing: 0.5px;
|
||||
font-variant-numeric: tabular-nums;
|
||||
line-height: 1.1;
|
||||
}
|
||||
|
||||
.daySpan {
|
||||
&::after {
|
||||
content: '*';
|
||||
vertical-align: super;
|
||||
font-size: 0.75em;
|
||||
color: $info-blue;
|
||||
}
|
||||
}
|
||||
|
||||
.muted {
|
||||
color: $muted-gray;
|
||||
}
|
||||
|
||||
.offset {
|
||||
color: $muted-gray;
|
||||
}
|
||||
|
||||
.ahead {
|
||||
color: $playback-ahead;
|
||||
}
|
||||
|
||||
.behind {
|
||||
color: $ontime-delay-text;
|
||||
}
|
||||
|
||||
.labelTitle {
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
|
||||
color: $active-indicator;
|
||||
font-size: calc(1rem - 2px);
|
||||
text-align: right;
|
||||
}
|
||||
@@ -0,0 +1,218 @@
|
||||
import { useMemo } from 'react';
|
||||
import { TbCalendar, TbCalendarClock, TbCalendarDown, TbCalendarStar, TbFlagDown, TbFlagStar } from 'react-icons/tb';
|
||||
import { isOntimeBlock, OntimeBlock, OntimeEvent, TimerType } from 'ontime-types';
|
||||
import { isPlaybackActive, millisToString } from 'ontime-utils';
|
||||
|
||||
import Tooltip from '../../../common/components/tooltip/Tooltip';
|
||||
import {
|
||||
useClock,
|
||||
useCurrentBlockId,
|
||||
useNextFlag,
|
||||
useRuntimeOverview,
|
||||
useRuntimePlaybackOverview,
|
||||
useTimer,
|
||||
} from '../../../common/hooks/useSocket';
|
||||
import { useEntry } from '../../../common/hooks-query/useRundown';
|
||||
import { cx, enDash, timerPlaceholder } from '../../../common/utils/styleUtils';
|
||||
import { formatTime, useTimeUntilStart } from '../../../common/utils/time';
|
||||
import { calculateEndAndDaySpan, formattedTime, getOffsetText } from '../overview.utils';
|
||||
|
||||
import { TimeColumn } from './TimeLayout';
|
||||
|
||||
import style from './TimeElements.module.scss';
|
||||
|
||||
export function StartTimes() {
|
||||
const { plannedEnd, plannedStart, actualStart, expectedEnd } = useRuntimeOverview();
|
||||
const [maybePlannedEnd, maybePlannedDaySpan] = useMemo(() => calculateEndAndDaySpan(plannedEnd), [plannedEnd]);
|
||||
|
||||
const [maybeExpectedEnd, maybeExpectedDaySpan] = useMemo(() => calculateEndAndDaySpan(expectedEnd), [expectedEnd]);
|
||||
|
||||
const muted = maybeExpectedEnd === null;
|
||||
|
||||
return (
|
||||
<div className={style.column}>
|
||||
<div className={style.row}>
|
||||
<span className={style.label}>Start</span>
|
||||
<div className={style.labelledElement}>
|
||||
<Tooltip text='Planned start time' render={<TbCalendar className={style.icon} />} />
|
||||
<span className={cx([style.time])}>{formatTime(plannedStart)}</span>
|
||||
</div>
|
||||
<div className={style.labelledElement}>
|
||||
<Tooltip text='Actual start time' render={<TbCalendarClock className={style.icon} />} />
|
||||
<span className={cx([style.time, muted && style.muted])}>{formattedTime(actualStart)}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className={style.row}>
|
||||
<span className={style.label}>End</span>
|
||||
<div className={style.labelledElement}>
|
||||
<Tooltip text='Planned end time' render={<TbCalendar className={style.icon} />} />
|
||||
{maybePlannedDaySpan >= 0 ? (
|
||||
<Tooltip
|
||||
text={`Event spans over ${maybePlannedDaySpan + 1} days`}
|
||||
render={<span className={cx([style.time, style.daySpan])} />}
|
||||
>
|
||||
{formatTime(maybePlannedEnd)}
|
||||
</Tooltip>
|
||||
) : (
|
||||
<span className={cx([style.time, muted && style.muted])}>{formatTime(maybePlannedEnd)}</span>
|
||||
)}
|
||||
</div>
|
||||
<div className={style.labelledElement}>
|
||||
<Tooltip text='Expected end time' render={<TbCalendarStar className={style.icon} />} />
|
||||
{maybeExpectedEnd !== null && maybeExpectedDaySpan >= 0 ? (
|
||||
<Tooltip
|
||||
text={`Event spans over ${maybeExpectedDaySpan + 1} days`}
|
||||
render={<span className={cx([style.time, style.daySpan])} />}
|
||||
>
|
||||
{formattedTime(maybeExpectedEnd)}
|
||||
</Tooltip>
|
||||
) : (
|
||||
<span className={cx([style.time, muted && style.muted])}>{formattedTime(maybeExpectedEnd)}</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function MetadataTimes() {
|
||||
return (
|
||||
<div className={style.column}>
|
||||
<GroupTimes />
|
||||
<FlagTimes />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function GroupTimes() {
|
||||
const { blockStartedAt, clock, blockExpectedEnd } = useRuntimePlaybackOverview();
|
||||
const { currentBlockId } = useCurrentBlockId();
|
||||
const entry = useEntry(currentBlockId);
|
||||
|
||||
if (!currentBlockId) {
|
||||
return (
|
||||
<div className={style.metadataRow}>
|
||||
<span className={style.label}>Group</span>
|
||||
<div className={style.labelledElement}>
|
||||
<Tooltip text='Time to scheduled group end' render={<TbCalendarDown className={style.icon} />} />
|
||||
<span className={cx([style.time, style.muted])}>{timerPlaceholder}</span>
|
||||
</div>
|
||||
<div className={style.labelledElement}>
|
||||
<Tooltip text='Time to expected group end' render={<TbCalendarStar className={style.icon} />} />
|
||||
<span className={cx([style.time, style.muted])}>{timerPlaceholder}</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const remainingBlockDuration = (() => {
|
||||
if (blockStartedAt === null || !entry) return timerPlaceholder;
|
||||
if (!isOntimeBlock(entry)) return timerPlaceholder;
|
||||
return formattedTime(blockStartedAt + entry.duration - clock, 3, TimerType.CountDown);
|
||||
})();
|
||||
|
||||
const timeUntilBlockEnd = (() => {
|
||||
if (blockExpectedEnd === null) return timerPlaceholder;
|
||||
return formattedTime(blockExpectedEnd - clock, 3, TimerType.CountDown);
|
||||
})();
|
||||
|
||||
const groupTitle = (entry as OntimeBlock | null)?.title || 'Group';
|
||||
|
||||
return (
|
||||
<div className={style.metadataRow}>
|
||||
<span className={style.labelTitle}>{groupTitle}</span>
|
||||
<div className={style.labelledElement}>
|
||||
<Tooltip text='Time to scheduled group end' render={<TbCalendarDown className={style.icon} />} />
|
||||
<span className={cx([style.time, blockStartedAt === null && style.muted])}>{remainingBlockDuration}</span>
|
||||
</div>
|
||||
<div className={style.labelledElement}>
|
||||
<Tooltip text='Time to expected group end' render={<TbCalendarStar className={style.icon} />} />
|
||||
<span className={cx([style.time, blockExpectedEnd === null && style.muted])}>{timeUntilBlockEnd}</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function FlagTimes() {
|
||||
const { clock } = useClock();
|
||||
const { nextFlag } = useNextFlag();
|
||||
const entry = useEntry(nextFlag?.id ?? null);
|
||||
|
||||
// TODO(v4): can we make a good approximation of time until next flag?
|
||||
const timeUntil = useTimeUntilStart({
|
||||
timeStart: nextFlag?.start ?? 0,
|
||||
delay: 0,
|
||||
dayOffset: 0,
|
||||
totalGap: 0,
|
||||
isLinkedToLoaded: true,
|
||||
});
|
||||
|
||||
if (!nextFlag) {
|
||||
return (
|
||||
<div className={style.metadataRow}>
|
||||
<span className={style.label}>Flag</span>
|
||||
<div className={style.labelledElement}>
|
||||
<Tooltip text='Time to next flag scheduled start' render={<TbFlagDown className={style.icon} />} />
|
||||
<span className={cx([style.time, style.muted])}>{timerPlaceholder}</span>
|
||||
</div>
|
||||
<div className={style.labelledElement}>
|
||||
<Tooltip text='Time to next flag expected start' render={<TbFlagStar className={style.icon} />} />
|
||||
<span className={cx([style.time, style.muted])}>{timerPlaceholder}</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const muted = nextFlag === null;
|
||||
const flagTitle = (entry as OntimeEvent | null)?.title || 'Flag';
|
||||
const timeToNextFlag = nextFlag.start - clock;
|
||||
const display = millisToString(timeToNextFlag, { fallback: timerPlaceholder });
|
||||
const timeUntilDisplay = millisToString(timeUntil, { fallback: timerPlaceholder });
|
||||
|
||||
return (
|
||||
<div className={style.metadataRow}>
|
||||
<span className={cx([style.labelTitle])}>{flagTitle}</span>
|
||||
<div className={style.labelledElement}>
|
||||
<Tooltip text='Time to next flag scheduled start' render={<TbFlagDown className={style.icon} />} />
|
||||
<span className={cx([style.time])}>{display}</span>
|
||||
</div>
|
||||
<div className={style.labelledElement}>
|
||||
<Tooltip text='Time to next flag expected start' render={<TbFlagStar className={style.icon} />} />
|
||||
<span className={cx([style.time, muted && style.muted])}>{timeUntilDisplay}</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function ProgressOverview() {
|
||||
const { numEvents, selectedEventIndex } = useRuntimePlaybackOverview();
|
||||
|
||||
const current = selectedEventIndex !== null ? selectedEventIndex + 1 : enDash;
|
||||
const progressText = numEvents ? `${current} of ${numEvents || enDash}` : enDash;
|
||||
|
||||
return <TimeColumn label='Progress' value={progressText} />;
|
||||
}
|
||||
|
||||
export function RuntimeOverview() {
|
||||
const { offset, playback } = useRuntimePlaybackOverview();
|
||||
|
||||
const isPlaying = isPlaybackActive(playback);
|
||||
const offsetText = getOffsetText(isPlaying ? offset : null);
|
||||
const offsetClasses = cx([style.offset, isPlaying && (offset < 0 ? style.behind : style.ahead)]);
|
||||
|
||||
return <TimeColumn label='Offset' value={offsetText} className={offsetClasses} testId='offset' />;
|
||||
}
|
||||
|
||||
export function ClockOverview() {
|
||||
const { clock } = useClock();
|
||||
|
||||
return <TimeColumn label='Time now' value={formattedTime(clock)} />;
|
||||
}
|
||||
|
||||
export function TimerOverview() {
|
||||
const { current } = useTimer();
|
||||
|
||||
const display = millisToString(current, { fallback: timerPlaceholder });
|
||||
|
||||
return <TimeColumn label='Running timer' value={display} muted={current === null} />;
|
||||
}
|
||||
@@ -1,7 +1,6 @@
|
||||
.label {
|
||||
color: $label-gray;
|
||||
font-size: calc(1rem - 2px);
|
||||
width: 15em; // a number large enough to force right alignment
|
||||
}
|
||||
|
||||
.clock {
|
||||
@@ -9,6 +8,7 @@
|
||||
font-size: 1.5rem;
|
||||
letter-spacing: 0.5px;
|
||||
min-width: 5em;
|
||||
font-variant-numeric: tabular-nums;
|
||||
|
||||
&::after {
|
||||
content: '\200b';
|
||||
@@ -21,6 +21,7 @@
|
||||
|
||||
.label {
|
||||
line-height: 0.9em;
|
||||
width: 7em;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -32,6 +33,7 @@
|
||||
|
||||
.label {
|
||||
text-align: right;
|
||||
width: 5em;
|
||||
}
|
||||
|
||||
.clock {
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import { PropsWithChildren } from 'react';
|
||||
|
||||
import Tooltip from '../../../common/components/tooltip/Tooltip';
|
||||
import { cx } from '../../../common/utils/styleUtils';
|
||||
|
||||
@@ -41,3 +43,22 @@ export function TimeRow({ label, value, daySpan, muted, className }: TimeLayoutP
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function TimeElementsRow({ label, value, daySpan, muted, className }: PropsWithChildren<TimeLayoutProps>) {
|
||||
return (
|
||||
<div className={style.row}>
|
||||
<span className={style.label}>{label}</span>
|
||||
{daySpan ? (
|
||||
<Tooltip
|
||||
text={`Event spans over ${daySpan + 1} days`}
|
||||
render={<span />}
|
||||
className={cx([style.clock, style.daySpan, className])}
|
||||
>
|
||||
{value}
|
||||
</Tooltip>
|
||||
) : (
|
||||
<span className={cx([style.clock, muted && style.muted, className])}>{value}</span>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
.title {
|
||||
font-size: 1.5rem;
|
||||
@include ellipsis-overflow;
|
||||
}
|
||||
|
||||
.description {
|
||||
font-size: 1rem;
|
||||
color: $label-gray;
|
||||
@include ellipsis-overflow;
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
import useProjectData from '../../../common/hooks-query/useProjectData';
|
||||
|
||||
import style from './TitleOverview.module.scss';
|
||||
|
||||
export default function TitleOverview() {
|
||||
const { data } = useProjectData();
|
||||
|
||||
if (!data.title && !data.description) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className={style.title}>{data.title}</div>
|
||||
<div className={style.description}>{data.description}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
+1
-1
@@ -6,7 +6,7 @@ import { enDash, timerPlaceholder, timerPlaceholderMin } from '../../common/util
|
||||
/**
|
||||
* Encapsulates the logic for formatting time in overview
|
||||
*/
|
||||
export function formatedTime(
|
||||
export function formattedTime(
|
||||
time: MaybeNumber,
|
||||
segments: number = 3,
|
||||
direction?: TimerType.CountDown | TimerType.CountUp,
|
||||
@@ -38,10 +38,24 @@
|
||||
}
|
||||
|
||||
.entryIndex {
|
||||
text-align: right;
|
||||
padding-block: 0.25rem;
|
||||
display: grid;
|
||||
grid-template-rows: 1fr 1fr 1fr;
|
||||
justify-items: end;
|
||||
min-width: 2em;
|
||||
color: $label-gray;
|
||||
font-size: calc(1rem - 3px);
|
||||
height: 6.5rem;
|
||||
}
|
||||
|
||||
.flag {
|
||||
grid-row: 1;
|
||||
color: $active-indicator;
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
.index {
|
||||
grid-row: 2;
|
||||
}
|
||||
|
||||
.entry {
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { Fragment, lazy, useCallback, useEffect, useRef, useState } from 'react';
|
||||
import { TbFlagFilled } from 'react-icons/tb';
|
||||
import {
|
||||
closestCenter,
|
||||
DndContext,
|
||||
@@ -482,7 +483,12 @@ export default function Rundown({ data }: RundownProps) {
|
||||
data-testid={`entry-${rundownMetadata.eventIndex}`}
|
||||
style={blockColour ? { '--user-bg': blockColour } : {}}
|
||||
>
|
||||
{isOntimeEvent(entry) && <div className={style.entryIndex}>{rundownMetadata.eventIndex}</div>}
|
||||
{isOntimeEvent(entry) && (
|
||||
<div className={style.entryIndex}>
|
||||
{entry.flag && <TbFlagFilled className={style.flag} />}
|
||||
<div className={style.index}>{rundownMetadata.eventIndex}</div>
|
||||
</div>
|
||||
)}
|
||||
<div className={style.entry} key={entry.id} ref={hasCursor ? cursorRef : undefined}>
|
||||
<RundownEntry
|
||||
type={entry.type}
|
||||
|
||||
@@ -17,7 +17,7 @@ $skip-opacity: 0.2;
|
||||
'binder ... ... ...';
|
||||
|
||||
grid-template-columns: $block-binder-width 3rem 1fr 3rem;
|
||||
grid-template-rows: 0.125rem 2rem 2rem auto 0.125rem;
|
||||
grid-template-rows: 0.125rem 2rem 2rem auto 0.125rem;
|
||||
align-items: center;
|
||||
padding-right: $block-clearance;
|
||||
gap: 2px;
|
||||
|
||||
@@ -2,7 +2,6 @@ import { MouseEvent, useEffect, useLayoutEffect, useRef, useState } from 'react'
|
||||
import {
|
||||
IoAdd,
|
||||
IoDuplicateOutline,
|
||||
IoFlag,
|
||||
IoFolder,
|
||||
IoLink,
|
||||
IoReorderTwo,
|
||||
@@ -10,6 +9,7 @@ import {
|
||||
IoTrash,
|
||||
IoUnlink,
|
||||
} from 'react-icons/io5';
|
||||
import { TbFlagFilled } from 'react-icons/tb';
|
||||
import { useSortable } from '@dnd-kit/sortable';
|
||||
import { CSS } from '@dnd-kit/utilities';
|
||||
import { EndAction, EntryId, OntimeEvent, Playback, TimerType, TimeStrategy } from 'ontime-types';
|
||||
@@ -148,7 +148,7 @@ export default function RundownEvent({
|
||||
{
|
||||
type: 'item',
|
||||
label: flag ? 'Remove flag' : 'Add flag',
|
||||
icon: IoFlag,
|
||||
icon: TbFlagFilled,
|
||||
onClick: () =>
|
||||
actionHandler('update', {
|
||||
field: 'flag',
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
import { MaybeNumber } from 'ontime-types';
|
||||
import { removeLeadingZero, removeSeconds } from 'ontime-utils';
|
||||
|
||||
import { formatedTime } from '../../../overview/overviewUtils';
|
||||
import { formattedTime } from '../../../overview/overview.utils';
|
||||
|
||||
interface RunningTimeProps {
|
||||
value: MaybeNumber;
|
||||
@@ -16,15 +16,15 @@ interface RunningTimeProps {
|
||||
|
||||
export default function RunningTime(props: RunningTimeProps) {
|
||||
const { value, hideSeconds, hideLeadingZero, className } = props;
|
||||
let formattedTime = formatedTime(value, hideSeconds || hideLeadingZero ? 2 : 3);
|
||||
let display = formattedTime(value, hideSeconds || hideLeadingZero ? 2 : 3);
|
||||
|
||||
if (hideLeadingZero) {
|
||||
formattedTime = removeLeadingZero(formattedTime);
|
||||
display = removeLeadingZero(display);
|
||||
}
|
||||
|
||||
if (hideSeconds) {
|
||||
formattedTime = removeSeconds(formattedTime);
|
||||
display = removeSeconds(display);
|
||||
}
|
||||
|
||||
return <div className={className}>{formattedTime}</div>;
|
||||
return <div className={className}>{display}</div>;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user