diff --git a/apps/client/src/common/hooks/useSocket.ts b/apps/client/src/common/hooks/useSocket.ts index b64cce05f..bce8b6c16 100644 --- a/apps/client/src/common/hooks/useSocket.ts +++ b/apps/client/src/common/hooks/useSocket.ts @@ -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, }; })(); diff --git a/apps/client/src/common/models/OntimeSettings.ts b/apps/client/src/common/models/OntimeSettings.ts index 26470c7b0..a12319f64 100644 --- a/apps/client/src/common/models/OntimeSettings.ts +++ b/apps/client/src/common/models/OntimeSettings.ts @@ -6,4 +6,5 @@ export const ontimePlaceholderSettings: Settings = { operatorKey: null, timeFormat: '24', language: 'en', + auxTimerNames: ['', '', ''], }; diff --git a/apps/client/src/common/stores/editorSettings.ts b/apps/client/src/common/stores/editorSettings.ts index bedebf976..2686a1d32 100644 --- a/apps/client/src/common/stores/editorSettings.ts +++ b/apps/client/src/common/stores/editorSettings.ts @@ -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((set) => { @@ -67,6 +71,10 @@ export const useEditorSettings = create((set) => { EditorSettingsKeys.InheritGroupColour, editorSettingsDefaults.inheritGroupColour, ), + auxTimersCollapsed: booleanFromLocalStorage( + EditorSettingsKeys.AuxTimersCollapsed, + editorSettingsDefaults.auxTimersCollapsed, + ), setDefaultDuration: (defaultDuration) => set(() => { @@ -110,5 +118,10 @@ export const useEditorSettings = create((set) => { localStorage.setItem(EditorSettingsKeys.InheritGroupColour, String(inheritGroupColour)); return { inheritGroupColour }; }), + setAuxTimersCollapsed: (auxTimersCollapsed) => + set(() => { + localStorage.setItem(EditorSettingsKeys.AuxTimersCollapsed, String(auxTimersCollapsed)); + return { auxTimersCollapsed }; + }), }; }); diff --git a/apps/client/src/common/utils/auxTimerUtils.ts b/apps/client/src/common/utils/auxTimerUtils.ts new file mode 100644 index 000000000..2bb6409ff --- /dev/null +++ b/apps/client/src/common/utils/auxTimerUtils.ts @@ -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}`; +} diff --git a/apps/client/src/features/app-settings/panel/settings-panel/AuxTimerSettings.tsx b/apps/client/src/features/app-settings/panel/settings-panel/AuxTimerSettings.tsx new file mode 100644 index 000000000..d6fdf4a6b --- /dev/null +++ b/apps/client/src/features/app-settings/panel/settings-panel/AuxTimerSettings.tsx @@ -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({ + 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 ( + preventEscape(event, onReset)} + id='aux-timer-settings' + > + + + Aux timers + + + + + + + + Give the aux timers custom names. Names are shown across the editor controls and views. + + {errors.root?.message} + + + + + + + + + + + + + + + + + + ); +} diff --git a/apps/client/src/features/app-settings/panel/settings-panel/SettingsPanel.tsx b/apps/client/src/features/app-settings/panel/settings-panel/SettingsPanel.tsx index 2e6d4473b..d96de65b9 100644 --- a/apps/client/src/features/app-settings/panel/settings-panel/SettingsPanel.tsx +++ b/apps/client/src/features/app-settings/panel/settings-panel/SettingsPanel.tsx @@ -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('data', location); const generalRef = useScrollIntoView('general', location); + const auxTimersRef = useScrollIntoView('aux-timers', location); const viewRef = useScrollIntoView('view', location); const customViewsRef = useScrollIntoView('custom-views', location); const mcpRef = useScrollIntoView('mcp', location); @@ -26,6 +28,9 @@ export default function SettingsPanel({ location }: PanelBaseProps) {
+
+ +
diff --git a/apps/client/src/features/app-settings/useAppSettingsMenu.tsx b/apps/client/src/features/app-settings/useAppSettingsMenu.tsx index 8c7b57f0e..4acc97dde 100644 --- a/apps/client/src/features/app-settings/useAppSettingsMenu.tsx +++ b/apps/client/src/features/app-settings/useAppSettingsMenu.tsx @@ -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', diff --git a/apps/client/src/features/control/playback/PlaybackControl.module.scss b/apps/client/src/features/control/playback/PlaybackControl.module.scss index c3dfc7bdf..402ff6aca 100644 --- a/apps/client/src/features/control/playback/PlaybackControl.module.scss +++ b/apps/client/src/features/control/playback/PlaybackControl.module.scss @@ -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; diff --git a/apps/client/src/features/control/playback/PlaybackControl.tsx b/apps/client/src/features/control/playback/PlaybackControl.tsx index 71cbb42be..6fd54a806 100644 --- a/apps/client/src/features/control/playback/PlaybackControl.tsx +++ b/apps/client/src/features/control/playback/PlaybackControl.tsx @@ -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 (
@@ -20,11 +29,42 @@ export default function PlaybackControl() { selectedEventIndex={data.selectedEventIndex} timerPhase={data.timerPhase} /> -
- - - +
+ + Aux timers + {auxTimersCollapsed && isAuxTimerActive && } + +
+ setLocation('settings__aux-timers')} + /> + } + > + + + setAuxTimersCollapsed(!auxTimersCollapsed)} + > + {auxTimersCollapsed ? : } + +
+ {!auxTimersCollapsed && ( +
+ + + +
+ )}
); } diff --git a/apps/client/src/features/control/playback/aux-timer/AuxTimer.module.scss b/apps/client/src/features/control/playback/aux-timer/AuxTimer.module.scss index 679b038f9..645cfe97f 100644 --- a/apps/client/src/features/control/playback/aux-timer/AuxTimer.module.scss +++ b/apps/client/src/features/control/playback/aux-timer/AuxTimer.module.scss @@ -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 { diff --git a/apps/client/src/features/control/playback/aux-timer/AuxTimer.tsx b/apps/client/src/features/control/playback/aux-timer/AuxTimer.tsx index 3dff8eab5..3c4b0694c 100644 --- a/apps/client/src/features/control/playback/aux-timer/AuxTimer.tsx +++ b/apps/client/src/features/control/playback/aux-timer/AuxTimer.tsx @@ -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 (