mirror of
https://github.com/cpvalente/ontime.git
synced 2026-08-09 17:33:55 +00:00
077748e5a4
The timer view lets operators show an aux timer or the secondary message
as a smaller secondary timer under the main event timer. This adds a
placement control so that selected source can be promoted into the main
slot, swapping positions with the event timer.
The swap is deliberate: the event timer is demoted to the secondary slot
rather than removed, so the show-critical countdown (and its phase colour)
is never lost from screen.
- add `SecondaryPlacement` ('below' | 'main') to the timer message, with
server validation and a store default
- expose the aux timer direction to the timer view and honour it when
formatting a promoted aux (previously hard-coded to count-down)
- add a `getTimerSlots` helper that assigns the event timer and secondary
content to the main/secondary slots, routing phase colour, paused/finished
styling and font sizing to whichever slot holds the event timer
- add a Placement radio control to the timer view panel (disabled until a
secondary source is active) and reflect the swap in the control preview
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MbgWVAXTJUX7E5pAmv6Rs7
66 lines
2.1 KiB
TypeScript
66 lines
2.1 KiB
TypeScript
import { TimerMessage } from 'ontime-types';
|
|
|
|
import * as assert from '../../utils/assert.js';
|
|
import { coerceBoolean, coerceString } from '../../utils/coerceType.js';
|
|
|
|
/**
|
|
* Creates a valid Message object from a payload
|
|
* @throws if the payload is not an object
|
|
*/
|
|
export function validateMessage(message: unknown): string {
|
|
return decodeURI(coerceString(message));
|
|
}
|
|
|
|
/**
|
|
* Creates a valid Timer Message object from a payload
|
|
* @throws if the payload is not an object
|
|
*/
|
|
export function validateTimerMessage(message: unknown): Partial<TimerMessage> {
|
|
assert.isObject(message);
|
|
|
|
const result: Partial<TimerMessage> = {};
|
|
|
|
if ('text' in message) result.text = decodeURI(coerceString(message.text));
|
|
if ('visible' in message) result.visible = coerceBoolean(message.visible);
|
|
if ('blink' in message) result.blink = coerceBoolean(message.blink);
|
|
if ('blackout' in message) result.blackout = coerceBoolean(message.blackout);
|
|
if ('secondarySource' in message) result.secondarySource = coerceSecondary(message.secondarySource);
|
|
if ('secondaryPlacement' in message) result.secondaryPlacement = coercePlacement(message.secondaryPlacement);
|
|
|
|
return result;
|
|
}
|
|
|
|
/**
|
|
* Asserts that the secondary value is one of the permitted values
|
|
*/
|
|
function assertSecondary(source: unknown): source is TimerMessage['secondarySource'] {
|
|
return source === 'aux1' || source === 'aux2' || source === 'aux3' || source === 'secondary' || source === null;
|
|
}
|
|
|
|
/**
|
|
* Ensures that the secondary value is one of the permitted values
|
|
*/
|
|
function coerceSecondary(source: unknown): TimerMessage['secondarySource'] {
|
|
if (!assertSecondary(source)) {
|
|
return null;
|
|
}
|
|
return source;
|
|
}
|
|
|
|
/**
|
|
* Asserts that the placement value is one of the permitted values
|
|
*/
|
|
function assertPlacement(placement: unknown): placement is TimerMessage['secondaryPlacement'] {
|
|
return placement === 'below' || placement === 'main';
|
|
}
|
|
|
|
/**
|
|
* Ensures that the placement value is one of the permitted values
|
|
*/
|
|
function coercePlacement(placement: unknown): TimerMessage['secondaryPlacement'] {
|
|
if (!assertPlacement(placement)) {
|
|
return 'below';
|
|
}
|
|
return placement;
|
|
}
|