mirror of
https://github.com/cpvalente/ontime.git
synced 2026-08-25 08:59:13 +00:00
feat: time to next flag
This commit is contained in:
@@ -150,6 +150,10 @@ export const useClock = createSelector((state: RuntimeStore) => ({
|
|||||||
clock: state.clock,
|
clock: state.clock,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
export const useNextFlag = createSelector((state: RuntimeStore) => ({
|
||||||
|
nextFlag: state.nextFlag,
|
||||||
|
}));
|
||||||
|
|
||||||
/** Used by the progress bar components */
|
/** Used by the progress bar components */
|
||||||
export const useProgressData = createSelector((state: RuntimeStore) => ({
|
export const useProgressData = createSelector((state: RuntimeStore) => ({
|
||||||
current: state.timer.current,
|
current: state.timer.current,
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
.label {
|
.label {
|
||||||
display: block;
|
display: block;
|
||||||
margin-top: 2rem;
|
margin-top: 1rem;
|
||||||
font-size: $inner-section-text-size;
|
font-size: $inner-section-text-size;
|
||||||
color: $label-gray;
|
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 { useIsMobileScreen } from '../../common/hooks/useIsMobileScreen';
|
||||||
import { useRuntimeOverview } from '../../common/hooks/useSocket';
|
|
||||||
|
|
||||||
import {
|
import { ClockOverview, MetadataTimes, RuntimeOverview, StartTimes, TimerOverview } from './composite/TimeElements';
|
||||||
ClockOverview,
|
import TitleOverview from './composite/TitleOverview';
|
||||||
CurrentBlockOverview,
|
import { OverviewWrapper } from './OverviewWrapper';
|
||||||
OverviewWrapper,
|
|
||||||
RuntimeOverview,
|
|
||||||
TimerOverview,
|
|
||||||
} from './composite/OverviewWrapper';
|
|
||||||
import { TimeRow } from './composite/TimeLayout';
|
|
||||||
import { calculateEndAndDaySpan, formatedTime } from './overviewUtils';
|
|
||||||
|
|
||||||
import style from './Overview.module.scss';
|
|
||||||
|
|
||||||
export default memo(CuesheetOverview);
|
export default memo(CuesheetOverview);
|
||||||
function CuesheetOverview({ children }: PropsWithChildren) {
|
function CuesheetOverview({ children }: PropsWithChildren) {
|
||||||
@@ -33,49 +24,14 @@ function CuesheetMobile({ children }: PropsWithChildren) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function CuesheetDesktop({ 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 (
|
return (
|
||||||
<OverviewWrapper navElements={children}>
|
<OverviewWrapper navElements={children}>
|
||||||
<div>
|
<TitleOverview />
|
||||||
<TimeRow
|
<StartTimes />
|
||||||
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>
|
|
||||||
<TimerOverview />
|
<TimerOverview />
|
||||||
<RuntimeOverview />
|
<RuntimeOverview />
|
||||||
<CurrentBlockOverview />
|
<MetadataTimes />
|
||||||
<ClockOverview />
|
<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>
|
</OverviewWrapper>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,67 +1,19 @@
|
|||||||
import { memo, PropsWithChildren, useMemo } from 'react';
|
import { memo, PropsWithChildren } from 'react';
|
||||||
|
|
||||||
import { useRuntimeOverview } from '../../common/hooks/useSocket';
|
import { ClockOverview, MetadataTimes, ProgressOverview, RuntimeOverview, StartTimes } from './composite/TimeElements';
|
||||||
|
import TitleOverview from './composite/TitleOverview';
|
||||||
import {
|
import { OverviewWrapper } from './OverviewWrapper';
|
||||||
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';
|
|
||||||
|
|
||||||
export default memo(EditorOverview);
|
export default memo(EditorOverview);
|
||||||
function EditorOverview({ children }: PropsWithChildren) {
|
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 (
|
return (
|
||||||
<OverviewWrapper navElements={children}>
|
<OverviewWrapper navElements={children}>
|
||||||
<TitlesOverview />
|
<TitleOverview />
|
||||||
<div>
|
<StartTimes />
|
||||||
<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>
|
|
||||||
<ProgressOverview />
|
<ProgressOverview />
|
||||||
<RuntimeOverview />
|
<RuntimeOverview />
|
||||||
<CurrentBlockOverview />
|
<MetadataTimes />
|
||||||
<ClockOverview />
|
<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>
|
</OverviewWrapper>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
grid-area: overview;
|
grid-area: overview;
|
||||||
font-size: $inner-section-text-size;
|
font-size: $inner-section-text-size;
|
||||||
display: flex;
|
display: flex;
|
||||||
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
.isOffline {
|
.isOffline {
|
||||||
@@ -23,36 +24,14 @@
|
|||||||
|
|
||||||
.nav {
|
.nav {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
gap: 0.5rem;
|
gap: 0.5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.info {
|
.info {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
padding-inline: 1rem;
|
padding-left: 1rem;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: space-between;
|
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 { dayInMs } from 'ontime-utils';
|
||||||
|
|
||||||
import { calculateEndAndDaySpan } from '../overviewUtils';
|
import { calculateEndAndDaySpan } from '../overview.utils';
|
||||||
|
|
||||||
describe('calculateEndAndDaySpan', () => {
|
describe('calculateEndAndDaySpan', () => {
|
||||||
it('should return [null, 0] when end is null', () => {
|
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 {
|
.label {
|
||||||
color: $label-gray;
|
color: $label-gray;
|
||||||
font-size: calc(1rem - 2px);
|
font-size: calc(1rem - 2px);
|
||||||
width: 15em; // a number large enough to force right alignment
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.clock {
|
.clock {
|
||||||
@@ -9,6 +8,7 @@
|
|||||||
font-size: 1.5rem;
|
font-size: 1.5rem;
|
||||||
letter-spacing: 0.5px;
|
letter-spacing: 0.5px;
|
||||||
min-width: 5em;
|
min-width: 5em;
|
||||||
|
font-variant-numeric: tabular-nums;
|
||||||
|
|
||||||
&::after {
|
&::after {
|
||||||
content: '\200b';
|
content: '\200b';
|
||||||
@@ -21,6 +21,7 @@
|
|||||||
|
|
||||||
.label {
|
.label {
|
||||||
line-height: 0.9em;
|
line-height: 0.9em;
|
||||||
|
width: 7em;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -32,6 +33,7 @@
|
|||||||
|
|
||||||
.label {
|
.label {
|
||||||
text-align: right;
|
text-align: right;
|
||||||
|
width: 5em;
|
||||||
}
|
}
|
||||||
|
|
||||||
.clock {
|
.clock {
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
import { PropsWithChildren } from 'react';
|
||||||
|
|
||||||
import Tooltip from '../../../common/components/tooltip/Tooltip';
|
import Tooltip from '../../../common/components/tooltip/Tooltip';
|
||||||
import { cx } from '../../../common/utils/styleUtils';
|
import { cx } from '../../../common/utils/styleUtils';
|
||||||
|
|
||||||
@@ -41,3 +43,22 @@ export function TimeRow({ label, value, daySpan, muted, className }: TimeLayoutP
|
|||||||
</div>
|
</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
|
* Encapsulates the logic for formatting time in overview
|
||||||
*/
|
*/
|
||||||
export function formatedTime(
|
export function formattedTime(
|
||||||
time: MaybeNumber,
|
time: MaybeNumber,
|
||||||
segments: number = 3,
|
segments: number = 3,
|
||||||
direction?: TimerType.CountDown | TimerType.CountUp,
|
direction?: TimerType.CountDown | TimerType.CountUp,
|
||||||
@@ -38,10 +38,24 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.entryIndex {
|
.entryIndex {
|
||||||
text-align: right;
|
padding-block: 0.25rem;
|
||||||
|
display: grid;
|
||||||
|
grid-template-rows: 1fr 1fr 1fr;
|
||||||
|
justify-items: end;
|
||||||
min-width: 2em;
|
min-width: 2em;
|
||||||
color: $label-gray;
|
color: $label-gray;
|
||||||
font-size: calc(1rem - 3px);
|
font-size: calc(1rem - 3px);
|
||||||
|
height: 6.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.flag {
|
||||||
|
grid-row: 1;
|
||||||
|
color: $active-indicator;
|
||||||
|
font-size: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.index {
|
||||||
|
grid-row: 2;
|
||||||
}
|
}
|
||||||
|
|
||||||
.entry {
|
.entry {
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { Fragment, lazy, useCallback, useEffect, useRef, useState } from 'react';
|
import { Fragment, lazy, useCallback, useEffect, useRef, useState } from 'react';
|
||||||
|
import { TbFlagFilled } from 'react-icons/tb';
|
||||||
import {
|
import {
|
||||||
closestCenter,
|
closestCenter,
|
||||||
DndContext,
|
DndContext,
|
||||||
@@ -482,7 +483,12 @@ export default function Rundown({ data }: RundownProps) {
|
|||||||
data-testid={`entry-${rundownMetadata.eventIndex}`}
|
data-testid={`entry-${rundownMetadata.eventIndex}`}
|
||||||
style={blockColour ? { '--user-bg': blockColour } : {}}
|
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}>
|
<div className={style.entry} key={entry.id} ref={hasCursor ? cursorRef : undefined}>
|
||||||
<RundownEntry
|
<RundownEntry
|
||||||
type={entry.type}
|
type={entry.type}
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ $skip-opacity: 0.2;
|
|||||||
'binder ... ... ...';
|
'binder ... ... ...';
|
||||||
|
|
||||||
grid-template-columns: $block-binder-width 3rem 1fr 3rem;
|
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;
|
align-items: center;
|
||||||
padding-right: $block-clearance;
|
padding-right: $block-clearance;
|
||||||
gap: 2px;
|
gap: 2px;
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ import { MouseEvent, useEffect, useLayoutEffect, useRef, useState } from 'react'
|
|||||||
import {
|
import {
|
||||||
IoAdd,
|
IoAdd,
|
||||||
IoDuplicateOutline,
|
IoDuplicateOutline,
|
||||||
IoFlag,
|
|
||||||
IoFolder,
|
IoFolder,
|
||||||
IoLink,
|
IoLink,
|
||||||
IoReorderTwo,
|
IoReorderTwo,
|
||||||
@@ -10,6 +9,7 @@ import {
|
|||||||
IoTrash,
|
IoTrash,
|
||||||
IoUnlink,
|
IoUnlink,
|
||||||
} from 'react-icons/io5';
|
} from 'react-icons/io5';
|
||||||
|
import { TbFlagFilled } from 'react-icons/tb';
|
||||||
import { useSortable } from '@dnd-kit/sortable';
|
import { useSortable } from '@dnd-kit/sortable';
|
||||||
import { CSS } from '@dnd-kit/utilities';
|
import { CSS } from '@dnd-kit/utilities';
|
||||||
import { EndAction, EntryId, OntimeEvent, Playback, TimerType, TimeStrategy } from 'ontime-types';
|
import { EndAction, EntryId, OntimeEvent, Playback, TimerType, TimeStrategy } from 'ontime-types';
|
||||||
@@ -148,7 +148,7 @@ export default function RundownEvent({
|
|||||||
{
|
{
|
||||||
type: 'item',
|
type: 'item',
|
||||||
label: flag ? 'Remove flag' : 'Add flag',
|
label: flag ? 'Remove flag' : 'Add flag',
|
||||||
icon: IoFlag,
|
icon: TbFlagFilled,
|
||||||
onClick: () =>
|
onClick: () =>
|
||||||
actionHandler('update', {
|
actionHandler('update', {
|
||||||
field: 'flag',
|
field: 'flag',
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
import { MaybeNumber } from 'ontime-types';
|
import { MaybeNumber } from 'ontime-types';
|
||||||
import { removeLeadingZero, removeSeconds } from 'ontime-utils';
|
import { removeLeadingZero, removeSeconds } from 'ontime-utils';
|
||||||
|
|
||||||
import { formatedTime } from '../../../overview/overviewUtils';
|
import { formattedTime } from '../../../overview/overview.utils';
|
||||||
|
|
||||||
interface RunningTimeProps {
|
interface RunningTimeProps {
|
||||||
value: MaybeNumber;
|
value: MaybeNumber;
|
||||||
@@ -16,15 +16,15 @@ interface RunningTimeProps {
|
|||||||
|
|
||||||
export default function RunningTime(props: RunningTimeProps) {
|
export default function RunningTime(props: RunningTimeProps) {
|
||||||
const { value, hideSeconds, hideLeadingZero, className } = props;
|
const { value, hideSeconds, hideLeadingZero, className } = props;
|
||||||
let formattedTime = formatedTime(value, hideSeconds || hideLeadingZero ? 2 : 3);
|
let display = formattedTime(value, hideSeconds || hideLeadingZero ? 2 : 3);
|
||||||
|
|
||||||
if (hideLeadingZero) {
|
if (hideLeadingZero) {
|
||||||
formattedTime = removeLeadingZero(formattedTime);
|
display = removeLeadingZero(display);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (hideSeconds) {
|
if (hideSeconds) {
|
||||||
formattedTime = removeSeconds(formattedTime);
|
display = removeSeconds(display);
|
||||||
}
|
}
|
||||||
|
|
||||||
return <div className={className}>{formattedTime}</div>;
|
return <div className={className}>{display}</div>;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -45,7 +45,7 @@ export default function BlockRow({ blockId, colour, hidePast, rowId, rowIndex, t
|
|||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
const rect = e.currentTarget.getBoundingClientRect();
|
const rect = e.currentTarget.getBoundingClientRect();
|
||||||
const yPos = 8 + rect.y + rect.height / 2;
|
const yPos = 8 + rect.y + rect.height / 2;
|
||||||
openMenu({ x: rect.x, y: yPos }, blockId, SupportedEntry.Block, rowIndex, null);
|
openMenu({ x: rect.x, y: yPos }, blockId, SupportedEntry.Block, rowIndex, null, null);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<IoEllipsisHorizontal />
|
<IoEllipsisHorizontal />
|
||||||
|
|||||||
@@ -100,7 +100,7 @@ export default function EventRow({
|
|||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
const rect = e.currentTarget.getBoundingClientRect();
|
const rect = e.currentTarget.getBoundingClientRect();
|
||||||
const yPos = 8 + rect.y + rect.height / 2;
|
const yPos = 8 + rect.y + rect.height / 2;
|
||||||
openMenu({ x: rect.x, y: yPos }, event.id, SupportedEntry.Event, rowIndex, event.parent);
|
openMenu({ x: rect.x, y: yPos }, event.id, SupportedEntry.Event, rowIndex, event.parent, event.flag);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<IoEllipsisHorizontal />
|
<IoEllipsisHorizontal />
|
||||||
|
|||||||
+7
@@ -0,0 +1,7 @@
|
|||||||
|
.flag {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
color: $active-indicator;
|
||||||
|
padding-top: 0.5rem;
|
||||||
|
padding-left: 0.75rem;
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
import { TbFlagFilled } from 'react-icons/tb';
|
||||||
|
|
||||||
|
import style from './FlagCell.module.scss';
|
||||||
|
|
||||||
|
export default function FlagCell() {
|
||||||
|
return (
|
||||||
|
<div className={style.flag}>
|
||||||
|
<TbFlagFilled />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
+1
-1
@@ -57,7 +57,7 @@ export default function MilestoneRow({
|
|||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
const rect = e.currentTarget.getBoundingClientRect();
|
const rect = e.currentTarget.getBoundingClientRect();
|
||||||
const yPos = 8 + rect.y + rect.height / 2;
|
const yPos = 8 + rect.y + rect.height / 2;
|
||||||
openMenu({ x: rect.x, y: yPos }, entryId, SupportedEntry.Milestone, rowIndex, parentId);
|
openMenu({ x: rect.x, y: yPos }, entryId, SupportedEntry.Milestone, rowIndex, parentId, null);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<IoEllipsisHorizontal />
|
<IoEllipsisHorizontal />
|
||||||
|
|||||||
+17
@@ -8,6 +8,7 @@ import { formatDuration, formatTime } from '../../../../common/utils/time';
|
|||||||
|
|
||||||
import DurationInput from './DurationInput';
|
import DurationInput from './DurationInput';
|
||||||
import EditableImage from './EditableImage';
|
import EditableImage from './EditableImage';
|
||||||
|
import FlagCell from './FlagCell';
|
||||||
import MultiLineCell from './MultiLineCell';
|
import MultiLineCell from './MultiLineCell';
|
||||||
import MutedText from './MutedText';
|
import MutedText from './MutedText';
|
||||||
import SingleLineCell from './SingleLineCell';
|
import SingleLineCell from './SingleLineCell';
|
||||||
@@ -154,6 +155,14 @@ function MakeSingleLineField({ row, column, table }: CellContext<OntimeEntry, un
|
|||||||
return <SingleLineCell initialValue={initialValue as string} handleUpdate={update} />;
|
return <SingleLineCell initialValue={initialValue as string} handleUpdate={update} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function MakeFlagField({ row }: CellContext<OntimeEntry, unknown>) {
|
||||||
|
const event = row.original;
|
||||||
|
if (!isOntimeEvent(event) || !event.flag) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return <FlagCell />;
|
||||||
|
}
|
||||||
|
|
||||||
function MakeCustomField({ row, column, table }: CellContext<OntimeEntry, unknown>) {
|
function MakeCustomField({ row, column, table }: CellContext<OntimeEntry, unknown>) {
|
||||||
const update = useCallback(
|
const update = useCallback(
|
||||||
(newValue: string) => {
|
(newValue: string) => {
|
||||||
@@ -189,6 +198,14 @@ export function makeCuesheetColumns(customFields: CustomFields): ColumnDef<Ontim
|
|||||||
}));
|
}));
|
||||||
|
|
||||||
return [
|
return [
|
||||||
|
{
|
||||||
|
accessorKey: 'flag',
|
||||||
|
id: 'flag',
|
||||||
|
header: 'Flag',
|
||||||
|
cell: MakeFlagField,
|
||||||
|
size: 45,
|
||||||
|
minSize: 45,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
accessorKey: 'cue',
|
accessorKey: 'cue',
|
||||||
id: 'cue',
|
id: 'cue',
|
||||||
|
|||||||
+10
-2
@@ -11,8 +11,8 @@ import { useCuesheetTableMenu } from './useCuesheetTableMenu';
|
|||||||
export default memo(CuesheetTableMenu);
|
export default memo(CuesheetTableMenu);
|
||||||
|
|
||||||
function CuesheetTableMenu() {
|
function CuesheetTableMenu() {
|
||||||
const { isOpen, entryId, entryIndex, parentId, position, closeMenu } = useCuesheetTableMenu();
|
const { isOpen, entryId, entryIndex, parentId, flag, position, closeMenu } = useCuesheetTableMenu();
|
||||||
const { addEntry, clone, deleteEntry, move } = useEntryActions();
|
const { addEntry, clone, deleteEntry, move, updateEntry } = useEntryActions();
|
||||||
const showModal = useCuesheetEditModal((state) => state.setEditableEntry);
|
const showModal = useCuesheetEditModal((state) => state.setEditableEntry);
|
||||||
|
|
||||||
if (!isOpen) {
|
if (!isOpen) {
|
||||||
@@ -26,6 +26,14 @@ function CuesheetTableMenu() {
|
|||||||
items={[
|
items={[
|
||||||
{ type: 'item', label: 'Edit...', onClick: () => showModal(entryId), icon: IoOptions },
|
{ type: 'item', label: 'Edit...', onClick: () => showModal(entryId), icon: IoOptions },
|
||||||
{ type: 'divider' },
|
{ type: 'divider' },
|
||||||
|
{
|
||||||
|
type: 'item',
|
||||||
|
label: flag ? 'Remove flag' : 'Add flag',
|
||||||
|
onClick: () => updateEntry({ id: entryId, flag: !flag }),
|
||||||
|
icon: IoDuplicateOutline,
|
||||||
|
disabled: flag === null,
|
||||||
|
},
|
||||||
|
{ type: 'divider' },
|
||||||
{
|
{
|
||||||
type: 'item',
|
type: 'item',
|
||||||
label: 'Add event above',
|
label: 'Add event above',
|
||||||
|
|||||||
+6
-1
@@ -9,6 +9,7 @@ type OpenMenu = {
|
|||||||
entryType: SupportedEntry;
|
entryType: SupportedEntry;
|
||||||
entryIndex: number;
|
entryIndex: number;
|
||||||
parentId: EntryId | null;
|
parentId: EntryId | null;
|
||||||
|
flag: boolean | null;
|
||||||
};
|
};
|
||||||
|
|
||||||
type ClosedMenu = {
|
type ClosedMenu = {
|
||||||
@@ -17,6 +18,7 @@ type ClosedMenu = {
|
|||||||
entryType: null;
|
entryType: null;
|
||||||
entryIndex: null;
|
entryIndex: null;
|
||||||
parentId: null;
|
parentId: null;
|
||||||
|
flag: null;
|
||||||
};
|
};
|
||||||
|
|
||||||
type CuesheetTableMenuStore = (OpenMenu | ClosedMenu) & {
|
type CuesheetTableMenuStore = (OpenMenu | ClosedMenu) & {
|
||||||
@@ -27,6 +29,7 @@ type CuesheetTableMenuStore = (OpenMenu | ClosedMenu) & {
|
|||||||
entryType: SupportedEntry,
|
entryType: SupportedEntry,
|
||||||
entryIndex: number,
|
entryIndex: number,
|
||||||
parentId: EntryId | null,
|
parentId: EntryId | null,
|
||||||
|
flag: boolean | null,
|
||||||
) => void;
|
) => void;
|
||||||
closeMenu: () => void;
|
closeMenu: () => void;
|
||||||
};
|
};
|
||||||
@@ -38,12 +41,14 @@ export const useCuesheetTableMenu = create<CuesheetTableMenuStore>((set) => ({
|
|||||||
entryIndex: null,
|
entryIndex: null,
|
||||||
parentId: null,
|
parentId: null,
|
||||||
position: { x: 0, y: 0 },
|
position: { x: 0, y: 0 },
|
||||||
|
flag: null,
|
||||||
openMenu: (
|
openMenu: (
|
||||||
position: Anchor,
|
position: Anchor,
|
||||||
entryId: EntryId,
|
entryId: EntryId,
|
||||||
entryType: SupportedEntry,
|
entryType: SupportedEntry,
|
||||||
entryIndex: number,
|
entryIndex: number,
|
||||||
parentId: EntryId | null,
|
parentId: EntryId | null,
|
||||||
) => set({ isOpen: true, position, entryId, entryType, entryIndex, parentId }),
|
flag: null | boolean,
|
||||||
|
) => set({ isOpen: true, position, entryId, entryType, entryIndex, parentId, flag }),
|
||||||
closeMenu: () => set({ isOpen: false }),
|
closeMenu: () => set({ isOpen: false }),
|
||||||
}));
|
}));
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ $panel-gap: 0.5rem;
|
|||||||
|
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: auto;
|
grid-template-columns: auto;
|
||||||
grid-template-rows: 3rem 1fr;
|
grid-template-rows: 3.5rem 1fr;
|
||||||
grid-template-areas:
|
grid-template-areas:
|
||||||
'overview'
|
'overview'
|
||||||
'main';
|
'main';
|
||||||
|
|||||||
@@ -427,6 +427,7 @@ describe('getCustomFieldData()', () => {
|
|||||||
linkStart: 'link start',
|
linkStart: 'link start',
|
||||||
timeEnd: 'time end',
|
timeEnd: 'time end',
|
||||||
duration: 'duration',
|
duration: 'duration',
|
||||||
|
flag: 'flag',
|
||||||
cue: 'cue',
|
cue: 'cue',
|
||||||
title: 'title',
|
title: 'title',
|
||||||
countToEnd: 'count to end',
|
countToEnd: 'count to end',
|
||||||
@@ -479,6 +480,7 @@ describe('getCustomFieldData()', () => {
|
|||||||
linkStart: 'link start',
|
linkStart: 'link start',
|
||||||
timeEnd: 'time end',
|
timeEnd: 'time end',
|
||||||
duration: 'duration',
|
duration: 'duration',
|
||||||
|
flag: 'flag',
|
||||||
cue: 'cue',
|
cue: 'cue',
|
||||||
title: 'title',
|
title: 'title',
|
||||||
countToEnd: 'count to end',
|
countToEnd: 'count to end',
|
||||||
|
|||||||
@@ -63,6 +63,7 @@ let rundownMetadata: RundownMetadata = {
|
|||||||
playableEventOrder: [],
|
playableEventOrder: [],
|
||||||
timedEventOrder: [],
|
timedEventOrder: [],
|
||||||
flatEntryOrder: [],
|
flatEntryOrder: [],
|
||||||
|
flags: [],
|
||||||
};
|
};
|
||||||
|
|
||||||
const customFieldsMetadata: CustomFieldsMetadata = {
|
const customFieldsMetadata: CustomFieldsMetadata = {
|
||||||
@@ -666,7 +667,7 @@ export function init(initialRundown: Readonly<Rundown>, initialCustomFields: Rea
|
|||||||
processedData;
|
processedData;
|
||||||
cachedRundown.entries = entries;
|
cachedRundown.entries = entries;
|
||||||
cachedRundown.order = order;
|
cachedRundown.order = order;
|
||||||
cachedRundown.flatOrder = metadata.flatEntryOrder; // TODO: remove in favour of the metadata flatEntryOrder
|
cachedRundown.flatOrder = metadata.flatEntryOrder;
|
||||||
cachedRundown.revision = rundown.revision;
|
cachedRundown.revision = rundown.revision;
|
||||||
customFieldsMetadata.assigned = assignedCustomFields;
|
customFieldsMetadata.assigned = assignedCustomFields;
|
||||||
rundownMetadata = metadata;
|
rundownMetadata = metadata;
|
||||||
|
|||||||
@@ -225,6 +225,7 @@ export function makeRundownMetadata(customFields: CustomFields) {
|
|||||||
playableEventOrder: [],
|
playableEventOrder: [],
|
||||||
timedEventOrder: [],
|
timedEventOrder: [],
|
||||||
flatEntryOrder: [],
|
flatEntryOrder: [],
|
||||||
|
flags: [],
|
||||||
|
|
||||||
entries: {},
|
entries: {},
|
||||||
order: [],
|
order: [],
|
||||||
@@ -300,6 +301,11 @@ function processEntry<T extends OntimeEntry>(
|
|||||||
processedData.firstStart = currentEntry.timeStart;
|
processedData.firstStart = currentEntry.timeStart;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// check if event is flagged
|
||||||
|
if (currentEntry.flag) {
|
||||||
|
processedData.flags.push(currentEntry.id);
|
||||||
|
}
|
||||||
|
|
||||||
currentEntry.gap = getTimeFrom(currentEntry, processedData.latestEvent);
|
currentEntry.gap = getTimeFrom(currentEntry, processedData.latestEvent);
|
||||||
|
|
||||||
if (currentEntry.gap === 0) {
|
if (currentEntry.gap === 0) {
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ export type RundownMetadata = {
|
|||||||
playableEventOrder: EntryId[]; // flat order of playable events
|
playableEventOrder: EntryId[]; // flat order of playable events
|
||||||
timedEventOrder: EntryId[]; // flat order of timed events
|
timedEventOrder: EntryId[]; // flat order of timed events
|
||||||
flatEntryOrder: EntryId[]; // flat order of entries
|
flatEntryOrder: EntryId[]; // flat order of entries
|
||||||
|
flags: EntryId[]; // flat order of flagged entries
|
||||||
};
|
};
|
||||||
|
|
||||||
export type AssignedMap = Record<CustomFieldKey, EntryId[]>;
|
export type AssignedMap = Record<CustomFieldKey, EntryId[]>;
|
||||||
|
|||||||
@@ -51,6 +51,7 @@ export class EventTimer {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// register a callback for the scheduled end
|
||||||
const endTime = state.timer.current - timerConfig.triggerAhead;
|
const endTime = state.timer.current - timerConfig.triggerAhead;
|
||||||
this.endCallback = setTimeout(() => this.update(), endTime);
|
this.endCallback = setTimeout(() => this.update(), endTime);
|
||||||
return true;
|
return true;
|
||||||
|
|||||||
@@ -810,6 +810,11 @@ function broadcastResult(_target: any, _propertyKey: string, descriptor: Propert
|
|||||||
RuntimeService.previousState.blockNext = state.blockNext;
|
RuntimeService.previousState.blockNext = state.blockNext;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (RuntimeService.previousState?.nextFlag !== state.nextFlag) {
|
||||||
|
batch.add('nextFlag', state.nextFlag);
|
||||||
|
RuntimeService.previousState.nextFlag = state.nextFlag;
|
||||||
|
}
|
||||||
|
|
||||||
if (hasImmediateChanges) {
|
if (hasImmediateChanges) {
|
||||||
saveRestoreState(state);
|
saveRestoreState(state);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ const baseState: RuntimeState = {
|
|||||||
eventNext: null,
|
eventNext: null,
|
||||||
blockNow: null,
|
blockNow: null,
|
||||||
blockNext: null,
|
blockNext: null,
|
||||||
|
nextFlag: null,
|
||||||
runtime: {
|
runtime: {
|
||||||
selectedEventIndex: null,
|
selectedEventIndex: null,
|
||||||
numEvents: 0,
|
numEvents: 0,
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import {
|
import {
|
||||||
BlockState,
|
CurrentBlockState,
|
||||||
isOntimeBlock,
|
isOntimeBlock,
|
||||||
|
isOntimeEvent,
|
||||||
MaybeNumber,
|
MaybeNumber,
|
||||||
MaybeString,
|
MaybeString,
|
||||||
OffsetMode,
|
OffsetMode,
|
||||||
@@ -11,6 +12,7 @@ import {
|
|||||||
runtimeStorePlaceholder,
|
runtimeStorePlaceholder,
|
||||||
TimerPhase,
|
TimerPhase,
|
||||||
TimerState,
|
TimerState,
|
||||||
|
UpcomingEntry,
|
||||||
} from 'ontime-types';
|
} from 'ontime-types';
|
||||||
import { calculateDuration, checkIsNow, dayInMs, isPlaybackActive } from 'ontime-utils';
|
import { calculateDuration, checkIsNow, dayInMs, isPlaybackActive } from 'ontime-utils';
|
||||||
|
|
||||||
@@ -32,8 +34,9 @@ import { getCurrentRundown } from '../api-data/rundown/rundown.dao.js';
|
|||||||
|
|
||||||
export type RuntimeState = {
|
export type RuntimeState = {
|
||||||
clock: number; // realtime clock
|
clock: number; // realtime clock
|
||||||
blockNow: BlockState | null;
|
blockNow: CurrentBlockState | null;
|
||||||
blockNext: MaybeString;
|
blockNext: MaybeString;
|
||||||
|
nextFlag: UpcomingEntry | null;
|
||||||
eventNow: PlayableEvent | null;
|
eventNow: PlayableEvent | null;
|
||||||
eventNext: PlayableEvent | null;
|
eventNext: PlayableEvent | null;
|
||||||
runtime: Runtime;
|
runtime: Runtime;
|
||||||
@@ -53,6 +56,7 @@ const runtimeState: RuntimeState = {
|
|||||||
clock: timeNow(),
|
clock: timeNow(),
|
||||||
blockNow: null,
|
blockNow: null,
|
||||||
blockNext: null,
|
blockNext: null,
|
||||||
|
nextFlag: null,
|
||||||
eventNow: null,
|
eventNow: null,
|
||||||
eventNext: null,
|
eventNext: null,
|
||||||
runtime: { ...runtimeStorePlaceholder.runtime },
|
runtime: { ...runtimeStorePlaceholder.runtime },
|
||||||
@@ -111,6 +115,7 @@ export function clearState() {
|
|||||||
|
|
||||||
runtimeState.blockNow = null;
|
runtimeState.blockNow = null;
|
||||||
runtimeState.blockNext = null;
|
runtimeState.blockNext = null;
|
||||||
|
runtimeState.nextFlag = null;
|
||||||
|
|
||||||
runtimeState.runtime.offset = 0;
|
runtimeState.runtime.offset = 0;
|
||||||
runtimeState.runtime.relativeOffset = 0;
|
runtimeState.runtime.relativeOffset = 0;
|
||||||
@@ -195,6 +200,7 @@ export function load(
|
|||||||
loadNow(rundown, metadata, eventIndex);
|
loadNow(rundown, metadata, eventIndex);
|
||||||
loadNext(rundown, metadata, eventIndex);
|
loadNext(rundown, metadata, eventIndex);
|
||||||
loadBlock(rundown);
|
loadBlock(rundown);
|
||||||
|
loadNextFlag(eventIndex, rundown, metadata);
|
||||||
|
|
||||||
// update state
|
// update state
|
||||||
runtimeState.timer.playback = Playback.Armed;
|
runtimeState.timer.playback = Playback.Armed;
|
||||||
@@ -720,6 +726,31 @@ export function loadBlock(rundown: Rundown, state = runtimeState) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* find and load the next flag from the currently loaded event
|
||||||
|
*/
|
||||||
|
export function loadNextFlag(currentIndex: number, rundown: Rundown, metadata: RundownMetadata) {
|
||||||
|
runtimeState.nextFlag = null;
|
||||||
|
|
||||||
|
if (metadata.flags.length === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let i = currentIndex; i < metadata.timedEventOrder.length; i++) {
|
||||||
|
const entryId = metadata.timedEventOrder[i];
|
||||||
|
if (metadata.flags.includes(entryId)) {
|
||||||
|
const event = rundown.entries[entryId];
|
||||||
|
if (!event || !isOntimeEvent(event)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
runtimeState.nextFlag = { id: event.id, start: event.timeStart };
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
export function setOffsetMode(mode: OffsetMode) {
|
export function setOffsetMode(mode: OffsetMode) {
|
||||||
runtimeState.runtime.offsetMode = mode;
|
runtimeState.runtime.offsetMode = mode;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,13 @@
|
|||||||
import type { MaybeNumber } from '../../utils/utils.type.js';
|
import type { MaybeNumber } from '../../utils/utils.type.js';
|
||||||
import type { EntryId } from '../core/OntimeEntry.js';
|
import type { EntryId } from '../core/OntimeEntry.js';
|
||||||
|
|
||||||
export type BlockState = {
|
export type CurrentBlockState = {
|
||||||
id: EntryId;
|
id: EntryId;
|
||||||
startedAt: MaybeNumber;
|
startedAt: MaybeNumber;
|
||||||
expectedEnd: MaybeNumber;
|
expectedEnd: MaybeNumber;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type UpcomingEntry = {
|
||||||
|
id: EntryId;
|
||||||
|
start: number;
|
||||||
|
};
|
||||||
|
|||||||
@@ -6,8 +6,8 @@ export enum OffsetMode {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export type Runtime = {
|
export type Runtime = {
|
||||||
numEvents: number;
|
|
||||||
selectedEventIndex: MaybeNumber;
|
selectedEventIndex: MaybeNumber;
|
||||||
|
numEvents: number;
|
||||||
offset: number;
|
offset: number;
|
||||||
relativeOffset: number;
|
relativeOffset: number;
|
||||||
plannedStart: MaybeNumber;
|
plannedStart: MaybeNumber;
|
||||||
|
|||||||
@@ -42,6 +42,7 @@ export const runtimeStorePlaceholder: Readonly<RuntimeStore> = {
|
|||||||
},
|
},
|
||||||
blockNow: null,
|
blockNow: null,
|
||||||
blockNext: null,
|
blockNext: null,
|
||||||
|
nextFlag: null,
|
||||||
eventNow: null,
|
eventNow: null,
|
||||||
eventNext: null,
|
eventNext: null,
|
||||||
auxtimer1: {
|
auxtimer1: {
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import type { MaybeString } from '../../utils/utils.type.js';
|
import type { MaybeString } from '../../utils/utils.type.js';
|
||||||
import type { OntimeEvent } from '../core/OntimeEntry.js';
|
import type { OntimeEvent } from '../core/OntimeEntry.js';
|
||||||
import type { SimpleTimerState } from './AuxTimer.type.js';
|
import type { SimpleTimerState } from './AuxTimer.type.js';
|
||||||
import type { BlockState } from './CurrentBlockState.type.js';
|
import type { CurrentBlockState, UpcomingEntry } from './CurrentBlockState.type.js';
|
||||||
import type { MessageState } from './MessageControl.type.js';
|
import type { MessageState } from './MessageControl.type.js';
|
||||||
import type { Runtime } from './Runtime.type.js';
|
import type { Runtime } from './Runtime.type.js';
|
||||||
import type { TimerState } from './TimerState.type.js';
|
import type { TimerState } from './TimerState.type.js';
|
||||||
@@ -20,8 +20,9 @@ export type RuntimeStore = {
|
|||||||
eventNow: OntimeEvent | null;
|
eventNow: OntimeEvent | null;
|
||||||
eventNext: OntimeEvent | null;
|
eventNext: OntimeEvent | null;
|
||||||
|
|
||||||
blockNow: BlockState | null;
|
blockNow: CurrentBlockState | null;
|
||||||
blockNext: MaybeString;
|
blockNext: MaybeString;
|
||||||
|
nextFlag: UpcomingEntry | null;
|
||||||
|
|
||||||
// extra timers
|
// extra timers
|
||||||
auxtimer1: SimpleTimerState;
|
auxtimer1: SimpleTimerState;
|
||||||
|
|||||||
@@ -100,7 +100,7 @@ export { OffsetMode } from './definitions/runtime/Runtime.type.js';
|
|||||||
export type { RuntimeStore } from './definitions/runtime/RuntimeStore.type.js';
|
export type { RuntimeStore } from './definitions/runtime/RuntimeStore.type.js';
|
||||||
export { runtimeStorePlaceholder } from './definitions/runtime/RuntimeStore.js';
|
export { runtimeStorePlaceholder } from './definitions/runtime/RuntimeStore.js';
|
||||||
export { type TimerState, TimerPhase } from './definitions/runtime/TimerState.type.js';
|
export { type TimerState, TimerPhase } from './definitions/runtime/TimerState.type.js';
|
||||||
export type { BlockState } from './definitions/runtime/CurrentBlockState.type.js';
|
export type { CurrentBlockState, UpcomingEntry } from './definitions/runtime/CurrentBlockState.type.js';
|
||||||
|
|
||||||
// ---> Extra Timer
|
// ---> Extra Timer
|
||||||
export { type SimpleTimerState, SimplePlayback, SimpleDirection } from './definitions/runtime/AuxTimer.type.js';
|
export { type SimpleTimerState, SimplePlayback, SimpleDirection } from './definitions/runtime/AuxTimer.type.js';
|
||||||
|
|||||||
@@ -2,13 +2,14 @@ import type { ImportMap } from '../spreadsheetImport';
|
|||||||
import { isImportMap } from '../spreadsheetImport';
|
import { isImportMap } from '../spreadsheetImport';
|
||||||
|
|
||||||
describe('isImportMap()', () => {
|
describe('isImportMap()', () => {
|
||||||
it('validates a v3 default import map', () => {
|
it('validates a v4 default import map', () => {
|
||||||
const v3ImportMap: ImportMap = {
|
const importMap: ImportMap = {
|
||||||
worksheet: 'event schedule',
|
worksheet: 'event schedule',
|
||||||
timeStart: 'time start',
|
timeStart: 'time start',
|
||||||
linkStart: 'link start',
|
linkStart: 'link start',
|
||||||
timeEnd: 'time end',
|
timeEnd: 'time end',
|
||||||
duration: 'duration',
|
duration: 'duration',
|
||||||
|
flag: 'flag',
|
||||||
cue: 'cue',
|
cue: 'cue',
|
||||||
title: 'title',
|
title: 'title',
|
||||||
countToEnd: 'count to end',
|
countToEnd: 'count to end',
|
||||||
@@ -23,7 +24,7 @@ describe('isImportMap()', () => {
|
|||||||
entryId: 'id',
|
entryId: 'id',
|
||||||
};
|
};
|
||||||
|
|
||||||
expect(isImportMap(v3ImportMap)).toBe(true);
|
expect(isImportMap(importMap)).toBe(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('rejects map missing keys', () => {
|
it('rejects map missing keys', () => {
|
||||||
@@ -50,12 +51,13 @@ describe('isImportMap()', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('handles custom properties', () => {
|
it('handles custom properties', () => {
|
||||||
const v3ImportMap: ImportMap = {
|
const importMap: ImportMap = {
|
||||||
worksheet: 'event schedule',
|
worksheet: 'event schedule',
|
||||||
timeStart: 'time start',
|
timeStart: 'time start',
|
||||||
linkStart: 'link start',
|
linkStart: 'link start',
|
||||||
timeEnd: 'time end',
|
timeEnd: 'time end',
|
||||||
duration: 'duration',
|
duration: 'duration',
|
||||||
|
flag: 'flag',
|
||||||
cue: 'cue',
|
cue: 'cue',
|
||||||
title: 'title',
|
title: 'title',
|
||||||
countToEnd: 'count to end',
|
countToEnd: 'count to end',
|
||||||
@@ -73,6 +75,6 @@ describe('isImportMap()', () => {
|
|||||||
entryId: 'id',
|
entryId: 'id',
|
||||||
};
|
};
|
||||||
|
|
||||||
expect(isImportMap(v3ImportMap)).toBe(true);
|
expect(isImportMap(importMap)).toBe(true);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user