mirror of
https://github.com/cpvalente/ontime.git
synced 2026-08-19 22:24:11 +00:00
feat: implement multiple aux timers
This commit is contained in:
committed by
Carlos Valente
parent
d1c58712ae
commit
bf8ed8d017
@@ -10,7 +10,7 @@
|
||||
.previewContainer {
|
||||
display: grid;
|
||||
gap: $element-spacing;
|
||||
grid-template-columns: 2fr 1fr;
|
||||
grid-template-columns: 3fr 2fr;
|
||||
}
|
||||
|
||||
.preview {
|
||||
@@ -39,6 +39,7 @@
|
||||
.mainContent {
|
||||
font-size: 1rem;
|
||||
font-weight: 600;
|
||||
width: 100%;
|
||||
color: var(--override-colour, $ui-white);
|
||||
|
||||
&[data-phase='pending'] {
|
||||
@@ -72,7 +73,3 @@
|
||||
color: $active-indicator;
|
||||
}
|
||||
}
|
||||
|
||||
.divider {
|
||||
border-top: 1px solid $gray-1000;
|
||||
}
|
||||
|
||||
@@ -1,12 +1,11 @@
|
||||
import { IoEye, IoEyeOffOutline } from 'react-icons/io5';
|
||||
|
||||
import TooltipActionBtn from '../../../common/components/buttons/TooltipActionBtn';
|
||||
import IconButton from '../../../common/components/buttons/IconButton';
|
||||
import {
|
||||
setMessage,
|
||||
useExternalMessageInput as useSecondaryMessageInput,
|
||||
useTimerMessageInput,
|
||||
} from '../../../common/hooks/useSocket';
|
||||
import { tooltipDelayMid } from '../../../ontimeConfig';
|
||||
|
||||
import InputRow from './InputRow';
|
||||
import TimerControlsPreview from './TimerViewControl';
|
||||
@@ -32,15 +31,13 @@ function TimerMessageInput() {
|
||||
visible={visible}
|
||||
changeHandler={(newValue) => setMessage.timerText(newValue)}
|
||||
>
|
||||
<TooltipActionBtn
|
||||
clickHandler={() => setMessage.timerVisible(!visible)}
|
||||
tooltip={visible ? 'Make invisible' : 'Make visible'}
|
||||
<IconButton
|
||||
aria-label='Toggle timer message visibility'
|
||||
openDelay={tooltipDelayMid}
|
||||
icon={visible ? <IoEye size='18px' /> : <IoEyeOffOutline size='18px' />}
|
||||
variant={visible ? 'ontime-filled' : 'ontime-subtle'}
|
||||
size='sm'
|
||||
/>
|
||||
onClick={() => setMessage.timerVisible(!visible)}
|
||||
variant={visible ? 'primary' : 'subtle'}
|
||||
>
|
||||
{visible ? <IoEye /> : <IoEyeOffOutline />}
|
||||
</IconButton>
|
||||
</InputRow>
|
||||
);
|
||||
}
|
||||
@@ -50,9 +47,9 @@ function SecondaryInput() {
|
||||
|
||||
const toggleSecondary = () => {
|
||||
if (visible) {
|
||||
setMessage.timerSecondary(null);
|
||||
setMessage.timerSecondarySource(null);
|
||||
} else {
|
||||
setMessage.timerSecondary('secondary');
|
||||
setMessage.timerSecondarySource('secondary');
|
||||
}
|
||||
};
|
||||
|
||||
@@ -64,15 +61,13 @@ function SecondaryInput() {
|
||||
visible={visible}
|
||||
changeHandler={(newValue) => setMessage.secondaryMessage(newValue)}
|
||||
>
|
||||
<TooltipActionBtn
|
||||
clickHandler={toggleSecondary}
|
||||
tooltip={visible ? 'Make invisible' : 'Make visible'}
|
||||
<IconButton
|
||||
aria-label='Toggle secondary message visibility'
|
||||
openDelay={tooltipDelayMid}
|
||||
icon={visible ? <IoEye size='18px' /> : <IoEyeOffOutline size='18px' />}
|
||||
variant={visible ? 'ontime-filled' : 'ontime-subtle'}
|
||||
size='sm'
|
||||
/>
|
||||
onClick={toggleSecondary}
|
||||
variant={visible ? 'primary' : 'subtle'}
|
||||
>
|
||||
{visible ? <IoEye /> : <IoEyeOffOutline />}
|
||||
</IconButton>
|
||||
</InputRow>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -11,12 +11,16 @@ import { tooltipDelayMid } from '../../../ontimeConfig';
|
||||
|
||||
import style from './MessageControl.module.scss';
|
||||
|
||||
export default function TimerPreview() {
|
||||
const { blink, blackout, countToEnd, phase, showAuxTimer, showSecondaryMessage, showTimerMessage, timerType } =
|
||||
useMessagePreview();
|
||||
const { data } = useViewSettings();
|
||||
const secondarySourceLabels: Record<string, string> = {
|
||||
aux1: 'Aux 1',
|
||||
aux2: 'Aux 2',
|
||||
aux3: 'Aux 3',
|
||||
secondary: 'Secondary message',
|
||||
};
|
||||
|
||||
const contentClasses = cx([style.previewContent, blink && style.blink, blackout && style.blackout]);
|
||||
export default function TimerPreview() {
|
||||
const { blink, blackout, countToEnd, phase, secondarySource, showTimerMessage, timerType } = useMessagePreview();
|
||||
const { data } = useViewSettings();
|
||||
|
||||
const main = (() => {
|
||||
if (showTimerMessage) return 'Message';
|
||||
@@ -29,13 +33,11 @@ export default function TimerPreview() {
|
||||
})();
|
||||
|
||||
const secondary = (() => {
|
||||
// message is a fullscreen overlay
|
||||
if (showTimerMessage) return null;
|
||||
// message is a fullscreen overlay or secondary is not active
|
||||
if (showTimerMessage || !secondarySource) return null;
|
||||
|
||||
// we need to check aux first since it takes priority
|
||||
if (showAuxTimer) return 'Aux Timer';
|
||||
if (showSecondaryMessage) return 'Secondary message';
|
||||
return null;
|
||||
return secondarySourceLabels[secondarySource];
|
||||
})();
|
||||
|
||||
const overrideColour = (() => {
|
||||
@@ -46,6 +48,7 @@ export default function TimerPreview() {
|
||||
})();
|
||||
|
||||
const showColourOverride = main == 'Timer';
|
||||
const contentClasses = cx([blink && style.blink, blackout && style.blackout]);
|
||||
|
||||
return (
|
||||
<div className={style.preview}>
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
import { Button } from '@chakra-ui/react';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { SecondarySource } from 'ontime-types';
|
||||
|
||||
import Button from '../../../common/components/buttons/Button';
|
||||
import * as Editor from '../../../common/components/editor-utils/EditorUtils';
|
||||
import Select from '../../../common/components/select/Select';
|
||||
import { setMessage, useTimerViewControl } from '../../../common/hooks/useSocket';
|
||||
|
||||
import TimerPreview from './TimerPreview';
|
||||
@@ -8,50 +11,27 @@ import TimerPreview from './TimerPreview';
|
||||
import style from './MessageControl.module.scss';
|
||||
|
||||
export default function TimerControlsPreview() {
|
||||
const { blackout, blink, secondarySource } = useTimerViewControl();
|
||||
|
||||
const toggleSecondary = (newValue: SecondarySource) => {
|
||||
if (secondarySource === newValue) {
|
||||
setMessage.timerSecondary(null);
|
||||
} else {
|
||||
setMessage.timerSecondary(newValue);
|
||||
}
|
||||
};
|
||||
const { blackout, blink } = useTimerViewControl();
|
||||
|
||||
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 === 'secondary' ? 'ontime-filled' : 'ontime-subtle'}
|
||||
onClick={() => toggleSecondary('secondary')}
|
||||
>
|
||||
Show secondary
|
||||
</Button>
|
||||
<SecondarySourceControl />
|
||||
|
||||
<hr className={style.divider} />
|
||||
<Editor.Separator orientation='horizontal' />
|
||||
|
||||
<Button
|
||||
size='sm'
|
||||
variant={blink ? 'ontime-filled' : 'ontime-subtle'}
|
||||
variant={blink ? 'primary' : 'subtle'}
|
||||
fluid
|
||||
onClick={() => setMessage.timerBlink(!blink)}
|
||||
data-testid='toggle timer blink'
|
||||
>
|
||||
Blink
|
||||
</Button>
|
||||
<Button
|
||||
size='sm'
|
||||
className={style.blackoutButton}
|
||||
variant={blackout ? 'ontime-filled' : 'ontime-subtle'}
|
||||
variant={blackout ? 'primary' : 'subtle'}
|
||||
fluid
|
||||
onClick={() => setMessage.timerBlackout(!blackout)}
|
||||
data-testid='toggle timer blackout'
|
||||
>
|
||||
@@ -61,3 +41,55 @@ export default function TimerControlsPreview() {
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function SecondarySourceControl() {
|
||||
const { secondarySource } = useTimerViewControl();
|
||||
const [value, setValue] = useState<SecondarySource>('aux1');
|
||||
|
||||
// sync secondary source with external changes
|
||||
useEffect(() => {
|
||||
if (secondarySource !== null) {
|
||||
setValue(secondarySource);
|
||||
}
|
||||
}, [secondarySource]);
|
||||
|
||||
const toggleSecondary = () => {
|
||||
if (secondarySource === value) {
|
||||
setMessage.timerSecondarySource(null);
|
||||
} else {
|
||||
setMessage.timerSecondarySource(value);
|
||||
}
|
||||
};
|
||||
|
||||
const changeValue = (newValue: SecondarySource) => {
|
||||
// we can only update the remote if it is enabled
|
||||
if (secondarySource !== null) {
|
||||
setMessage.timerSecondarySource(newValue);
|
||||
}
|
||||
setValue(newValue);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<Select
|
||||
value={value}
|
||||
placeholder='Secondary source'
|
||||
options={[
|
||||
{ value: 'aux1', label: 'Aux 1' },
|
||||
{ value: 'aux2', label: 'Aux 2' },
|
||||
{ value: 'aux3', label: 'Aux 3' },
|
||||
{ value: 'secondary', label: 'Secondary message' },
|
||||
]}
|
||||
onChange={changeValue}
|
||||
/>
|
||||
<Button
|
||||
variant={secondarySource !== null ? 'primary' : 'subtle'}
|
||||
fluid
|
||||
onClick={toggleSecondary}
|
||||
data-testid='toggle secondary'
|
||||
>
|
||||
Show secondary
|
||||
</Button>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -2,3 +2,8 @@
|
||||
width: 100%;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.auxTimers {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
@@ -23,7 +23,11 @@ export default function PlaybackControl() {
|
||||
selectedEventIndex={data.selectedEventIndex}
|
||||
timerPhase={data.timerPhase}
|
||||
/>
|
||||
<AuxTimer />
|
||||
<div className={style.auxTimers}>
|
||||
<AuxTimer index={1} />
|
||||
<AuxTimer index={2} />
|
||||
<AuxTimer index={3} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -7,6 +7,18 @@
|
||||
|
||||
.controls {
|
||||
margin-top: 0.25rem;
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.input {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr auto;
|
||||
gap: 0.25rem;
|
||||
}
|
||||
|
||||
.twoSides {
|
||||
margin-top: 0.25rem;
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 0.25rem;
|
||||
height: 1.5rem;
|
||||
}
|
||||
|
||||
@@ -8,71 +8,82 @@ import TapButton from '../tap-button/TapButton';
|
||||
|
||||
import style from './AuxTimer.module.scss';
|
||||
|
||||
export function AuxTimer() {
|
||||
const { playback, direction } = useAuxTimerControl();
|
||||
interface AuxTimerProps {
|
||||
index: number;
|
||||
}
|
||||
|
||||
const { start, pause, stop, setDirection } = setAuxTimer;
|
||||
export function AuxTimer({ index }: AuxTimerProps) {
|
||||
const { playback, direction } = useAuxTimerControl(index);
|
||||
|
||||
const { stop, setDirection } = setAuxTimer;
|
||||
|
||||
const toggleDirection = () => {
|
||||
const newDirection = direction === SimpleDirection.CountDown ? SimpleDirection.CountUp : SimpleDirection.CountDown;
|
||||
setDirection(newDirection);
|
||||
setDirection(index, newDirection);
|
||||
};
|
||||
|
||||
const userCan = {
|
||||
start: playback !== SimplePlayback.Start,
|
||||
pause: playback === SimplePlayback.Start,
|
||||
stop: playback !== SimplePlayback.Stop,
|
||||
};
|
||||
const canStop = playback !== SimplePlayback.Stop;
|
||||
const playbackAction = playback === SimplePlayback.Start ? 'pause' : 'play';
|
||||
|
||||
return (
|
||||
<label className={style.label}>
|
||||
Auxiliary Timer
|
||||
Aux Timer {index}
|
||||
<div className={style.controls}>
|
||||
<AuxTimerInput />
|
||||
<TapButton onClick={toggleDirection} aspect='tight'>
|
||||
{direction === SimpleDirection.CountDown && <IoArrowDown data-testid='aux-timer-direction' />}
|
||||
{direction === SimpleDirection.CountUp && <IoArrowUp data-testid='aux-timer-direction' />}
|
||||
</TapButton>
|
||||
|
||||
<TapButton
|
||||
onClick={start}
|
||||
theme={Playback.Play}
|
||||
active={playback === SimplePlayback.Start}
|
||||
disabled={!userCan.start}
|
||||
>
|
||||
<IoPlay data-testid='aux-timer-start' />
|
||||
</TapButton>
|
||||
<TapButton
|
||||
onClick={pause}
|
||||
theme={Playback.Pause}
|
||||
active={playback === SimplePlayback.Pause}
|
||||
disabled={!userCan.pause}
|
||||
>
|
||||
<IoPause data-testid='aux-timer-pause' />
|
||||
</TapButton>
|
||||
<TapButton onClick={stop} theme={Playback.Stop} disabled={!userCan.stop}>
|
||||
<IoStop data-testid='aux-timer-stop' />
|
||||
</TapButton>
|
||||
<div className={style.input}>
|
||||
<AuxTimerInput index={index} />
|
||||
<TapButton onClick={toggleDirection} aspect='tight'>
|
||||
{direction === SimpleDirection.CountDown && <IoArrowDown data-testid={`aux-timer-direction-${index}`} />}
|
||||
{direction === SimpleDirection.CountUp && <IoArrowUp data-testid={`aux-timer-direction-${index}`} />}
|
||||
</TapButton>
|
||||
</div>
|
||||
<div className={style.twoSides}>
|
||||
<AuxTogglePlay index={index} action={playbackAction} />
|
||||
<TapButton onClick={() => stop(index)} theme={Playback.Stop} disabled={!canStop}>
|
||||
<IoStop data-testid={`aux-timer-stop-${index}`} />
|
||||
</TapButton>
|
||||
</div>
|
||||
</div>
|
||||
</label>
|
||||
);
|
||||
}
|
||||
|
||||
function AuxTimerInput() {
|
||||
const newTimeInMs = useAuxTimerTime();
|
||||
interface AuxTimerInput {
|
||||
index: number;
|
||||
}
|
||||
|
||||
function AuxTimerInput({ index }: AuxTimerProps) {
|
||||
const newTimeInMs = useAuxTimerTime(index);
|
||||
const { setDuration } = setAuxTimer;
|
||||
|
||||
const handleTimeUpdate = (_field: string, value: string) => {
|
||||
const newTimeInMs = parseUserTime(value);
|
||||
setDuration(newTimeInMs);
|
||||
setDuration(index, newTimeInMs);
|
||||
};
|
||||
|
||||
return (
|
||||
<TimeInput<'auxTimer'>
|
||||
submitHandler={handleTimeUpdate}
|
||||
name='auxTimer'
|
||||
time={newTimeInMs}
|
||||
placeholder='Aux Timer 1'
|
||||
/>
|
||||
<TimeInput submitHandler={handleTimeUpdate} name={`aux${index}`} time={newTimeInMs} placeholder={`Aux ${index}`} />
|
||||
);
|
||||
}
|
||||
|
||||
interface AuxTogglePlayProps {
|
||||
index: number;
|
||||
action: 'play' | 'pause';
|
||||
}
|
||||
|
||||
function AuxTogglePlay({ index, action }: AuxTogglePlayProps) {
|
||||
const { start, pause } = setAuxTimer;
|
||||
|
||||
if (action === 'play') {
|
||||
return (
|
||||
<TapButton onClick={() => start(index)} theme={Playback.Play}>
|
||||
<IoPlay data-testid={`aux-timer-start-${index}`} />
|
||||
</TapButton>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<TapButton onClick={() => pause(index)} theme={Playback.Pause}>
|
||||
<IoPause data-testid={`aux-timer-pause-${index}`} />
|
||||
</TapButton>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user