Skip to content

Commit

Permalink
feat: timer preview
Browse files Browse the repository at this point in the history
  • Loading branch information
cpvalente committed Sep 7, 2024
1 parent aa4ee54 commit 55ee7dd
Show file tree
Hide file tree
Showing 20 changed files with 393 additions and 179 deletions.
22 changes: 20 additions & 2 deletions apps/client/src/common/hooks/useSocket.ts
Original file line number Diff line number Diff line change
@@ -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';
Expand Down Expand Up @@ -32,7 +32,22 @@ export const useMessageControl = () => {
const featureSelector = (state: RuntimeStore) => ({
timer: state.message.timer,
external: state.message.external,
onAir: state.onAir,
});

return useRuntimeStore(featureSelector);
};

export const useMessagePreview = () => {
const featureSelector = (state: RuntimeStore) => ({
messageVisible: state.message.timer.visible,
messageHasContent: Boolean(state.message.timer.text),
blink: state.message.timer.blink,
blackout: state.message.timer.blackout,
externalVisible: state.message.external.visible,
externalHasContent: Boolean(state.message.external.text),
secondary: state.message.timer.secondary,
phase: state.timer.phase,
timerType: state.eventNow?.timerType ?? null,
});

return useRuntimeStore(featureSelector);
Expand All @@ -41,8 +56,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: { text: payload } }),
externalVisible: (payload: boolean) => socketSendJson('message', { external: { visible: payload } }),
timerBlink: (payload: boolean) => socketSendJson('message', { timer: { blink: payload } }),
timerBlackout: (payload: boolean) => socketSendJson('message', { timer: { blackout: payload } }),
timerSecondary: (payload: TimerMessage['secondary']) => socketSendJson('message', { timer: { secondary: payload } }),
};

export const usePlaybackControl = () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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>
Expand Down
33 changes: 10 additions & 23 deletions apps/client/src/features/control/message/InputRow.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,14 +14,13 @@ interface InputRowProps {
placeholder: string;
text: string;
visible?: boolean;
readonly?: boolean;
actionHandler: (action: string, payload: object) => 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, className } = props;

const inputRef = useRef<HTMLInputElement>(null);
const cursorPositionRef = useRef(0);
Expand Down Expand Up @@ -49,31 +48,19 @@ export default function InputRow(props: InputRowProps) {
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('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'
/>
</div>
</div>
);
Expand Down
75 changes: 61 additions & 14 deletions apps/client/src/features/control/message/MessageControl.module.scss
Original file line number Diff line number Diff line change
@@ -1,28 +1,75 @@
.messageContainer {
display: flex;
flex-direction: column;
gap: $section-spacing;
.label {
font-size: $inner-section-text-size;
color: $label-gray;

&.active {
color: $action-text-color;
}
}

.buttonSection {
.previewContainer {
display: grid;
grid-template-columns: 1fr 1fr;
gap: $element-spacing;
margin-top: -0.5rem;
grid-template-columns: 2fr 1fr;
}

.singleAction {
.preview {
background-color: $ui-black;
display: grid;
place-content: center;
text-align: center;
position: relative;
}

.options {
display: flex;
flex-direction: column;
gap: $element-spacing;
margin-top: $element-inner-spacing;
}

.label {
font-size: $inner-section-text-size;
color: $label-gray;
.eventStatus {
position: absolute;
right: 0;
margin: 0.5rem 0.25rem;
display: flex;
flex-direction: column;
gap: 0.25rem;
}

&.active {
color: $action-text-color;
.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;
}
}

.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;
}
66 changes: 16 additions & 50 deletions apps/client/src/features/control/message/MessageControl.tsx
Original file line number Diff line number Diff line change
@@ -1,64 +1,30 @@
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 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;
const { timer, external } = useMessageControl();

return (
<div className={style.messageContainer}>
<>
<TimerControlsPreview />
<InputRow
label='Timer'
placeholder='Message shown in stage timer'
text={message.timer.text}
visible={message.timer.visible}
label='Timer message'
placeholder='Message shown fullscreen in stage timer'
text={timer.text}
visible={timer.visible}
changeHandler={(newValue) => setMessage.timerText(newValue)}
actionHandler={() => setMessage.timerVisible(!message.timer.visible)}
actionHandler={() => setMessage.timerVisible(!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}
label='External Message'
placeholder='Message shown as secondary text in stage timer'
text={external.text}
visible={external.visible}
changeHandler={(newValue) => setMessage.externalText(newValue)}
actionHandler={() => setMessage.externalVisible(!external.visible)}
/>
</div>
</>
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -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>
Expand Down
99 changes: 99 additions & 0 deletions apps/client/src/features/control/message/TimerPreview.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
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 message = useMessagePreview();
const { data } = useViewSettings();

const contentClasses = cx([style.previewContent, message.blink && style.blink, message.blackout && style.blackout]);

const showTimerMessage = message.messageVisible && message.messageHasContent;
const showExternalMessage = message.externalVisible && message.externalHasContent;

const main = (() => {
if (showTimerMessage) {
return 'Message';
}

if (message.phase === TimerPhase.Pending) {
return 'Standby to start';
}

if (message.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 (message.secondary === 'aux') {
return 'Aux Timer';
}

if (message.secondary === 'external' && showExternalMessage) {
return 'External message';
}

return null;
})();

const overrideColour = (() => {
// override fallback colours from starter project
if (message.phase === TimerPhase.Warning) {
return data.warningColor ?? '#FFAB33';
}
if (message.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={message.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={message.timerType === TimerType.CountDown} />
</Tooltip>
<Tooltip label='Time type: Count up' openDelay={tooltipDelayMid} shouldWrapChildren>
<IoArrowUp className={style.statusIcon} data-active={message.timerType === TimerType.CountUp} />
</Tooltip>
<Tooltip label='Time type: Clock' openDelay={tooltipDelayMid} shouldWrapChildren>
<IoTime className={style.statusIcon} data-active={message.timerType === TimerType.Clock} />
</Tooltip>
<Tooltip label='Time type: Time to end' openDelay={tooltipDelayMid} shouldWrapChildren>
<IoFlag className={style.statusIcon} data-active={message.timerType === TimerType.TimeToEnd} />
</Tooltip>
</div>
</div>
);
}
Loading

0 comments on commit 55ee7dd

Please sign in to comment.