End pause (#832)

* refactor: trigger ahead

* refactor: refresh often

* fix: handle empty rundown updates

* fix: handle falsy values

* style: avoid overlap of components

* refactor: freeze end option
This commit is contained in:
Carlos Valente
2024-03-19 18:04:05 +01:00
committed by GitHub
parent 8156da03d5
commit 4bb836b6f2
18 changed files with 89 additions and 39 deletions
@@ -1,9 +1,10 @@
import { ViewSettings } from 'ontime-types';
export const viewsSettingsPlaceholder: ViewSettings = {
overrideStyles: false,
normalColor: '#ffffffcc',
warningColor: '#FFAB33',
dangerColor: '#ED3333',
endMessage: '',
freezeEnd: false,
normalColor: '#ffffffcc',
overrideStyles: false,
warningColor: '#FFAB33',
};
@@ -1,7 +1,7 @@
.corner {
position: absolute;
top: 1rem;
right: 1rem;
right: 2rem;
z-index: 100;
}
@@ -118,7 +118,17 @@ export default function ViewSettingsForm() {
</Panel.ListGroup>
<Panel.ListGroup>
<Panel.ListItem>
<Panel.Field title='End message' description='If no end message is provided, timer will continue' />
<Panel.Field
title='Freeze timer on end'
description='Timer in views will stop from going negative after reaching'
/>
<Switch {...register('freezeEnd')} variant='ontime' size='lg' />
</Panel.ListItem>
<Panel.ListItem>
<Panel.Field
title='End message'
description='Message to show on negative timers if not frozen. If not provided, timer will continue'
/>
<Input
size='sm'
autoComplete='off'
@@ -4,7 +4,7 @@ import type { ViewExtendedTimer } from '../../../common/models/TimeManager.type'
type TimerTypeParams = Pick<ViewExtendedTimer, 'timerType' | 'current' | 'elapsed' | 'clock'>;
export function getTimerByType(timerObject?: TimerTypeParams): number | null {
export function getTimerByType(freezeEnd: boolean, timerObject?: TimerTypeParams): number | null {
if (!timerObject) {
return null;
}
@@ -12,7 +12,10 @@ export function getTimerByType(timerObject?: TimerTypeParams): number | null {
switch (timerObject.timerType) {
case TimerType.CountDown:
case TimerType.TimeToEnd:
return timerObject.current;
if (timerObject.current === null) {
return null;
}
return freezeEnd ? Math.max(timerObject.current, 0) : timerObject.current;
case TimerType.CountUp:
return Math.abs(timerObject.elapsed ?? 0);
case TimerType.Clock:
@@ -1,7 +1,7 @@
import { useEffect } from 'react';
import { useSearchParams } from 'react-router-dom';
import { Playback, TimerMessage, TimerType, ViewSettings } from 'ontime-types';
import { millisToString, removeLeadingZero, removeSeconds } from 'ontime-utils';
import { MILLIS_PER_SECOND, millisToString, removeLeadingZero, removeSeconds } from 'ontime-utils';
import { overrideStylesURL } from '../../../common/api/constants';
import NavigationMenu from '../../../common/components/navigation-menu/NavigationMenu';
@@ -150,7 +150,7 @@ export default function MinimalTimer(props: MinimalTimerProps) {
if (!timerIsTimeOfDay && showProgress && showWarning) timerColor = viewSettings.warningColor;
if (!timerIsTimeOfDay && showProgress && showDanger) timerColor = viewSettings.dangerColor;
const stageTimer = getTimerByType(time);
const stageTimer = getTimerByType(viewSettings.freezeEnd, time);
let display = millisToString(stageTimer, { fallback: timerPlaceholder });
if (stageTimer !== null) {
if (hideTimerSeconds) {
@@ -158,7 +158,8 @@ export default function MinimalTimer(props: MinimalTimerProps) {
}
display = removeLeadingZero(display);
// last unit rounds up in negative timers
const isNegative = (stageTimer ?? 0 < 0) && !timerIsTimeOfDay && time.timerType !== TimerType.CountUp;
const isNegative =
(stageTimer ?? 0 < MILLIS_PER_SECOND) && !timerIsTimeOfDay && time.timerType !== TimerType.CountUp;
if (isNegative && display === '0') {
display = '-1';
}
@@ -11,7 +11,7 @@ import {
TimerType,
ViewSettings,
} from 'ontime-types';
import { millisToString, removeLeadingZero, removeSeconds } from 'ontime-utils';
import { MILLIS_PER_SECOND, millisToString, removeLeadingZero, removeSeconds } from 'ontime-utils';
import { overrideStylesURL } from '../../../common/api/constants';
import MultiPartProgressBar from '../../../common/components/multi-part-progress-bar/MultiPartProgressBar';
@@ -59,6 +59,7 @@ interface TimerProps {
export default function Timer(props: TimerProps) {
const { customFields, isMirrored, pres, eventNow, eventNext, time, viewSettings, external, settings } = props;
const { shouldRender } = useRuntimeStylesheet(viewSettings?.overrideStyles && overrideStylesURL);
const { getLocalizedString } = useTranslation();
const [searchParams] = useSearchParams();
@@ -127,7 +128,7 @@ export default function Timer(props: TimerProps) {
if (!timerIsTimeOfDay && showProgress && showWarning) timerColor = viewSettings.warningColor;
if (!timerIsTimeOfDay && showProgress && showDanger) timerColor = viewSettings.dangerColor;
const stageTimer = getTimerByType(time);
const stageTimer = getTimerByType(viewSettings.freezeEnd, time);
let display = millisToString(stageTimer, { fallback: timerPlaceholder });
if (stageTimer !== null) {
if (hideTimerSeconds) {
@@ -135,7 +136,8 @@ export default function Timer(props: TimerProps) {
}
display = removeLeadingZero(display);
// last unit rounds up in negative timers
const isNegative = (stageTimer ?? 0 < 0) && !timerIsTimeOfDay && time.timerType !== TimerType.CountUp;
const isNegative =
(stageTimer ?? 0 < -MILLIS_PER_SECOND) && !timerIsTimeOfDay && time.timerType !== TimerType.CountUp;
if (isNegative && display === '0') {
display = '-1';
}
@@ -215,7 +217,7 @@ export default function Timer(props: TimerProps) {
{!userOptions.hideCards && (
<>
<AnimatePresence>
{eventNow && (
{eventNow?.title && (
<motion.div
className='event now'
key='now'
@@ -230,7 +232,7 @@ export default function Timer(props: TimerProps) {
</AnimatePresence>
<AnimatePresence>
{eventNext && (
{eventNext?.title && (
<motion.div
className='event next'
key='next'
@@ -17,11 +17,12 @@ export async function postViewSettings(req: Request, res: Response<ViewSettings
try {
const newData = {
overrideStyles: req.body.overrideStyles,
endMessage: req.body?.endMessage || '',
normalColor: req.body.normalColor,
warningColor: req.body.warningColor,
dangerColor: req.body.dangerColor,
endMessage: req.body?.endMessage ?? '',
freezeEnd: req.body.freezeEnd,
normalColor: req.body.normalColor,
overrideStyles: req.body.overrideStyles,
warningColor: req.body.warningColor,
};
await DataProvider.setViewSettings(newData);
res.status(200).send(newData);
@@ -23,6 +23,7 @@ describe('safeMerge', () => {
},
viewSettings: {
overrideStyles: false,
freezeEnd: false,
endMessage: 'existing endMessage',
normalColor: '#ffffffcc',
warningColor: '#FFAB33',
+1
View File
@@ -2,4 +2,5 @@ export const timerConfig = {
skipLimit: 1000, // threshold of skip for recalculating
updateRate: 32, // how often do we update the timer
notificationRate: 1000, // how often do we notify clients and integrations
triggerAhead: 16, // how far ahead do we trigger the end event
};
+1
View File
@@ -25,6 +25,7 @@ export const dbModel: DatabaseModel = {
normalColor: '#ffffffcc',
warningColor: '#FFAB33',
dangerColor: '#ED3333',
freezeEnd: false,
endMessage: '',
},
urlPresets: [],
+24 -3
View File
@@ -48,7 +48,7 @@ export class TimerService {
this.onUpdateCallback = timerConfig.onUpdateCallback;
this._interval = setInterval(() => {
this.update();
}, TimerService._updateInterval);
}, TimerService._refreshInterval);
}
@broadcastResult
@@ -58,7 +58,8 @@ export class TimerService {
}
const state = runtimeState.getState();
this.endCallback = setTimeout(() => this.update(), state.timer.expectedFinish);
const endTime = state.timer.current - 10;
this.endCallback = setTimeout(() => this.update(), endTime);
return true;
}
@@ -107,7 +108,6 @@ export class TimerService {
@broadcastResult
update() {
const updateResult = runtimeState.update();
// pass the result to the parent
this.onUpdateCallback(updateResult);
}
@@ -143,6 +143,7 @@ function broadcastResult(_target: any, _propertyKey: string, descriptor: Propert
// some changes need an immediate update
const hasNewLoaded = state.eventNow?.id !== TimerService.previousState?.eventNow?.id;
const hasSkippedBack = state.clock < TimerService.previousUpdate;
const justStarted = !TimerService.previousState?.timer;
const hasChangedPlayback = TimerService.previousState.timer?.playback !== state.timer.playback;
@@ -175,7 +176,27 @@ function broadcastResult(_target: any, _propertyKey: string, descriptor: Propert
// Helper function to update an event if it has changed
function updateEventIfChanged(eventKey: keyof RuntimeStore, state: RuntimeState) {
const previous = TimerService.previousState?.[eventKey];
const now = state[eventKey];
// if there was nothing, and there is nothing, noop
if (!previous?.id && !now?.id) {
return;
}
// if load status changed, save new
if (previous?.id !== now?.id) {
storeKey(eventKey);
return;
}
// maybe the event itself has changed
if (!deepEqual(TimerService.previousState?.[eventKey], state[eventKey])) {
storeKey(eventKey);
return;
}
function storeKey(eventKey: keyof RuntimeStore) {
eventStore.set(eventKey, state[eventKey]);
TimerService.previousState[eventKey] = { ...state[eventKey] };
}
@@ -1219,7 +1219,7 @@ describe('updateRoll()', () => {
clock: 11,
timer: {
current: 10,
expectedFinish: 15,
expectedFinish: 100,
secondaryTimer: null,
startedAt: 1,
},
@@ -1229,7 +1229,7 @@ describe('updateRoll()', () => {
} as RuntimeState;
const expected = {
updatedTimer: 15 - 11,
updatedTimer: 100 - 11,
updatedSecondaryTimer: null, // usually clock - expectedFinish
doRollLoad: false,
isFinished: false,
@@ -100,8 +100,8 @@ export async function deleteAllEvents() {
// notify event loader that rundown has changed
updateRuntimeOnChange();
// no need to modify timer since we will reset
notifyChanges({ external: true });
// notify timer and external services of change
notifyChanges({ timer: true, external: true });
}
/**
@@ -213,10 +213,15 @@ function updateRuntimeOnChange() {
export function notifyChanges(options: { timer?: boolean | string[]; external?: boolean }) {
if (options.timer) {
const playableEvents = getPlayableEvents();
// notify timer service of changed events
// timer can be true or an array of changed IDs
const affected = Array.isArray(options.timer) ? options.timer : undefined;
runtimeService.maybeUpdate(playableEvents, affected);
if (playableEvents.length === 0) {
runtimeService.stop();
} else {
// notify timer service of changed events
// timer can be true or an array of changed IDs
const affected = Array.isArray(options.timer) ? options.timer : undefined;
runtimeService.maybeUpdate(playableEvents, affected);
}
}
if (options.external) {
@@ -70,7 +70,7 @@ class RuntimeService {
this.eventTimer = new TimerService({
refresh: timerConfig.updateRate,
updateInterval: timerConfig.notificationRate,
onUpdateCallback: () => this.checkTimerUpdate,
onUpdateCallback: (updateResult) => this.checkTimerUpdate(updateResult),
});
if (resumable) {
+2 -1
View File
@@ -1,6 +1,7 @@
import { MaybeNumber, MaybeString, OntimeEvent, TimerType } from 'ontime-types';
import { dayInMs, sortArrayByProperty } from 'ontime-utils';
import { RuntimeState } from '../stores/runtimeState.js';
import { timerConfig } from '../config/config.js';
/**
* handle events that span over midnight
@@ -272,7 +273,7 @@ export const updateRoll = (state: RuntimeState) => {
updatedTimer -= dayInMs;
}
if (updatedTimer < 0) {
if (updatedTimer <= timerConfig.triggerAhead) {
isPrimaryFinished = true;
// we need a new event
doRollLoad = true;
+2 -1
View File
@@ -407,7 +407,8 @@ export function update(): UpdateResult {
function onPlayUpdate() {
let isFinished = false;
runtimeState.timer.current = getCurrent(runtimeState);
const finishedNow = runtimeState.timer.current <= 0 && runtimeState.timer.finishedAt === null;
const finishedNow =
runtimeState.timer.current <= timerConfig.triggerAhead && runtimeState.timer.finishedAt === null;
if (runtimeState.timer.playback === Playback.Play && finishedNow) {
runtimeState.timer.finishedAt = runtimeState.clock;
@@ -1,6 +1,6 @@
export enum EndAction {
None = 'none',
Stop = 'stop',
LoadNext = 'load-next',
None = 'none',
PlayNext = 'play-next',
Stop = 'stop',
}
@@ -1,7 +1,8 @@
export type ViewSettings = {
overrideStyles: boolean;
endMessage: string;
normalColor: string;
warningColor: string;
dangerColor: string;
endMessage: string;
freezeEnd: boolean;
normalColor: string;
overrideStyles: boolean;
warningColor: string;
};