mirror of
https://github.com/cpvalente/ontime.git
synced 2026-08-12 10:53:51 +00:00
feat: timer preview
This commit is contained in:
committed by
Carlos Valente
parent
fa709dc9be
commit
4d8bea2940
@@ -1,4 +1,4 @@
|
||||
import { RuntimeStore, SimpleDirection, SimplePlayback } from 'ontime-types';
|
||||
import { RuntimeStore, SimpleDirection, SimplePlayback, TimerMessage } from 'ontime-types';
|
||||
|
||||
import { useRuntimeStore } from '../stores/runtime';
|
||||
import { socketSendJson } from '../utils/socket';
|
||||
@@ -28,11 +28,43 @@ export const useOperator = () => {
|
||||
return useRuntimeStore(featureSelector);
|
||||
};
|
||||
|
||||
export const useMessageControl = () => {
|
||||
export const useTimerViewControl = () => {
|
||||
const featureSelector = (state: RuntimeStore) => ({
|
||||
timer: state.message.timer,
|
||||
external: state.message.external,
|
||||
onAir: state.onAir,
|
||||
blackout: state.message.timer.blackout,
|
||||
blink: state.message.timer.blink,
|
||||
secondarySource: state.message.timer.secondarySource,
|
||||
});
|
||||
|
||||
return useRuntimeStore(featureSelector);
|
||||
};
|
||||
|
||||
export const useTimerMessageInput = () => {
|
||||
const featureSelector = (state: RuntimeStore) => ({
|
||||
text: state.message.timer.text,
|
||||
visible: state.message.timer.visible,
|
||||
});
|
||||
|
||||
return useRuntimeStore(featureSelector);
|
||||
};
|
||||
|
||||
export const useExternalMessageInput = () => {
|
||||
const featureSelector = (state: RuntimeStore) => ({
|
||||
text: state.message.external,
|
||||
visible: state.message.timer.secondarySource === 'external',
|
||||
});
|
||||
|
||||
return useRuntimeStore(featureSelector);
|
||||
};
|
||||
|
||||
export const useMessagePreview = () => {
|
||||
const featureSelector = (state: RuntimeStore) => ({
|
||||
blink: state.message.timer.blink,
|
||||
blackout: state.message.timer.blackout,
|
||||
phase: state.timer.phase,
|
||||
showAuxTimer: state.message.timer.secondarySource === 'aux',
|
||||
showExternalMessage: state.message.timer.secondarySource === 'external' && Boolean(state.message.external),
|
||||
showTimerMessage: state.message.timer.visible && Boolean(state.message.timer.text),
|
||||
timerType: state.eventNow?.timerType ?? null,
|
||||
});
|
||||
|
||||
return useRuntimeStore(featureSelector);
|
||||
@@ -41,8 +73,11 @@ export const useMessageControl = () => {
|
||||
export const setMessage = {
|
||||
timerText: (payload: string) => socketSendJson('message', { timer: { text: payload } }),
|
||||
timerVisible: (payload: boolean) => socketSendJson('message', { timer: { visible: payload } }),
|
||||
externalText: (payload: string) => socketSendJson('message', { external: payload }),
|
||||
timerBlink: (payload: boolean) => socketSendJson('message', { timer: { blink: payload } }),
|
||||
timerBlackout: (payload: boolean) => socketSendJson('message', { timer: { blackout: payload } }),
|
||||
timerSecondary: (payload: TimerMessage['secondarySource']) =>
|
||||
socketSendJson('message', { timer: { secondarySource: payload } }),
|
||||
};
|
||||
|
||||
export const usePlaybackControl = () => {
|
||||
|
||||
@@ -23,11 +23,9 @@ export const runtimeStorePlaceholder: RuntimeStore = {
|
||||
visible: false,
|
||||
blink: false,
|
||||
blackout: false,
|
||||
secondarySource: null,
|
||||
},
|
||||
external: {
|
||||
text: '',
|
||||
visible: false,
|
||||
},
|
||||
external: '',
|
||||
},
|
||||
runtime: {
|
||||
selectedEventIndex: null,
|
||||
|
||||
@@ -147,7 +147,7 @@ export default function ViewSettingsForm() {
|
||||
variant='ontime-filled'
|
||||
maxLength={150}
|
||||
width='275px'
|
||||
placeholder='Message shown when timer reaches end'
|
||||
placeholder='Shown when timer reaches end'
|
||||
{...register('endMessage')}
|
||||
/>
|
||||
</Panel.ListItem>
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
import { useEffect, useRef } from 'react';
|
||||
import { IconButton, Input } from '@chakra-ui/react';
|
||||
import { Input } from '@chakra-ui/react';
|
||||
import { IoEye } from '@react-icons/all-files/io5/IoEye';
|
||||
import { IoEyeOffOutline } from '@react-icons/all-files/io5/IoEyeOffOutline';
|
||||
|
||||
import TooltipActionBtn from '../../../common/components/buttons/TooltipActionBtn';
|
||||
import { cx } from '../../../common/utils/styleUtils';
|
||||
import { tooltipDelayMid } from '../../../ontimeConfig';
|
||||
|
||||
import style from './InputRow.module.scss';
|
||||
@@ -13,15 +12,13 @@ interface InputRowProps {
|
||||
label: string;
|
||||
placeholder: string;
|
||||
text: string;
|
||||
visible?: boolean;
|
||||
readonly?: boolean;
|
||||
actionHandler: (action: string, payload: object) => void;
|
||||
visible: boolean;
|
||||
actionHandler: () => void;
|
||||
changeHandler: (newValue: string) => void;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export default function InputRow(props: InputRowProps) {
|
||||
const { label, placeholder, text, visible, actionHandler, changeHandler, className, readonly } = props;
|
||||
const { label, placeholder, text, visible, actionHandler, changeHandler } = props;
|
||||
|
||||
const inputRef = useRef<HTMLInputElement>(null);
|
||||
const cursorPositionRef = useRef(0);
|
||||
@@ -39,41 +36,27 @@ export default function InputRow(props: InputRowProps) {
|
||||
changeHandler(event.target.value);
|
||||
};
|
||||
|
||||
const classes = cx([style.inputRow, className]);
|
||||
|
||||
return (
|
||||
<div className={classes}>
|
||||
<div className={style.inputRow}>
|
||||
<label className={`${style.label} ${visible ? style.active : ''}`}>{label}</label>
|
||||
<div className={style.inputItems}>
|
||||
<Input
|
||||
ref={inputRef}
|
||||
size='sm'
|
||||
variant='ontime-filled'
|
||||
readOnly={readonly}
|
||||
disabled={readonly}
|
||||
value={text}
|
||||
onChange={handleInputChange}
|
||||
placeholder={placeholder}
|
||||
/>
|
||||
{readonly ? (
|
||||
<IconButton
|
||||
size='sm'
|
||||
isDisabled
|
||||
icon={visible ? <IoEye size='18px' /> : <IoEyeOffOutline size='18px' />}
|
||||
aria-label={`Toggle ${label}`}
|
||||
variant={visible ? 'ontime-filled' : 'ontime-subtle'}
|
||||
/>
|
||||
) : (
|
||||
<TooltipActionBtn
|
||||
clickHandler={() => actionHandler('update', { field: 'isPublic', value: !visible })}
|
||||
tooltip={visible ? 'Make invisible' : 'Make visible'}
|
||||
aria-label={`Toggle ${label}`}
|
||||
openDelay={tooltipDelayMid}
|
||||
icon={visible ? <IoEye size='18px' /> : <IoEyeOffOutline size='18px' />}
|
||||
variant={visible ? 'ontime-filled' : 'ontime-subtle'}
|
||||
size='sm'
|
||||
/>
|
||||
)}
|
||||
<TooltipActionBtn
|
||||
clickHandler={actionHandler}
|
||||
tooltip={visible ? 'Make invisible' : 'Make visible'}
|
||||
aria-label={`Toggle ${label}`}
|
||||
openDelay={tooltipDelayMid}
|
||||
icon={visible ? <IoEye size='18px' /> : <IoEyeOffOutline size='18px' />}
|
||||
variant={visible ? 'ontime-filled' : 'ontime-subtle'}
|
||||
size='sm'
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -1,23 +1,3 @@
|
||||
.messageContainer {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: $section-spacing;
|
||||
}
|
||||
|
||||
.buttonSection {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: $element-spacing;
|
||||
margin-top: -0.5rem;
|
||||
}
|
||||
|
||||
.singleAction {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: $element-spacing;
|
||||
margin-top: $element-inner-spacing;
|
||||
}
|
||||
|
||||
.label {
|
||||
font-size: $inner-section-text-size;
|
||||
color: $label-gray;
|
||||
@@ -26,3 +6,73 @@
|
||||
color: $action-text-color;
|
||||
}
|
||||
}
|
||||
|
||||
.previewContainer {
|
||||
display: grid;
|
||||
gap: $element-spacing;
|
||||
grid-template-columns: 2fr 1fr;
|
||||
}
|
||||
|
||||
.preview {
|
||||
background-color: $ui-black;
|
||||
display: grid;
|
||||
place-content: center;
|
||||
text-align: center;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.options {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: $element-spacing;
|
||||
}
|
||||
|
||||
.eventStatus {
|
||||
position: absolute;
|
||||
right: 0;
|
||||
margin: 0.5rem 0.25rem;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.25rem;
|
||||
}
|
||||
|
||||
.mainContent {
|
||||
font-size: 1rem;
|
||||
font-weight: 600;
|
||||
color: var(--override-colour, $ui-white);
|
||||
|
||||
&[data-phase='pending'] {
|
||||
color: $ontime-roll;
|
||||
}
|
||||
&[data-phase='overtime'] {
|
||||
color: $playback-negative;
|
||||
}
|
||||
&[data-phase='none'] {
|
||||
opacity: $opacity-disabled;
|
||||
}
|
||||
}
|
||||
|
||||
.secondaryContent {
|
||||
border-top: 1px solid $white-7;
|
||||
}
|
||||
|
||||
.blackout {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.timerIndicators {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.statusIcon {
|
||||
color: $gray-1000;
|
||||
|
||||
&[data-active='true'] {
|
||||
color: $active-indicator;
|
||||
}
|
||||
}
|
||||
|
||||
.divider {
|
||||
border-top: 1px solid $gray-1000;
|
||||
}
|
||||
|
||||
@@ -1,64 +1,52 @@
|
||||
import { Button } from '@chakra-ui/react';
|
||||
import { IoEye } from '@react-icons/all-files/io5/IoEye';
|
||||
import { IoEyeOffOutline } from '@react-icons/all-files/io5/IoEyeOffOutline';
|
||||
import { IoSunny } from '@react-icons/all-files/io5/IoSunny';
|
||||
import { IoSunnyOutline } from '@react-icons/all-files/io5/IoSunnyOutline';
|
||||
|
||||
import { setMessage, useMessageControl } from '../../../common/hooks/useSocket';
|
||||
import { enDash } from '../../../common/utils/styleUtils';
|
||||
import { setMessage, useExternalMessageInput, useTimerMessageInput } from '../../../common/hooks/useSocket';
|
||||
|
||||
import InputRow from './InputRow';
|
||||
|
||||
import style from './MessageControl.module.scss';
|
||||
|
||||
const noop = () => undefined;
|
||||
import TimerControlsPreview from './TimerViewControl';
|
||||
|
||||
export default function MessageControl() {
|
||||
const message = useMessageControl();
|
||||
const blink = message.timer.blink;
|
||||
const blackout = message.timer.blackout;
|
||||
|
||||
return (
|
||||
<div className={style.messageContainer}>
|
||||
<InputRow
|
||||
label='Timer'
|
||||
placeholder='Message shown in stage timer'
|
||||
text={message.timer.text}
|
||||
visible={message.timer.visible}
|
||||
changeHandler={(newValue) => setMessage.timerText(newValue)}
|
||||
actionHandler={() => setMessage.timerVisible(!message.timer.visible)}
|
||||
/>
|
||||
<div className={style.buttonSection}>
|
||||
<Button
|
||||
size='sm'
|
||||
className={`${blink ? style.blink : ''}`}
|
||||
variant={blink ? 'ontime-filled' : 'ontime-subtle'}
|
||||
leftIcon={blink ? <IoSunny size='1rem' /> : <IoSunnyOutline size='1rem' />}
|
||||
onClick={() => setMessage.timerBlink(!blink)}
|
||||
data-testid='toggle timer blink'
|
||||
>
|
||||
Blink
|
||||
</Button>
|
||||
<Button
|
||||
size='sm'
|
||||
className={style.blackoutButton}
|
||||
variant={blackout ? 'ontime-filled' : 'ontime-subtle'}
|
||||
leftIcon={blackout ? <IoEye size='1rem' /> : <IoEyeOffOutline size='1rem' />}
|
||||
onClick={() => setMessage.timerBlackout(!blackout)}
|
||||
data-testid='toggle timer blackout'
|
||||
>
|
||||
Blackout screen
|
||||
</Button>
|
||||
</div>
|
||||
<InputRow
|
||||
label='External Message (read only)'
|
||||
placeholder={enDash}
|
||||
readonly
|
||||
text={message.external.text}
|
||||
visible={message.external.visible}
|
||||
changeHandler={noop}
|
||||
actionHandler={noop}
|
||||
/>
|
||||
</div>
|
||||
<>
|
||||
<TimerControlsPreview />
|
||||
<TimerMessageInput />
|
||||
<ExternalInput />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function TimerMessageInput() {
|
||||
const { text, visible } = useTimerMessageInput();
|
||||
|
||||
return (
|
||||
<InputRow
|
||||
label='Timer Message'
|
||||
placeholder='Message shown fullscreen in stage timer'
|
||||
text={text}
|
||||
visible={visible}
|
||||
changeHandler={(newValue) => setMessage.timerText(newValue)}
|
||||
actionHandler={() => setMessage.timerVisible(!visible)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function ExternalInput() {
|
||||
const { text, visible } = useExternalMessageInput();
|
||||
|
||||
const toggleExternal = () => {
|
||||
if (visible) {
|
||||
setMessage.timerSecondary(null);
|
||||
} else {
|
||||
setMessage.timerSecondary('external');
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<InputRow
|
||||
label='External Message'
|
||||
placeholder='Message shown as secondary text in stage timer'
|
||||
text={text}
|
||||
visible={visible}
|
||||
changeHandler={(newValue) => setMessage.externalText(newValue)}
|
||||
actionHandler={toggleExternal}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -3,16 +3,19 @@ import { IoArrowUp } from '@react-icons/all-files/io5/IoArrowUp';
|
||||
|
||||
import ErrorBoundary from '../../../common/components/error-boundary/ErrorBoundary';
|
||||
import { handleLinks } from '../../../common/utils/linkUtils';
|
||||
import { cx } from '../../../common/utils/styleUtils';
|
||||
|
||||
import MessageControl from './MessageControl';
|
||||
|
||||
import style from '../../editors/Editor.module.scss';
|
||||
|
||||
const MessageControlExport = () => {
|
||||
const classes = cx([style.content, style.contentColumnLayout]);
|
||||
|
||||
return (
|
||||
<div className={style.messages} data-testid='panel-messages-control'>
|
||||
<IoArrowUp className={style.corner} onClick={(event) => handleLinks(event, 'messagecontrol')} />
|
||||
<div className={style.content}>
|
||||
<div className={classes}>
|
||||
<ErrorBoundary>
|
||||
<MessageControl />
|
||||
</ErrorBoundary>
|
||||
|
||||
@@ -0,0 +1,76 @@
|
||||
import { Tooltip } from '@chakra-ui/react';
|
||||
import { IoArrowDown } from '@react-icons/all-files/io5/IoArrowDown';
|
||||
import { IoArrowUp } from '@react-icons/all-files/io5/IoArrowUp';
|
||||
import { IoFlag } from '@react-icons/all-files/io5/IoFlag';
|
||||
import { IoTime } from '@react-icons/all-files/io5/IoTime';
|
||||
import { TimerPhase, TimerType } from 'ontime-types';
|
||||
|
||||
import { useMessagePreview } from '../../../common/hooks/useSocket';
|
||||
import useViewSettings from '../../../common/hooks-query/useViewSettings';
|
||||
import { cx } from '../../../common/utils/styleUtils';
|
||||
import { tooltipDelayMid } from '../../../ontimeConfig';
|
||||
|
||||
import style from './MessageControl.module.scss';
|
||||
|
||||
export default function TimerPreview() {
|
||||
const { blink, blackout, phase, showAuxTimer, showExternalMessage, showTimerMessage, timerType } =
|
||||
useMessagePreview();
|
||||
const { data } = useViewSettings();
|
||||
|
||||
const contentClasses = cx([style.previewContent, blink && style.blink, blackout && style.blackout]);
|
||||
|
||||
const main = (() => {
|
||||
if (showTimerMessage) return 'Message';
|
||||
if (phase === TimerPhase.Pending) return 'Standby to start';
|
||||
if (phase === TimerPhase.Overtime && data.endMessage) return 'Custom end message';
|
||||
return 'Timer';
|
||||
})();
|
||||
|
||||
const secondary = (() => {
|
||||
// message is a fullscreen overlay
|
||||
if (showTimerMessage) return null;
|
||||
|
||||
// we need to check aux first since it takes priority
|
||||
if (showAuxTimer) return 'Aux Timer';
|
||||
if (showExternalMessage) return 'External message';
|
||||
return null;
|
||||
})();
|
||||
|
||||
const overrideColour = (() => {
|
||||
// override fallback colours from starter project
|
||||
if (phase === TimerPhase.Warning) return data.warningColor ?? '#FFAB33';
|
||||
if (phase === TimerPhase.Danger) return data.dangerColor ?? '#ED3333';
|
||||
return data.normalColor ?? '#FFFC';
|
||||
})();
|
||||
|
||||
const showColourOverride = main == 'Timer';
|
||||
|
||||
return (
|
||||
<div className={style.preview}>
|
||||
<div className={contentClasses}>
|
||||
<div
|
||||
className={style.mainContent}
|
||||
data-phase={phase}
|
||||
style={showColourOverride ? { '--override-colour': overrideColour } : {}}
|
||||
>
|
||||
{main}
|
||||
</div>
|
||||
{secondary !== null && <div className={style.secondaryContent}>{secondary}</div>}
|
||||
</div>
|
||||
<div className={style.eventStatus}>
|
||||
<Tooltip label='Time type: Count down' openDelay={tooltipDelayMid} shouldWrapChildren>
|
||||
<IoArrowDown className={style.statusIcon} data-active={timerType === TimerType.CountDown} />
|
||||
</Tooltip>
|
||||
<Tooltip label='Time type: Count up' openDelay={tooltipDelayMid} shouldWrapChildren>
|
||||
<IoArrowUp className={style.statusIcon} data-active={timerType === TimerType.CountUp} />
|
||||
</Tooltip>
|
||||
<Tooltip label='Time type: Clock' openDelay={tooltipDelayMid} shouldWrapChildren>
|
||||
<IoTime className={style.statusIcon} data-active={timerType === TimerType.Clock} />
|
||||
</Tooltip>
|
||||
<Tooltip label='Time type: Time to end' openDelay={tooltipDelayMid} shouldWrapChildren>
|
||||
<IoFlag className={style.statusIcon} data-active={timerType === TimerType.TimeToEnd} />
|
||||
</Tooltip>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
import { Button } from '@chakra-ui/react';
|
||||
|
||||
import { setMessage, useTimerViewControl } from '../../../common/hooks/useSocket';
|
||||
|
||||
import TimerPreview from './TimerPreview';
|
||||
|
||||
import style from './MessageControl.module.scss';
|
||||
|
||||
export default function TimerControlsPreview() {
|
||||
const { blackout, blink, secondarySource } = useTimerViewControl();
|
||||
|
||||
const toggleSecondary = (newValue: 'aux' | 'external' | null) => {
|
||||
if (secondarySource === newValue) {
|
||||
setMessage.timerSecondary(null);
|
||||
} else {
|
||||
setMessage.timerSecondary(newValue);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={style.previewContainer}>
|
||||
<TimerPreview />
|
||||
|
||||
<div className={style.options}>
|
||||
<Button
|
||||
size='sm'
|
||||
variant={secondarySource === 'aux' ? 'ontime-filled' : 'ontime-subtle'}
|
||||
onClick={() => toggleSecondary('aux')}
|
||||
>
|
||||
Show Aux timer
|
||||
</Button>
|
||||
<Button
|
||||
size='sm'
|
||||
variant={secondarySource === 'external' ? 'ontime-filled' : 'ontime-subtle'}
|
||||
onClick={() => toggleSecondary('external')}
|
||||
>
|
||||
Show external
|
||||
</Button>
|
||||
|
||||
<hr className={style.divider} />
|
||||
|
||||
<Button
|
||||
size='sm'
|
||||
variant={blink ? 'ontime-filled' : 'ontime-subtle'}
|
||||
onClick={() => setMessage.timerBlink(!blink)}
|
||||
data-testid='toggle timer blink'
|
||||
>
|
||||
Blink
|
||||
</Button>
|
||||
<Button
|
||||
size='sm'
|
||||
className={style.blackoutButton}
|
||||
variant={blackout ? 'ontime-filled' : 'ontime-subtle'}
|
||||
onClick={() => setMessage.timerBlackout(!blackout)}
|
||||
data-testid='toggle timer blackout'
|
||||
>
|
||||
Blackout screen
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -68,7 +68,7 @@ function AuxTimerInput() {
|
||||
|
||||
const handleTimeUpdate = (_field: string, value: string) => {
|
||||
const newTime = parseUserTime(value);
|
||||
setDuration(newTime / 1000); //frontend api is seconds based;
|
||||
setDuration(newTime / 1000); // frontend api is seconds based
|
||||
};
|
||||
|
||||
return (
|
||||
|
||||
@@ -40,7 +40,7 @@ $panel-gap: 0.5rem;
|
||||
.playback,
|
||||
.messages {
|
||||
position: relative;
|
||||
border-radius: 8px;
|
||||
border-radius: var(--editor--panel__br);
|
||||
background-color: $bg-container-l2;
|
||||
padding: 1rem;
|
||||
}
|
||||
@@ -62,3 +62,10 @@ $panel-gap: 0.5rem;
|
||||
.content {
|
||||
padding-top: 1.5rem;
|
||||
}
|
||||
|
||||
.contentColumnLayout {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: $section-spacing;
|
||||
color: $ui-white;
|
||||
}
|
||||
|
||||
@@ -1,10 +1,18 @@
|
||||
@use '../../theme/ontimeColours' as *;
|
||||
@use '../../theme/ontimeStyles' as *;
|
||||
|
||||
@mixin absolute-top-right($distance) {
|
||||
// declare editor specific styling constants
|
||||
:root {
|
||||
--editor--panel__br: 8px;
|
||||
}
|
||||
|
||||
@mixin corner() {
|
||||
display: none;
|
||||
transform: rotate(45deg);
|
||||
|
||||
position: absolute;
|
||||
top: $distance;
|
||||
right: $distance;
|
||||
top: 0.5rem;
|
||||
right: 0.5rem;
|
||||
cursor: pointer;
|
||||
color: $ui-white;
|
||||
transition-property: color;
|
||||
@@ -15,17 +23,11 @@
|
||||
}
|
||||
}
|
||||
|
||||
@mixin corner() {
|
||||
display: none;
|
||||
@include absolute-top-right(0.5rem);
|
||||
transform: rotate(45deg);
|
||||
}
|
||||
|
||||
@mixin panel() {
|
||||
display: flex;
|
||||
|
||||
position: relative;
|
||||
border-radius: 8px;
|
||||
border-radius: var(--editor--panel__br);
|
||||
height: 100%;
|
||||
background-color: $bg-container-l2;
|
||||
padding: 1rem;
|
||||
|
||||
@@ -21,10 +21,6 @@ import EventBlockProgressBar from './composite/EventBlockProgressBar';
|
||||
|
||||
import style from './EventBlock.module.scss';
|
||||
|
||||
const tooltipProps = {
|
||||
openDelay: tooltipDelayMid,
|
||||
};
|
||||
|
||||
interface EventBlockInnerProps {
|
||||
timeStart: number;
|
||||
timeEnd: number;
|
||||
@@ -98,11 +94,7 @@ const EventBlockInner = (props: EventBlockInnerProps) => {
|
||||
</div>
|
||||
<div className={style.titleSection}>
|
||||
<EditableBlockTitle title={title} eventId={eventId} placeholder='Event title' className={style.eventTitle} />
|
||||
{isNext && (
|
||||
<Tooltip label='Next event' {...tooltipProps}>
|
||||
<span className={style.nextTag}>UP NEXT</span>
|
||||
</Tooltip>
|
||||
)}
|
||||
{isNext && <span className={style.nextTag}>UP NEXT</span>}
|
||||
</div>
|
||||
<EventBlockPlayback
|
||||
eventId={eventId}
|
||||
@@ -118,17 +110,17 @@ const EventBlockInner = (props: EventBlockInnerProps) => {
|
||||
{loaded && <EventBlockProgressBar />}
|
||||
</div>
|
||||
<div className={style.eventStatus} tabIndex={-1}>
|
||||
<Tooltip label={`Time type: ${timerType}`} {...tooltipProps}>
|
||||
<Tooltip label={`Time type: ${timerType}`} openDelay={tooltipDelayMid}>
|
||||
<span>
|
||||
<TimerIcon type={timerType} className={style.statusIcon} />
|
||||
</span>
|
||||
</Tooltip>
|
||||
<Tooltip label={`End action: ${endAction}`} {...tooltipProps}>
|
||||
<Tooltip label={`End action: ${endAction}`} openDelay={tooltipDelayMid}>
|
||||
<span>
|
||||
<EndActionIcon action={endAction} className={style.statusIcon} />
|
||||
</span>
|
||||
</Tooltip>
|
||||
<Tooltip label={`${isPublic ? 'Event is public' : 'Event is private'}`} {...tooltipProps}>
|
||||
<Tooltip label={`${isPublic ? 'Event is public' : 'Event is private'}`} openDelay={tooltipDelayMid}>
|
||||
<span>
|
||||
<IoPeople className={`${style.statusIcon} ${isPublic ? style.active : style.disabled}`} />
|
||||
</span>
|
||||
|
||||
@@ -2,13 +2,13 @@ import { ComponentType, useMemo } from 'react';
|
||||
import { ViewExtendedTimer } from 'common/models/TimeManager.type';
|
||||
import {
|
||||
CustomFields,
|
||||
Message,
|
||||
MessageState,
|
||||
OntimeEvent,
|
||||
ProjectData,
|
||||
Runtime,
|
||||
Settings,
|
||||
SimpleTimerState,
|
||||
SupportedEvent,
|
||||
TimerMessage,
|
||||
ViewSettings,
|
||||
} from 'ontime-types';
|
||||
import { useStore } from 'zustand';
|
||||
@@ -23,17 +23,17 @@ import { runtimeStore } from '../../common/stores/runtime';
|
||||
import { useViewOptionsStore } from '../../common/stores/viewOptions';
|
||||
|
||||
type WithDataProps = {
|
||||
auxTimer: SimpleTimerState;
|
||||
backstageEvents: OntimeEvent[];
|
||||
customFields: CustomFields;
|
||||
eventNext: OntimeEvent | null;
|
||||
eventNow: OntimeEvent | null;
|
||||
events: OntimeEvent[];
|
||||
external: Message;
|
||||
general: ProjectData;
|
||||
isMirrored: boolean;
|
||||
message: MessageState;
|
||||
nextId: string | null;
|
||||
onAir: boolean;
|
||||
pres: TimerMessage;
|
||||
publicEventNext: OntimeEvent | null;
|
||||
publicEventNow: OntimeEvent | null;
|
||||
publicSelectedId: string | null;
|
||||
@@ -68,7 +68,7 @@ const withData = <P extends WithDataProps>(Component: ComponentType<P>) => {
|
||||
}, [rundownData]);
|
||||
|
||||
// websocket data
|
||||
const { clock, timer, message, onAir, eventNext, publicEventNext, publicEventNow, eventNow, runtime } =
|
||||
const { clock, timer, message, onAir, eventNext, publicEventNext, publicEventNow, eventNow, runtime, auxtimer1 } =
|
||||
useStore(runtimeStore);
|
||||
const publicSelectedId = publicEventNow?.id ?? null;
|
||||
const selectedId = eventNow?.id ?? null;
|
||||
@@ -96,17 +96,17 @@ const withData = <P extends WithDataProps>(Component: ComponentType<P>) => {
|
||||
<ViewNavigationMenu />
|
||||
<Component
|
||||
{...props}
|
||||
auxTimer={auxtimer1}
|
||||
backstageEvents={rundownData}
|
||||
customFields={customFields}
|
||||
eventNext={eventNext}
|
||||
eventNow={eventNow}
|
||||
events={publicEvents}
|
||||
external={message.external}
|
||||
general={project}
|
||||
isMirrored={isMirrored}
|
||||
message={message}
|
||||
nextId={nextId}
|
||||
onAir={onAir}
|
||||
pres={message.timer}
|
||||
publicEventNext={publicEventNext}
|
||||
publicEventNow={publicEventNow}
|
||||
publicSelectedId={publicSelectedId}
|
||||
|
||||
@@ -2,11 +2,11 @@ import { useSearchParams } from 'react-router-dom';
|
||||
import { AnimatePresence, motion } from 'framer-motion';
|
||||
import {
|
||||
CustomFields,
|
||||
Message,
|
||||
MessageState,
|
||||
OntimeEvent,
|
||||
Playback,
|
||||
Settings,
|
||||
TimerMessage,
|
||||
SimpleTimerState,
|
||||
TimerPhase,
|
||||
TimerType,
|
||||
ViewSettings,
|
||||
@@ -48,19 +48,19 @@ const titleVariants = {
|
||||
export const MotionTitleCard = motion(TitleCard);
|
||||
|
||||
interface TimerProps {
|
||||
auxTimer: SimpleTimerState;
|
||||
customFields: CustomFields;
|
||||
eventNext: OntimeEvent | null;
|
||||
eventNow: OntimeEvent | null;
|
||||
external: Message;
|
||||
isMirrored: boolean;
|
||||
pres: TimerMessage;
|
||||
message: MessageState;
|
||||
settings: Settings | undefined;
|
||||
time: ViewExtendedTimer;
|
||||
viewSettings: ViewSettings;
|
||||
}
|
||||
|
||||
export default function Timer(props: TimerProps) {
|
||||
const { customFields, isMirrored, pres, eventNow, eventNext, time, viewSettings, external, settings } = props;
|
||||
const { auxTimer, customFields, eventNow, eventNext, isMirrored, message, settings, time, viewSettings } = props;
|
||||
|
||||
const { shouldRender } = useRuntimeStylesheet(viewSettings?.overrideStyles && overrideStylesURL);
|
||||
const { getLocalizedString } = useTranslation();
|
||||
@@ -114,7 +114,7 @@ export default function Timer(props: TimerProps) {
|
||||
const mainFieldNow = (main ? getPropertyValue(eventNow, main) : eventNow?.title) ?? '';
|
||||
const mainFieldNext = (main ? getPropertyValue(eventNext, main) : eventNext?.title) ?? '';
|
||||
|
||||
const showOverlay = pres.text !== '' && pres.visible;
|
||||
const showOverlay = message.timer.text !== '' && message.timer.visible;
|
||||
const isPlaying = time.playback !== Playback.Pause;
|
||||
|
||||
const timerIsTimeOfDay = time.timerType === TimerType.Clock;
|
||||
@@ -128,10 +128,20 @@ export default function Timer(props: TimerProps) {
|
||||
const showFinished = finished && (shouldShowModifiers || showEndMessage);
|
||||
const showWarning = shouldShowModifiers && time.phase === TimerPhase.Warning;
|
||||
const showDanger = shouldShowModifiers && time.phase === TimerPhase.Danger;
|
||||
const showBlinking = pres.blink;
|
||||
const showBlackout = pres.blackout;
|
||||
const showClock = time.timerType !== TimerType.Clock;
|
||||
const showExternal = external.visible && external.text;
|
||||
|
||||
const secondaryContent = ((): string | undefined => {
|
||||
if (message.timer.secondarySource === 'aux') {
|
||||
return getFormattedTimer(auxTimer.current, TimerType.CountDown, getLocalizedString('common.minutes'), {
|
||||
removeSeconds: userOptions.hideTimerSeconds,
|
||||
removeLeadingZero: userOptions.removeLeadingZeros,
|
||||
});
|
||||
}
|
||||
if (message.timer.secondarySource === 'external' && message.external) {
|
||||
return message.external;
|
||||
}
|
||||
return;
|
||||
})();
|
||||
|
||||
let timerColor = viewSettings.normalColor;
|
||||
if (!timerIsTimeOfDay && showProgress && showWarning) timerColor = viewSettings.warningColor;
|
||||
@@ -146,13 +156,14 @@ export default function Timer(props: TimerProps) {
|
||||
const stageTimerCharacters = display.replace('/:/g', '').length;
|
||||
|
||||
const baseClasses = `stage-timer ${isMirrored ? 'mirror' : ''}`;
|
||||
|
||||
let timerFontSize = 89 / (stageTimerCharacters - 1);
|
||||
// we need to shrink the timer if the external is going to be there
|
||||
if (showExternal) {
|
||||
if (secondaryContent) {
|
||||
timerFontSize *= 0.8;
|
||||
}
|
||||
const externalFontSize = timerFontSize * 0.4;
|
||||
const timerContainerClasses = `timer-container ${showBlinking ? (showOverlay ? '' : 'blink') : ''}`;
|
||||
const timerContainerClasses = `timer-container ${message.timer.blink ? (showOverlay ? '' : 'blink') : ''}`;
|
||||
const timerClasses = `timer ${!isPlaying ? 'timer--paused' : ''} ${showFinished ? 'timer--finished' : ''}`;
|
||||
|
||||
const defaultFormat = getDefaultFormat(settings?.timeFormat);
|
||||
@@ -161,11 +172,11 @@ export default function Timer(props: TimerProps) {
|
||||
return (
|
||||
<div className={showFinished ? `${baseClasses} stage-timer--finished` : baseClasses} data-testid='timer-view'>
|
||||
<ViewParamsEditor viewOptions={timerOptions} />
|
||||
<div className={showBlackout ? 'blackout blackout--active' : 'blackout'} />
|
||||
<div className={message.timer.blackout ? 'blackout blackout--active' : 'blackout'} />
|
||||
{!userOptions.hideMessage && (
|
||||
<div className={showOverlay ? 'message-overlay message-overlay--active' : 'message-overlay'}>
|
||||
<FitText mode='multi' min={32} max={256} className={`message ${showBlinking ? 'blink' : ''}`}>
|
||||
{pres.text}
|
||||
<FitText mode='multi' min={32} max={256} className={`message ${message.timer.blink ? 'blink' : ''}`}>
|
||||
{message.timer.text}
|
||||
</FitText>
|
||||
</div>
|
||||
)}
|
||||
@@ -192,10 +203,10 @@ export default function Timer(props: TimerProps) {
|
||||
</div>
|
||||
)}
|
||||
<div
|
||||
className={`external${showExternal ? '' : ' external--hidden'}`}
|
||||
className={`external${secondaryContent ? '' : ' external--hidden'}`}
|
||||
style={{ fontSize: `${externalFontSize}vw` }}
|
||||
>
|
||||
{external.text}
|
||||
{secondaryContent}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user