mirror of
https://github.com/cpvalente/ontime.git
synced 2026-08-17 05:13:32 +00:00
323b30ea68
Review pass over the view. Removed: estimateWordsPerLine and linesPerMinuteToWordsPerMinute, written for a words-per-minute readout that was never built; play and pause on the controller, which nothing outside the hook called; and three constants that were exported but never imported. The tick function was being reassigned to a ref on every render, which is a side effect during render. It only ever touched refs and state setters, so it is now a stable callback and the ref is gone. The synthetic contentKey string is replaced by the memoised blocks array it was standing in for. It was also a dependency of the ResizeObserver effect, which tore the observer down and rebuilt it for no gain: the observer already covers every reflow that changes the document. ScriptBlock was memoised but never actually memoising, because the parent built its ref callback inline and handed it a new identity every render. That also churned the follow map, unregistering and re-registering every block. The id is now bound inside the block against a stable callback. Tests: dropped six that asserted arithmetic identities or wrapped clamps rather than behaviour, and added three for branches that were untested, group titles across and back into a group, and a heading with no cue. The e2e rewind assertion waited 600ms for an eased scroll that needs about 900ms from a nudge and a second from the bottom of a long script; it now polls. The navigation menu assertion raced app hydration and now waits for the view, as the existing navigation tests do. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Cb8RVPNQ2ETPJxdy4b8CHf
114 lines
3.6 KiB
TypeScript
114 lines
3.6 KiB
TypeScript
import { type CustomFields, isOntimeEvent, isOntimeGroup, type MaybeString, type Rundown } from 'ontime-types';
|
|
|
|
import type { RundownMetadataObject } from '../../common/utils/rundownMetadata';
|
|
import { getPropertyValue } from '../common/viewUtils';
|
|
import type { HeadingSource, ScriptBlock, TeleprompterOptions } from './teleprompter.types';
|
|
|
|
type BuildScriptOptions = Pick<
|
|
TeleprompterOptions,
|
|
'scriptSource' | 'heading' | 'hideEmpty' | 'hidePast' | 'showGroups'
|
|
>;
|
|
|
|
/**
|
|
* Resolves the heading shown above a script block.
|
|
*/
|
|
function makeHeading(source: HeadingSource, cue: string, title: string): string {
|
|
switch (source) {
|
|
case 'cue':
|
|
return cue;
|
|
case 'title':
|
|
return title;
|
|
case 'both':
|
|
return [cue, title].filter(Boolean).join(' · ');
|
|
case 'none':
|
|
return '';
|
|
}
|
|
}
|
|
|
|
/**
|
|
* An image custom field holds a URL, which is meaningless to read aloud.
|
|
* The params editor filters these out of the select, but the URL can be hand typed.
|
|
*/
|
|
function isReadableSource(scriptSource: string, customFields: CustomFields): boolean {
|
|
if (!scriptSource.startsWith('custom-')) {
|
|
return true;
|
|
}
|
|
const key = scriptSource.slice('custom-'.length);
|
|
return customFields[key]?.type === 'text';
|
|
}
|
|
|
|
/**
|
|
* Flattens the rundown into the continuous document the prompter scrolls through.
|
|
*
|
|
* We iterate flatOrder so that events nested in groups arrive in reading order
|
|
* without a second pass.
|
|
*/
|
|
export function buildScript(
|
|
rundown: Rundown,
|
|
rundownMetadata: RundownMetadataObject,
|
|
customFields: CustomFields,
|
|
options: BuildScriptOptions,
|
|
): ScriptBlock[] {
|
|
const { scriptSource, heading, hideEmpty, hidePast, showGroups } = options;
|
|
|
|
if (!scriptSource || scriptSource === 'none' || !isReadableSource(scriptSource, customFields)) {
|
|
return [];
|
|
}
|
|
|
|
const blocks: ScriptBlock[] = [];
|
|
let lastGroupId: MaybeString = null;
|
|
|
|
for (const id of rundown.flatOrder) {
|
|
const entry = rundown.entries[id];
|
|
if (!isOntimeEvent(entry) || entry.skip) {
|
|
continue;
|
|
}
|
|
|
|
const metadata = rundownMetadata[id];
|
|
if (hidePast && metadata?.isPast) {
|
|
continue;
|
|
}
|
|
|
|
const text = getPropertyValue(entry, scriptSource, rundown.entries)?.trim() ?? '';
|
|
if (hideEmpty && !text) {
|
|
continue;
|
|
}
|
|
|
|
// a group title is emitted once, on the first block that belongs to it
|
|
const groupId = metadata?.groupId ?? null;
|
|
let groupTitle: MaybeString = null;
|
|
if (showGroups && groupId && groupId !== lastGroupId) {
|
|
const group = rundown.entries[groupId];
|
|
groupTitle = isOntimeGroup(group) ? group.title : null;
|
|
}
|
|
lastGroupId = groupId;
|
|
|
|
blocks.push({
|
|
id,
|
|
heading: makeHeading(heading, entry.cue, entry.title),
|
|
text,
|
|
groupTitle,
|
|
isLoaded: Boolean(metadata?.isLoaded),
|
|
});
|
|
}
|
|
|
|
return blocks;
|
|
}
|
|
|
|
/**
|
|
* Folds Ontime's global "Flip Screen" toggle into the per view flips.
|
|
*
|
|
* The shared `.mirror` class is `rotate(180deg)`, which is the same matrix as
|
|
* `scale(-1, -1)`: a flip on both axes at once. So the global toggle is exactly
|
|
* the pair of flips this view already has, and composing them with XOR keeps the
|
|
* teleprompter behaving like every other view without two transforms competing
|
|
* for the same property.
|
|
*
|
|
* It cannot replace the per view flips, though. A rotation preserves handedness,
|
|
* so it never yields the mirror image a beam splitter reflection needs; only a
|
|
* single axis flip does. That is why both exist.
|
|
*/
|
|
export function composeFlip(flipH: boolean, flipV: boolean, isMirrored: boolean): { flipH: boolean; flipV: boolean } {
|
|
return { flipH: flipH !== isMirrored, flipV: flipV !== isMirrored };
|
|
}
|