mirror of
https://github.com/cpvalente/ontime.git
synced 2026-08-31 20:09:11 +00:00
feat: allow naming aux timers
This commit is contained in:
@@ -95,6 +95,22 @@ export const useAuxTimersTime = createSelector((state: RuntimeStore) => {
|
||||
};
|
||||
});
|
||||
|
||||
export const useAuxTimersName = createSelector((state: RuntimeStore) => {
|
||||
return {
|
||||
aux1: state.auxtimer1.name,
|
||||
aux2: state.auxtimer2.name,
|
||||
aux3: state.auxtimer3.name,
|
||||
};
|
||||
});
|
||||
|
||||
export const useAuxTimersActive = createSelector((state: RuntimeStore) => {
|
||||
return (
|
||||
state.auxtimer1.playback === SimplePlayback.Start ||
|
||||
state.auxtimer2.playback === SimplePlayback.Start ||
|
||||
state.auxtimer3.playback === SimplePlayback.Start
|
||||
);
|
||||
});
|
||||
|
||||
export const useAuxTimerTime = (index: number) =>
|
||||
createSelector((state: RuntimeStore) => {
|
||||
if (index === 1) return state.auxtimer1.current;
|
||||
@@ -108,15 +124,18 @@ export const useAuxTimerControl = (index: number) =>
|
||||
return {
|
||||
playback: state.auxtimer1.playback,
|
||||
direction: state.auxtimer1.direction,
|
||||
name: state.auxtimer1.name,
|
||||
};
|
||||
if (index === 2)
|
||||
return {
|
||||
playback: state.auxtimer2.playback,
|
||||
direction: state.auxtimer2.direction,
|
||||
name: state.auxtimer2.name,
|
||||
};
|
||||
return {
|
||||
playback: state.auxtimer3.playback,
|
||||
direction: state.auxtimer3.direction,
|
||||
name: state.auxtimer3.name,
|
||||
};
|
||||
})();
|
||||
|
||||
|
||||
@@ -6,4 +6,5 @@ export const ontimePlaceholderSettings: Settings = {
|
||||
operatorKey: null,
|
||||
timeFormat: '24',
|
||||
language: 'en',
|
||||
auxTimerNames: ['', '', ''],
|
||||
};
|
||||
|
||||
@@ -13,6 +13,7 @@ type EditorSettingsStore = {
|
||||
defaultTimerType: TimerType;
|
||||
defaultEndAction: EndAction;
|
||||
inheritGroupColour: boolean;
|
||||
auxTimersCollapsed: boolean;
|
||||
setDefaultDuration: (defaultDuration: string) => void;
|
||||
setLinkPrevious: (linkPrevious: boolean) => void;
|
||||
setInheritGroupColour: (inheritGroupColour: boolean) => void;
|
||||
@@ -21,6 +22,7 @@ type EditorSettingsStore = {
|
||||
setDangerTime: (dangerTime: string) => void;
|
||||
setDefaultTimerType: (defaultTimerType: TimerType) => void;
|
||||
setDefaultEndAction: (defaultEndAction: EndAction) => void;
|
||||
setAuxTimersCollapsed: (auxTimersCollapsed: boolean) => void;
|
||||
};
|
||||
|
||||
export const editorSettingsDefaults = {
|
||||
@@ -32,6 +34,7 @@ export const editorSettingsDefaults = {
|
||||
timerType: TimerType.CountDown,
|
||||
endAction: EndAction.None,
|
||||
inheritGroupColour: false,
|
||||
auxTimersCollapsed: false,
|
||||
};
|
||||
|
||||
enum EditorSettingsKeys {
|
||||
@@ -43,6 +46,7 @@ enum EditorSettingsKeys {
|
||||
DefaultTimerType = 'ontime-default-timer-type',
|
||||
DefaultEndAction = 'ontime-default-end-action',
|
||||
InheritGroupColour = 'ontime-inherit-group-colour',
|
||||
AuxTimersCollapsed = 'ontime-aux-timers-collapsed',
|
||||
}
|
||||
|
||||
export const useEditorSettings = create<EditorSettingsStore>((set) => {
|
||||
@@ -67,6 +71,10 @@ export const useEditorSettings = create<EditorSettingsStore>((set) => {
|
||||
EditorSettingsKeys.InheritGroupColour,
|
||||
editorSettingsDefaults.inheritGroupColour,
|
||||
),
|
||||
auxTimersCollapsed: booleanFromLocalStorage(
|
||||
EditorSettingsKeys.AuxTimersCollapsed,
|
||||
editorSettingsDefaults.auxTimersCollapsed,
|
||||
),
|
||||
|
||||
setDefaultDuration: (defaultDuration) =>
|
||||
set(() => {
|
||||
@@ -110,5 +118,10 @@ export const useEditorSettings = create<EditorSettingsStore>((set) => {
|
||||
localStorage.setItem(EditorSettingsKeys.InheritGroupColour, String(inheritGroupColour));
|
||||
return { inheritGroupColour };
|
||||
}),
|
||||
setAuxTimersCollapsed: (auxTimersCollapsed) =>
|
||||
set(() => {
|
||||
localStorage.setItem(EditorSettingsKeys.AuxTimersCollapsed, String(auxTimersCollapsed));
|
||||
return { auxTimersCollapsed };
|
||||
}),
|
||||
};
|
||||
});
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
export function getAuxTimerLabel(name: string | undefined, fallback: string): string {
|
||||
const custom = name?.trim();
|
||||
return custom ? custom : fallback;
|
||||
}
|
||||
|
||||
/** Combines the aux timer's index with its custom name, eg. "Aux 1: Speaker" */
|
||||
export function getAuxTimerIndexedLabel(name: string | undefined, index: number): string {
|
||||
const custom = name?.trim();
|
||||
return custom ? `Aux ${index}: ${custom}` : `Aux ${index}`;
|
||||
}
|
||||
@@ -0,0 +1,95 @@
|
||||
import { Settings } from 'ontime-types';
|
||||
import { auxTimerNameMaxLength } from 'ontime-utils';
|
||||
import { useEffect } from 'react';
|
||||
import { useForm } from 'react-hook-form';
|
||||
|
||||
import { postSettings } from '../../../../common/api/settings';
|
||||
import { maybeAxiosError } from '../../../../common/api/utils';
|
||||
import Button from '../../../../common/components/buttons/Button';
|
||||
import Info from '../../../../common/components/info/Info';
|
||||
import Input from '../../../../common/components/input/input/Input';
|
||||
import useSettings from '../../../../common/hooks-query/useSettings';
|
||||
import { preventEscape } from '../../../../common/utils/keyEvent';
|
||||
import * as Panel from '../../panel-utils/PanelUtils';
|
||||
|
||||
export default function AuxTimerSettings() {
|
||||
const { data, status, refetch } = useSettings();
|
||||
const {
|
||||
handleSubmit,
|
||||
register,
|
||||
reset,
|
||||
setError,
|
||||
formState: { isSubmitting, isDirty, errors },
|
||||
} = useForm<Settings>({
|
||||
defaultValues: data,
|
||||
resetOptions: {
|
||||
keepDirtyValues: true,
|
||||
},
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (data) {
|
||||
reset(data);
|
||||
}
|
||||
}, [data, reset]);
|
||||
|
||||
const onSubmit = async (formData: Settings) => {
|
||||
try {
|
||||
await postSettings(formData);
|
||||
} catch (error) {
|
||||
const message = maybeAxiosError(error);
|
||||
setError('root', { message });
|
||||
} finally {
|
||||
await refetch();
|
||||
}
|
||||
};
|
||||
|
||||
const onReset = () => {
|
||||
reset(data);
|
||||
};
|
||||
|
||||
const isLoading = status === 'pending';
|
||||
|
||||
return (
|
||||
<Panel.Section
|
||||
as='form'
|
||||
onSubmit={handleSubmit(onSubmit)}
|
||||
onKeyDown={(event) => preventEscape(event, onReset)}
|
||||
id='aux-timer-settings'
|
||||
>
|
||||
<Panel.Card>
|
||||
<Panel.SubHeader>
|
||||
Aux timers
|
||||
<Panel.InlineElements>
|
||||
<Button disabled={!isDirty || isSubmitting} variant='ghosted' onClick={onReset}>
|
||||
Revert to saved
|
||||
</Button>
|
||||
<Button type='submit' loading={isSubmitting} disabled={!isDirty} variant='primary'>
|
||||
Save
|
||||
</Button>
|
||||
</Panel.InlineElements>
|
||||
</Panel.SubHeader>
|
||||
<Panel.Divider />
|
||||
<Panel.Section>
|
||||
<Info>Give the aux timers custom names. Names are shown across the editor controls and views.</Info>
|
||||
<Panel.Loader isLoading={isLoading} />
|
||||
<Panel.Error>{errors.root?.message}</Panel.Error>
|
||||
<Panel.ListGroup>
|
||||
<Panel.ListItem>
|
||||
<Panel.Field title='Aux timer 1' description='Custom name for aux timer 1' />
|
||||
<Input maxLength={auxTimerNameMaxLength} placeholder='Aux 1' {...register('auxTimerNames.0')} />
|
||||
</Panel.ListItem>
|
||||
<Panel.ListItem>
|
||||
<Panel.Field title='Aux timer 2' description='Custom name for aux timer 2' />
|
||||
<Input maxLength={auxTimerNameMaxLength} placeholder='Aux 2' {...register('auxTimerNames.1')} />
|
||||
</Panel.ListItem>
|
||||
<Panel.ListItem>
|
||||
<Panel.Field title='Aux timer 3' description='Custom name for aux timer 3' />
|
||||
<Input maxLength={auxTimerNameMaxLength} placeholder='Aux 3' {...register('auxTimerNames.2')} />
|
||||
</Panel.ListItem>
|
||||
</Panel.ListGroup>
|
||||
</Panel.Section>
|
||||
</Panel.Card>
|
||||
</Panel.Section>
|
||||
);
|
||||
}
|
||||
@@ -3,6 +3,7 @@ import { isDocker } from '../../../../externals';
|
||||
import type { PanelBaseProps } from '../../panel-list/PanelList';
|
||||
import * as Panel from '../../panel-utils/PanelUtils';
|
||||
import CustomViews from '../manage-panel/CustomViews';
|
||||
import AuxTimerSettings from './AuxTimerSettings';
|
||||
import GeneralSettings from './GeneralSettings';
|
||||
import McpSection from './McpSection';
|
||||
import ProjectData from './ProjectData';
|
||||
@@ -12,6 +13,7 @@ import ViewSettings from './ViewSettings';
|
||||
export default function SettingsPanel({ location }: PanelBaseProps) {
|
||||
const dataRef = useScrollIntoView<HTMLDivElement>('data', location);
|
||||
const generalRef = useScrollIntoView<HTMLDivElement>('general', location);
|
||||
const auxTimersRef = useScrollIntoView<HTMLDivElement>('aux-timers', location);
|
||||
const viewRef = useScrollIntoView<HTMLDivElement>('view', location);
|
||||
const customViewsRef = useScrollIntoView<HTMLDivElement>('custom-views', location);
|
||||
const mcpRef = useScrollIntoView<HTMLDivElement>('mcp', location);
|
||||
@@ -26,6 +28,9 @@ export default function SettingsPanel({ location }: PanelBaseProps) {
|
||||
<div ref={generalRef}>
|
||||
<GeneralSettings />
|
||||
</div>
|
||||
<div ref={auxTimersRef}>
|
||||
<AuxTimerSettings />
|
||||
</div>
|
||||
<div ref={viewRef}>
|
||||
<ViewSettings />
|
||||
</div>
|
||||
|
||||
@@ -24,6 +24,7 @@ const staticOptions = [
|
||||
label: 'General settings',
|
||||
keywords: ['pin', 'password', 'lock', 'language', 'time format', 'timezone'],
|
||||
},
|
||||
{ id: 'settings__aux-timers', label: 'Aux timers', keywords: ['aux', 'auxiliary'] },
|
||||
{
|
||||
id: 'settings__view',
|
||||
label: 'View settings',
|
||||
|
||||
@@ -3,6 +3,34 @@
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.auxHeader {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-top: 1rem;
|
||||
}
|
||||
|
||||
.label {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
font-size: $inner-section-text-size;
|
||||
color: $label-gray;
|
||||
}
|
||||
|
||||
.auxHeaderButtons {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.25rem;
|
||||
}
|
||||
|
||||
.activeIndicator {
|
||||
width: 0.5rem;
|
||||
height: 0.5rem;
|
||||
border-radius: 50%;
|
||||
background-color: $active-indicator;
|
||||
}
|
||||
|
||||
.auxTimers {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr 1fr;
|
||||
|
||||
@@ -1,4 +1,10 @@
|
||||
import { usePlaybackControl } from '../../../common/hooks/useSocket';
|
||||
import { IoChevronDown, IoChevronUp, IoSettingsOutline } from 'react-icons/io5';
|
||||
|
||||
import IconButton from '../../../common/components/buttons/IconButton';
|
||||
import Tooltip from '../../../common/components/tooltip/Tooltip';
|
||||
import { useAuxTimersActive, usePlaybackControl } from '../../../common/hooks/useSocket';
|
||||
import { useEditorSettings } from '../../../common/stores/editorSettings';
|
||||
import useAppSettingsNavigation from '../../app-settings/useAppSettingsNavigation';
|
||||
import AddTime from './add-time/AddTime';
|
||||
import { AuxTimer } from './aux-timer/AuxTimer';
|
||||
import PlaybackButtons from './playback-buttons/PlaybackButtons';
|
||||
@@ -8,6 +14,9 @@ import style from './PlaybackControl.module.scss';
|
||||
|
||||
export default function PlaybackControl() {
|
||||
const data = usePlaybackControl();
|
||||
const { setLocation } = useAppSettingsNavigation();
|
||||
const { auxTimersCollapsed, setAuxTimersCollapsed } = useEditorSettings();
|
||||
const isAuxTimerActive = useAuxTimersActive();
|
||||
|
||||
return (
|
||||
<div className={style.mainContainer}>
|
||||
@@ -20,11 +29,42 @@ export default function PlaybackControl() {
|
||||
selectedEventIndex={data.selectedEventIndex}
|
||||
timerPhase={data.timerPhase}
|
||||
/>
|
||||
<div className={style.auxTimers}>
|
||||
<AuxTimer index={1} />
|
||||
<AuxTimer index={2} />
|
||||
<AuxTimer index={3} />
|
||||
<div className={style.auxHeader}>
|
||||
<span className={style.label}>
|
||||
Aux timers
|
||||
{auxTimersCollapsed && isAuxTimerActive && <span className={style.activeIndicator} />}
|
||||
</span>
|
||||
<div className={style.auxHeaderButtons}>
|
||||
<Tooltip
|
||||
text='Name aux timers'
|
||||
render={
|
||||
<IconButton
|
||||
size='small'
|
||||
variant='subtle-white'
|
||||
aria-label='Name aux timers'
|
||||
onClick={() => setLocation('settings__aux-timers')}
|
||||
/>
|
||||
}
|
||||
>
|
||||
<IoSettingsOutline />
|
||||
</Tooltip>
|
||||
<IconButton
|
||||
size='small'
|
||||
variant='subtle-white'
|
||||
aria-label={auxTimersCollapsed ? 'Expand aux timers' : 'Collapse aux timers'}
|
||||
onClick={() => setAuxTimersCollapsed(!auxTimersCollapsed)}
|
||||
>
|
||||
{auxTimersCollapsed ? <IoChevronUp /> : <IoChevronDown />}
|
||||
</IconButton>
|
||||
</div>
|
||||
</div>
|
||||
{!auxTimersCollapsed && (
|
||||
<div className={style.auxTimers}>
|
||||
<AuxTimer index={1} />
|
||||
<AuxTimer index={2} />
|
||||
<AuxTimer index={3} />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,8 +1,18 @@
|
||||
.label {
|
||||
display: block;
|
||||
margin-top: 1rem;
|
||||
// aux timers sit in a 3 column grid, without this a long name would grow its column
|
||||
// instead of shrinking to it, breaking the equal column layout
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.labelText {
|
||||
display: block;
|
||||
font-size: $inner-section-text-size;
|
||||
color: $label-gray;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.controls {
|
||||
|
||||
@@ -3,7 +3,9 @@ import { millisToString, parseUserTime } from 'ontime-utils';
|
||||
import { IoArrowDown, IoArrowUp, IoPause, IoPlay, IoStop } from 'react-icons/io5';
|
||||
|
||||
import TimeInput from '../../../../common/components/input/time-input/TimeInput';
|
||||
import Tooltip from '../../../../common/components/tooltip/Tooltip';
|
||||
import { setAuxTimer, useAuxTimerControl, useAuxTimerTime } from '../../../../common/hooks/useSocket';
|
||||
import { getAuxTimerIndexedLabel } from '../../../../common/utils/auxTimerUtils';
|
||||
import TapButton from '../tap-button/TapButton';
|
||||
|
||||
import style from './AuxTimer.module.scss';
|
||||
@@ -13,10 +15,12 @@ interface AuxTimerProps {
|
||||
}
|
||||
|
||||
export function AuxTimer({ index }: AuxTimerProps) {
|
||||
const { playback, direction } = useAuxTimerControl(index);
|
||||
const { playback, direction, name } = useAuxTimerControl(index);
|
||||
|
||||
const { stop, setDirection } = setAuxTimer;
|
||||
|
||||
const label = getAuxTimerIndexedLabel(name, index);
|
||||
|
||||
const toggleDirection = () => {
|
||||
const newDirection = direction === SimpleDirection.CountDown ? SimpleDirection.CountUp : SimpleDirection.CountDown;
|
||||
setDirection(index, newDirection);
|
||||
@@ -27,10 +31,12 @@ export function AuxTimer({ index }: AuxTimerProps) {
|
||||
|
||||
return (
|
||||
<label className={style.label}>
|
||||
Aux Timer {index}
|
||||
<Tooltip text={label} render={<span />} className={style.labelText}>
|
||||
{label}
|
||||
</Tooltip>
|
||||
<div className={style.controls}>
|
||||
<div className={style.input}>
|
||||
<AuxTimerInput index={index} isActive={isActive} />
|
||||
<AuxTimerInput index={index} isActive={isActive} placeholder={`Aux ${index}`} />
|
||||
<TapButton onClick={toggleDirection} aspect='tight' disabled={isActive}>
|
||||
{direction === SimpleDirection.CountDown && <IoArrowDown data-testid={`aux-timer-direction-${index}`} />}
|
||||
{direction === SimpleDirection.CountUp && <IoArrowUp data-testid={`aux-timer-direction-${index}`} />}
|
||||
@@ -50,9 +56,10 @@ export function AuxTimer({ index }: AuxTimerProps) {
|
||||
interface AuxTimerInputProps {
|
||||
index: number;
|
||||
isActive: boolean;
|
||||
placeholder: string;
|
||||
}
|
||||
|
||||
function AuxTimerInput({ index, isActive }: AuxTimerInputProps) {
|
||||
function AuxTimerInput({ index, isActive, placeholder }: AuxTimerInputProps) {
|
||||
const newTimeInMs = useAuxTimerTime(index);
|
||||
const { setDuration } = setAuxTimer;
|
||||
|
||||
@@ -70,7 +77,7 @@ function AuxTimerInput({ index, isActive }: AuxTimerInputProps) {
|
||||
}
|
||||
|
||||
return (
|
||||
<TimeInput submitHandler={handleTimeUpdate} name={`aux${index}`} time={newTimeInMs} placeholder={`Aux ${index}`} />
|
||||
<TimeInput submitHandler={handleTimeUpdate} name={`aux${index}`} time={newTimeInMs} placeholder={placeholder} />
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import { Playback, TimerPhase, ViewSettings } from 'ontime-types';
|
||||
import { millisToString } from 'ontime-utils';
|
||||
|
||||
import { useAuxTimersTime, useStudioTimersSocket } from '../../common/hooks/useSocket';
|
||||
import { useAuxTimersName, useAuxTimersTime, useStudioTimersSocket } from '../../common/hooks/useSocket';
|
||||
import { getAuxTimerLabel } from '../../common/utils/auxTimerUtils';
|
||||
import { getOffsetState } from '../../common/utils/offset';
|
||||
import { cx } from '../../common/utils/styleUtils';
|
||||
import { useTranslation } from '../../translation/TranslationProvider';
|
||||
@@ -121,22 +122,23 @@ export default function StudioTimers({ viewSettings }: StudioTimersProps) {
|
||||
|
||||
function StudioTimersAux() {
|
||||
const auxTimer = useAuxTimersTime();
|
||||
const auxName = useAuxTimersName();
|
||||
|
||||
return (
|
||||
<div className='card' id='card-aux'>
|
||||
<div className='card__row'>
|
||||
<div>
|
||||
<div className='label'>Aux 1</div>
|
||||
<div className='label'>{getAuxTimerLabel(auxName.aux1, 'Aux 1')}</div>
|
||||
<div className='extra'>{millisToString(auxTimer.aux1)}</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div className='label center'>Aux 2</div>
|
||||
<div className='label center'>{getAuxTimerLabel(auxName.aux2, 'Aux 2')}</div>
|
||||
<div className='extra center'>{millisToString(auxTimer.aux2)}</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div className='label right'>Aux 3</div>
|
||||
<div className='label right'>{getAuxTimerLabel(auxName.aux3, 'Aux 3')}</div>
|
||||
<div className='extra right'>{millisToString(auxTimer.aux3)}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user