mirror of
https://github.com/cpvalente/ontime.git
synced 2026-09-01 04:19:12 +00:00
176 lines
5.8 KiB
TypeScript
176 lines
5.8 KiB
TypeScript
import { OntimeView } from 'ontime-types';
|
|
import { type CSSProperties, useState } from 'react';
|
|
|
|
import EmptyPage from '../../common/components/state/EmptyPage';
|
|
import ViewParamsEditor from '../../common/components/view-params-editor/ViewParamsEditor';
|
|
import { useSelectedEventId } from '../../common/hooks/useSocket';
|
|
import { useWindowTitle } from '../../common/hooks/useWindowTitle';
|
|
import { useViewOptionsStore } from '../../common/stores/viewOptions';
|
|
import { cx } from '../../common/utils/styleUtils';
|
|
import Loader from '../common/loader/Loader';
|
|
import ControlOverlay from './control-overlay/ControlOverlay';
|
|
import HelpOverlay from './help-overlay/HelpOverlay';
|
|
import ScriptBlockView from './script-block/ScriptBlock';
|
|
import { defaults, getTeleprompterOptions, useTeleprompterOptions } from './teleprompter.options';
|
|
import { stepFontSize } from './teleprompter.scroll';
|
|
import { buildScript, composeFlip } from './teleprompter.utils';
|
|
import { useSyncTeleprompterParams } from './useSyncTeleprompterParams';
|
|
import { useTeleprompterControls } from './useTeleprompterControls';
|
|
import { type TeleprompterData, useTeleprompterData } from './useTeleprompterData';
|
|
import { useTeleprompterScroll } from './useTeleprompterScroll';
|
|
|
|
import './Teleprompter.scss';
|
|
|
|
export default function TeleprompterLoader() {
|
|
const { data, status } = useTeleprompterData();
|
|
|
|
useWindowTitle('Teleprompter');
|
|
|
|
if (status === 'pending') {
|
|
return <Loader />;
|
|
}
|
|
|
|
if (status === 'error') {
|
|
return <EmptyPage variant='error' text='There was an error fetching data, please refresh the page.' />;
|
|
}
|
|
|
|
return <Teleprompter {...data} />;
|
|
}
|
|
|
|
function Teleprompter({ rundown, rundownMetadata, customFields }: TeleprompterData) {
|
|
'use memo';
|
|
|
|
const options = useTeleprompterOptions();
|
|
const selectedEventId = useSelectedEventId();
|
|
const isMirrored = useViewOptionsStore((state) => state.mirror);
|
|
|
|
const [showHelp, setShowHelp] = useState(false);
|
|
|
|
const fromParams = { flipH: options.flipH, flipV: options.flipV, fontSize: options.fontSize };
|
|
const paramsKey = `${fromParams.flipH}|${fromParams.flipV}|${fromParams.fontSize}`;
|
|
|
|
const [live, setLive] = useState(fromParams);
|
|
const [seededFrom, setSeededFrom] = useState(paramsKey);
|
|
// Reset live controls before commit when the URL configuration changes.
|
|
if (seededFrom !== paramsKey) {
|
|
setSeededFrom(paramsKey);
|
|
setLive(fromParams);
|
|
}
|
|
|
|
const viewOptions = getTeleprompterOptions(customFields);
|
|
|
|
const blocks = buildScript(rundown, rundownMetadata, customFields, {
|
|
scriptSource: options.scriptSource,
|
|
heading: options.heading,
|
|
hideEmpty: options.hideEmpty,
|
|
showGroups: options.showGroups,
|
|
});
|
|
|
|
const {
|
|
scrollerRef,
|
|
contentRef,
|
|
registerBlock,
|
|
handleUserScroll,
|
|
controller,
|
|
isRunning,
|
|
speed,
|
|
followLocked,
|
|
atEnd,
|
|
} = useTeleprompterScroll({
|
|
initialSpeed: options.speed,
|
|
followLoaded: options.followLoaded,
|
|
selectedEventId,
|
|
readingLinePos: options.readingLinePos,
|
|
blocks,
|
|
});
|
|
|
|
const handleFlip = (axis: 'h' | 'v') =>
|
|
setLive((current) => {
|
|
const key = axis === 'h' ? 'flipH' : 'flipV';
|
|
return { ...current, [key]: !current[key] };
|
|
});
|
|
|
|
const handleFontSize = (steps: number) =>
|
|
setLive((current) => ({ ...current, fontSize: stepFontSize(current.fontSize, steps) }));
|
|
|
|
const handleResetFontSize = () => setLive((current) => ({ ...current, fontSize: defaults.fontSize }));
|
|
const handleToggleHelp = () => setShowHelp((current) => !current);
|
|
|
|
useSyncTeleprompterParams({ speed, fontSize: live.fontSize, flipH: live.flipH, flipV: live.flipV });
|
|
|
|
useTeleprompterControls({
|
|
controller,
|
|
isHelpOpen: showHelp,
|
|
onFlip: handleFlip,
|
|
onFontSize: handleFontSize,
|
|
onResetFontSize: handleResetFontSize,
|
|
onToggleHelp: handleToggleHelp,
|
|
});
|
|
|
|
const hasScriptSource = options.scriptSource !== 'none';
|
|
|
|
const effectiveFlip = composeFlip(live.flipH, live.flipV, isMirrored);
|
|
|
|
const viewStyles = {
|
|
'--tp-configured-font-size': `${live.fontSize}px`,
|
|
'--tp-line-height': options.lineHeight,
|
|
'--tp-text-width': `${options.textWidth}%`,
|
|
'--tp-reading-line': options.readingLinePos,
|
|
} as CSSProperties;
|
|
|
|
return (
|
|
<div
|
|
className={cx([
|
|
'teleprompter',
|
|
effectiveFlip.flipH && 'teleprompter--flip-h',
|
|
effectiveFlip.flipV && 'teleprompter--flip-v',
|
|
])}
|
|
style={viewStyles}
|
|
data-testid='teleprompter-view'
|
|
>
|
|
<ViewParamsEditor target={OntimeView.Teleprompter} viewOptions={viewOptions} />
|
|
|
|
{!hasScriptSource ? (
|
|
<EmptyPage text='Select which field holds the script in the view options' />
|
|
) : blocks.length === 0 ? (
|
|
<EmptyPage text='There is no script text in the selected field' />
|
|
) : (
|
|
<>
|
|
<div
|
|
className='teleprompter__scroller'
|
|
data-testid='teleprompter-scroller'
|
|
ref={scrollerRef}
|
|
onWheel={handleUserScroll}
|
|
onTouchMove={handleUserScroll}
|
|
onPointerDown={handleUserScroll}
|
|
>
|
|
<div className='teleprompter__content' ref={contentRef}>
|
|
{blocks.map((block) => (
|
|
<ScriptBlockView key={block.id} block={block} registerRef={registerBlock} />
|
|
))}
|
|
</div>
|
|
</div>
|
|
|
|
<div className='teleprompter__dim' />
|
|
{options.readingLine && (
|
|
<div className='teleprompter__reading-line'>
|
|
<span className='teleprompter__reading-marker' />
|
|
</div>
|
|
)}
|
|
|
|
<ControlOverlay
|
|
isRunning={isRunning}
|
|
speed={speed}
|
|
canReengageFollow={followLocked && options.followLoaded}
|
|
atEnd={atEnd}
|
|
controller={controller}
|
|
onToggleHelp={handleToggleHelp}
|
|
/>
|
|
</>
|
|
)}
|
|
|
|
<HelpOverlay isOpen={showHelp} onClose={handleToggleHelp} />
|
|
</div>
|
|
);
|
|
}
|