diff --git a/apps/client/src/common/components/timer-display/TimerDisplay.scss b/apps/client/src/common/components/timer-display/TimerDisplay.scss index 698d9ddae..59db74db7 100644 --- a/apps/client/src/common/components/timer-display/TimerDisplay.scss +++ b/apps/client/src/common/components/timer-display/TimerDisplay.scss @@ -3,18 +3,12 @@ .timer { font-family: var(--font-family-override, $viewer-font-family); color: var(--timer-color-override, $timer-color); - font-size: 20vw; line-height: 0.9em; text-align: center; - letter-spacing: 0.05em; + letter-spacing: 0.1em; font-weight: 600; - - &--small { - font-size: 3.75em; - text-align: center; - letter-spacing: 0.1em; - } - + font-size: 3.75em; + &--finished { color: $timer-finished-color; } diff --git a/apps/client/src/common/components/timer-display/TimerDisplay.tsx b/apps/client/src/common/components/timer-display/TimerDisplay.tsx index de28d1667..7af63835f 100644 --- a/apps/client/src/common/components/timer-display/TimerDisplay.tsx +++ b/apps/client/src/common/components/timer-display/TimerDisplay.tsx @@ -6,9 +6,6 @@ import './TimerDisplay.scss'; interface TimerDisplayProps { time?: number | null; - small?: boolean; - hideZeroHours?: boolean; - className?: string; } /** @@ -17,15 +14,19 @@ interface TimerDisplayProps { * @constructor */ const TimerDisplay = (props: TimerDisplayProps) => { - const { time, small, hideZeroHours, className = '' } = props; + const { time } = props; - const display = - (time === null || typeof time === 'undefined' || isNaN(time)) - ? '-- : -- : --' - : formatDisplay(millisToSeconds(time), hideZeroHours); + let display = ''; + + if (time === null || typeof time === 'undefined' || isNaN(time)) { + display = '-- : -- : --'; + } else { + display = formatDisplay(millisToSeconds(time)); + } const isNegative = (time ?? 0) < 0; - const classes = `timer ${small ? 'timer--small' : ''} ${isNegative ? 'timer--finished' : ''} ${className}`; + + const classes = `timer ${isNegative ? 'timer--finished' : ''}`; return
{display}
; }; diff --git a/apps/client/src/common/models/TimeManager.type.ts b/apps/client/src/common/models/TimeManager.type.ts index b96ddc92a..7e1860395 100644 --- a/apps/client/src/common/models/TimeManager.type.ts +++ b/apps/client/src/common/models/TimeManager.type.ts @@ -1,9 +1,12 @@ -import { Playback } from 'ontime-types'; +import { Playback, TimerType } from 'ontime-types'; export type TimeManagerType = { clock: number; current: null | number; elapsed: null | number; + duration: null | number; + timerBehaviour?: string; + timerType: TimerType; expectedFinish: null | number; addedTime: number; startedAt: null | number; diff --git a/apps/client/src/features/control/playback/PlaybackTimer.tsx b/apps/client/src/features/control/playback/PlaybackTimer.tsx index c76ab82e0..c4dd8e2dd 100644 --- a/apps/client/src/features/control/playback/PlaybackTimer.tsx +++ b/apps/client/src/features/control/playback/PlaybackTimer.tsx @@ -44,7 +44,7 @@ export default function PlaybackTimer(props: PlaybackTimerProps) {
- +
{isWaiting ? (
diff --git a/apps/client/src/features/event-editor/EventEditor.tsx b/apps/client/src/features/event-editor/EventEditor.tsx index 50a336ed3..5ffc5d8c2 100644 --- a/apps/client/src/features/event-editor/EventEditor.tsx +++ b/apps/client/src/features/event-editor/EventEditor.tsx @@ -2,7 +2,7 @@ import { useCallback, useContext, useEffect, useState } from 'react'; import { Button, Select, Switch } from '@chakra-ui/react'; import { IoBan } from '@react-icons/all-files/io5/IoBan'; import { useAtom } from 'jotai'; -import { OntimeEvent } from 'ontime-types'; +import { OntimeEvent, TimerType } from 'ontime-types'; import { editorEventId } from '../../common/atoms/LocalEventSettings'; import CopyTag from '../../common/components/copy-tag/CopyTag'; @@ -84,7 +84,8 @@ export default function EventEditor() { [emitError, event, updateEvent], ); - const timerValidationHandler = useCallback((entry: TimeEntryField, val: number) => { + const timerValidationHandler = useCallback( + (entry: TimeEntryField, val: number) => { if (!event) { return; } @@ -97,7 +98,15 @@ export default function EventEditor() { [event, emitWarning], ); - const togglePublic = useCallback((currentValue: boolean) => { + const handleChange = useCallback( + (field: string, value: string) => { + updateEvent({ id: event.id, [field]: value }); + }, + [event, updateEvent], + ); + + const togglePublic = useCallback( + (currentValue: boolean) => { if (!event) { return; } @@ -111,9 +120,7 @@ export default function EventEditor() { } const delayed = delay !== 0; - const addedTime = delayed - ? `${delay >= 0 ? '+' : '-'} ${millisToMinutes(Math.abs(delay))} minutes` - : null; + const addedTime = delayed ? `${delay >= 0 ? '+' : '-'} ${millisToMinutes(Math.abs(delay))} minutes` : null; const newStart = delayed ? `New start ${stringFromMillis(event.timeStart + delay)}` : null; const newEnd = delayed ? `New end ${stringFromMillis(event.timeEnd + delay)}` : null; @@ -162,26 +169,32 @@ export default function EventEditor() { />
- - handleChange('timerBehaviour', event.target.value)} + > + + + + - - handleChange('timerType', event.target.value)} + > + + +
@@ -194,11 +207,7 @@ export default function EventEditor() {
- +
@@ -209,30 +218,15 @@ export default function EventEditor() {
- -
- +
diff --git a/apps/client/src/features/viewers/common/viewerUtils.ts b/apps/client/src/features/viewers/common/viewerUtils.ts new file mode 100644 index 000000000..6b2b9f858 --- /dev/null +++ b/apps/client/src/features/viewers/common/viewerUtils.ts @@ -0,0 +1,44 @@ +import { TimerType } from 'ontime-types'; + +import { TimeManagerType } from '../../../common/models/TimeManager.type'; +import { formatDisplay, millisToSeconds } from '../../../common/utils/dateConfig'; +import { formatTime } from '../../../common/utils/time'; + +const formatOptions = { + showSeconds: true, + format: 'hh:mm:ss a', +}; + +type TimerTypeParams = Pick; + +export function getTimerByType(timerObject?: TimerTypeParams): string | number | null { + let timer = null; + if (!timerObject) { + return timer; + } + + if (timerObject.timerType === TimerType.CountDown) { + timer = timerObject.current; + } else if (timerObject.timerType === TimerType.CountUp) { + //todo: fix counting down issue in backend later + timer = (timerObject.elapsed ?? 0) < 0 ? 0 : timerObject.elapsed; + } else if (timerObject.timerType === TimerType.Clock) { + timer = formatTime(timerObject.clock, formatOptions); + } + + return timer; +} + +export function formatTimerDisplay(timer?: string | number | null): string { + let display = ''; + + if (typeof timer === 'string') { + display = timer; + } else if (timer === null || typeof timer === 'undefined' || isNaN(timer)) { + display = '-- : -- : --'; + } else { + display = formatDisplay(millisToSeconds(timer), true); + } + + return display; +} diff --git a/apps/client/src/features/viewers/minimal-timer/MinimalTimer.tsx b/apps/client/src/features/viewers/minimal-timer/MinimalTimer.tsx index 0d232c076..fd2ababe0 100644 --- a/apps/client/src/features/viewers/minimal-timer/MinimalTimer.tsx +++ b/apps/client/src/features/viewers/minimal-timer/MinimalTimer.tsx @@ -1,7 +1,7 @@ import { useEffect } from 'react'; import { useSearchParams } from 'react-router-dom'; import { useAtom } from 'jotai'; -import { EventData, Message, ViewSettings } from 'ontime-types'; +import { EventData, Message, TimerType, ViewSettings } from 'ontime-types'; import { overrideStylesURL } from '../../../common/api/apiConstants'; import { mirrorViewersAtom } from '../../../common/atoms/ViewerSettings'; @@ -9,7 +9,7 @@ import NavigationMenu from '../../../common/components/navigation-menu/Navigatio import { useRuntimeStylesheet } from '../../../common/hooks/useRuntimeStylesheet'; import { TimeManagerType } from '../../../common/models/TimeManager.type'; import { OverridableOptions } from '../../../common/models/View.types'; -import { formatDisplay, millisToSeconds } from '../../../common/utils/dateConfig'; +import { formatTimerDisplay, getTimerByType } from '../common/viewerUtils'; import './MinimalTimer.scss'; @@ -128,23 +128,24 @@ export default function MinimalTimer(props: MinimalTimerProps) { const showOverlay = pres.text !== '' && pres.visible; const isPlaying = time.playback !== 'pause'; - const isNegative = (time.current ?? 0) < 0; - const showFinished = isNegative && !userOptions?.hideOvertime; + const isNegative = + (time.current ?? 0) < 0 && time.timerType !== TimerType.Clock && time.timerType !== TimerType.CountUp; const showEndMessage = time.current < 0 && general.endMessage && !hideEndMessage; + const showFinished = + time.finished && !userOptions?.hideOvertime && (time.timerType !== TimerType.Clock || showEndMessage); + + const stageTimer = getTimerByType(time); + let display = formatTimerDisplay(stageTimer); + if (isNegative) { + display = `-${display}`; + } + const stageTimerCharacters = display.replace('/:/g', '').length; + + const timerFontSize = (89 / (stageTimerCharacters - 1)) * (userOptions.size || 1); + const timerClasseNames = `timer ${!isPlaying ? 'timer--paused' : ''} ${showFinished ? 'timer--finished' : ''}`; const baseClasses = `minimal-timer ${isMirrored ? 'mirror' : ''}`; - let stageTimer; - if (time.current === null) { - stageTimer = '- - : - -'; - } else { - stageTimer = formatDisplay(Math.abs(millisToSeconds(time.current)), true); - if (time.current < 0) { - stageTimer = `-${stageTimer}`; - } - } - const stageTimerCharacters = stageTimer.replace('/:/g', '').length; - return (
{general.endMessage}
) : (
- {stageTimer} + {display}
)} diff --git a/apps/client/src/features/viewers/timer/Timer.scss b/apps/client/src/features/viewers/timer/Timer.scss index 6a12c2ab7..665add342 100644 --- a/apps/client/src/features/viewers/timer/Timer.scss +++ b/apps/client/src/features/viewers/timer/Timer.scss @@ -35,6 +35,7 @@ grid-area: clock; margin-left: auto; font-weight: 600; + transition: opacity $viewer-transition-time; .label { font-size: clamp(16px, 1.5vw, 24px); @@ -47,6 +48,10 @@ color: var(--secondary-color-override, $viewer-secondary-color); letter-spacing: 0.05em; } + + &--hidden { + opacity: 0; + } } /* =================== TITLES ===================*/ @@ -71,7 +76,6 @@ grid-area: timer; justify-self: center; align-self: center; - color: var(--timer-color-override, $timer-color); .end-message { text-align: center; @@ -84,6 +88,22 @@ .timer { opacity: 1; + font-size: 20vw; + font-family: var(--font-family-override, $viewer-font-family); + color: var(--timer-color-override, $timer-color); + line-height: 0.9em; + text-align: center; + letter-spacing: 0.05em; + font-weight: 600; + + &--paused { + opacity: $viewer-opacity-disabled; + transition: $viewer-transition-time; + } + + &--finished { + color: $timer-finished-color; + } } } @@ -93,13 +113,12 @@ margin: 0 auto; opacity: 1; transition: $viewer-transition-time; - } - .timer--paused, - .progress-container--paused { + &--paused { opacity: $viewer-opacity-disabled; transition: $viewer-transition-time; } +} /* =================== OVERLAY ===================*/ diff --git a/apps/client/src/features/viewers/timer/Timer.tsx b/apps/client/src/features/viewers/timer/Timer.tsx index 9e3758a75..457f7d7a9 100644 --- a/apps/client/src/features/viewers/timer/Timer.tsx +++ b/apps/client/src/features/viewers/timer/Timer.tsx @@ -1,16 +1,17 @@ import { useEffect } from 'react'; import { AnimatePresence, motion } from 'framer-motion'; import { useAtom } from 'jotai'; +import { TimerType } from 'ontime-types'; import PropTypes from 'prop-types'; import { overrideStylesURL } from '../../../common/api/apiConstants'; import { mirrorViewersAtom } from '../../../common/atoms/ViewerSettings'; import NavigationMenu from '../../../common/components/navigation-menu/NavigationMenu'; import ProgressBar from '../../../common/components/progress-bar/ProgressBar'; -import TimerDisplay from '../../../common/components/timer-display/TimerDisplay'; import TitleCard from '../../../common/components/title-card/TitleCard'; import { useRuntimeStylesheet } from '../../../common/hooks/useRuntimeStylesheet'; import { formatTime } from '../../../common/utils/time'; +import { formatTimerDisplay, getTimerByType } from '../common/viewerUtils'; import './Timer.scss'; @@ -61,29 +62,49 @@ export default function Timer(props) { const clock = formatTime(time.clock, formatOptions); const showOverlay = pres.text !== '' && pres.visible; const isPlaying = time.playback !== 'pause'; - const normalisedTime = time.current === null ? null : Math.max(time.current, 0); + const isNegative = + (time.current ?? 0) < 0 && time.timerType !== TimerType.Clock && time.timerType !== TimerType.CountUp; + + const showEndMessage = time.current < 0 && general.endMessage; const showProgress = time.playback !== 'stop'; + const showFinished = time.finished && (time.timerType !== TimerType.Clock || showEndMessage); + const showClock = time.timerType !== TimerType.Clock; const baseClasses = `stage-timer ${isMirrored ? 'mirror' : ''}`; + const stageTimer = getTimerByType(time); + let display = formatTimerDisplay(stageTimer); + const stageTimerCharacters = display.replace('/:/g', '').length; + if (isNegative) { + display = `-${display}`; + } + + const timerFontSize = 100 / stageTimerCharacters; + const timerClasseNames = `timer ${!isPlaying ? 'timer--paused' : ''} ${showFinished ? 'timer--finished' : ''}`; + return ( -
+
{pres.text}
-
+
Time Now
{clock}
- {time.finished ? ( -
- {!general.endMessage ? : general.endMessage} -
+ {showEndMessage ? ( +
{general.endMessage}
) : ( - +
+ {display} +
)}
diff --git a/apps/server/src/models/eventsDefinition.js b/apps/server/src/models/eventsDefinition.js deleted file mode 100644 index 1eaec867c..000000000 --- a/apps/server/src/models/eventsDefinition.js +++ /dev/null @@ -1,35 +0,0 @@ -export const event = { - title: '', - subtitle: '', - presenter: '', - note: '', - timeType: 'start-end', - timeStart: 0, - timeEnd: 0, - duration: 0, - isPublic: false, - skip: false, - colour: '', - user0: '', - user1: '', - user2: '', - user3: '', - user4: '', - user5: '', - user6: '', - user7: '', - user8: '', - user9: '', - type: 'event', - revision: 0, -}; - -export const delay = { - duration: 0, - type: 'delay', - revision: 0, -}; - -export const block = { - type: 'block', -}; diff --git a/apps/server/src/models/eventsDefinition.ts b/apps/server/src/models/eventsDefinition.ts new file mode 100644 index 000000000..769b076d3 --- /dev/null +++ b/apps/server/src/models/eventsDefinition.ts @@ -0,0 +1,38 @@ +import { OntimeBlock, OntimeDelay, OntimeEvent, SupportedEvent, TimerType } from 'ontime-types'; + +export const event: Omit = { + title: '', + subtitle: '', + presenter: '', + note: '', + timerBehaviour: 'start-end', + timerType: TimerType.CountDown, + timeStart: 0, + timeEnd: 0, + duration: 0, + isPublic: false, + skip: false, + colour: '', + user0: '', + user1: '', + user2: '', + user3: '', + user4: '', + user5: '', + user6: '', + user7: '', + user8: '', + user9: '', + type: SupportedEvent.Event, + revision: 0, +}; + +export const delay: Omit = { + duration: 0, + type: SupportedEvent.Delay, + revision: 0, +}; + +export const block: Omit = { + type: SupportedEvent.Block, +}; diff --git a/apps/server/src/services/TimerService.ts b/apps/server/src/services/TimerService.ts index 96f1f4aff..a68055453 100644 --- a/apps/server/src/services/TimerService.ts +++ b/apps/server/src/services/TimerService.ts @@ -1,4 +1,4 @@ -import { TimerLifeCycle } from 'ontime-types'; +import { TimerLifeCycle, TimerType } from 'ontime-types'; import { eventStore } from '../stores/EventStore.js'; import { PlaybackService } from './PlaybackService.js'; @@ -27,6 +27,7 @@ export class TimerService { secondaryTimer: number | null; selectedEventId: string | null; duration: number | null; + timerType: TimerType | null; }; /** @@ -91,6 +92,7 @@ export class TimerService { secondaryTimer: null, selectedEventId: null, duration: null, + timerType: null, }; this.loadedTimerId = null; this._pausedInterval = 0; @@ -121,6 +123,7 @@ export class TimerService { // update relevant information and force update this.timer.duration = timer.duration; + this.timer.timerType = timer.timerType; // this might not be ideal this.timer.finishedAt = null; @@ -138,7 +141,8 @@ export class TimerService { * @param {number} timer.timeStart * @param {number} timer.timeEnd * @param {number} timer.duration - * @param {string} timer.timeType + * @param {string} timer.timerBehaviour + * @param {string} timer.timerType * @param {boolean} timer.skip */ load(timer) { @@ -152,6 +156,7 @@ export class TimerService { this.timer.duration = timer.duration; this.timer.current = timer.duration; this.playback = 'armed'; + this.timer.timerType = timer.timerType; this._pausedInterval = 0; this._pausedAt = 0; diff --git a/apps/server/src/utils/parser.js b/apps/server/src/utils/parser.js index 9e663c979..100fd86a4 100644 --- a/apps/server/src/utils/parser.js +++ b/apps/server/src/utils/parser.js @@ -310,7 +310,8 @@ export const validateEvent = (eventArgs) => { presenter: makeString(e.presenter, d.presenter), timeStart: start, timeEnd: end, - timeType: 'start-end', + timerBehaviour: makeString(e.timerBehaviour, d.timerBehaviour), + timerType: makeString(e.timerType, d.timerType), duration: validateDuration(start, end), isPublic: typeof e.isPublic === 'boolean' ? e.isPublic : d.isPublic, skip: typeof e.skip === 'boolean' ? e.skip : d.skip, diff --git a/packages/types/src/definitions/TimerType.type.ts b/packages/types/src/definitions/TimerType.type.ts index 69150eeae..25446c941 100644 --- a/packages/types/src/definitions/TimerType.type.ts +++ b/packages/types/src/definitions/TimerType.type.ts @@ -1,4 +1,4 @@ -export enum TimerTypeType { +export enum TimerType { CountDown = 'count-down', CountUp = 'count-up', Clock = 'clock' diff --git a/packages/types/src/definitions/core/OntimeEvent.type.ts b/packages/types/src/definitions/core/OntimeEvent.type.ts index ed73eb658..6d2fa4732 100644 --- a/packages/types/src/definitions/core/OntimeEvent.type.ts +++ b/packages/types/src/definitions/core/OntimeEvent.type.ts @@ -1,3 +1,5 @@ +import { TimerType } from '../TimerType.type.js'; + export enum SupportedEvent { Event = 'event', Delay = 'delay', @@ -26,7 +28,8 @@ export type OntimeEvent = OntimeBaseEvent & { subtitle: string; presenter: string; note: string; - timeType?: string; + timerBehaviour: 'start-end', + timerType: TimerType, timeStart: number; timeEnd: number; duration: number; diff --git a/packages/types/src/index.ts b/packages/types/src/index.ts index 68ee07488..f07fd7389 100644 --- a/packages/types/src/index.ts +++ b/packages/types/src/index.ts @@ -14,7 +14,7 @@ import { OSCSettings, OscSubscription } from './definitions/core/OscSettings.typ import { Playback } from './definitions/runtime/Playback.type.js'; import { Settings } from './definitions/core/Settings.type.js'; import { TimerLifeCycle } from './definitions/core/TimerLifecycle.type.js'; -import { TimerTypeType } from './definitions/TimerType.type.js'; +import { TimerType } from './definitions/TimerType.type.js'; import { UserFields } from './definitions/core/UserFields.type.js'; import { ViewSettings } from './definitions/core/Views.type.js'; @@ -22,10 +22,10 @@ import { ViewSettings } from './definitions/core/Views.type.js'; export type { DatabaseModel }; // ---> Rundown -export type { TimerTypeType }; +export { TimerType }; +export { SupportedEvent }; export type { OntimeBaseEvent, OntimeBlock, OntimeDelay, OntimeEvent }; export type { OntimeRundown, OntimeRundownEntry }; -export { SupportedEvent }; // ---> Event export type { EventData };