mirror of
https://github.com/cpvalente/ontime.git
synced 2026-09-10 00:29:41 +00:00
Feat/countdown style (#290)
* feat: Countdown Style --------- Co-authored-by: Fabian Posenau <fabian@fphome.de> Co-authored-by: cv <34649812+cpvalente@users.noreply.github.com> Co-authored-by: Fabian Posenau <19673098+kellhogs@users.noreply.github.com>
This commit is contained in:
@@ -3,18 +3,12 @@
|
|||||||
.timer {
|
.timer {
|
||||||
font-family: var(--font-family-override, $viewer-font-family);
|
font-family: var(--font-family-override, $viewer-font-family);
|
||||||
color: var(--timer-color-override, $timer-color);
|
color: var(--timer-color-override, $timer-color);
|
||||||
font-size: 20vw;
|
|
||||||
line-height: 0.9em;
|
line-height: 0.9em;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
letter-spacing: 0.05em;
|
letter-spacing: 0.1em;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
|
font-size: 3.75em;
|
||||||
&--small {
|
|
||||||
font-size: 3.75em;
|
|
||||||
text-align: center;
|
|
||||||
letter-spacing: 0.1em;
|
|
||||||
}
|
|
||||||
|
|
||||||
&--finished {
|
&--finished {
|
||||||
color: $timer-finished-color;
|
color: $timer-finished-color;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,9 +6,6 @@ import './TimerDisplay.scss';
|
|||||||
|
|
||||||
interface TimerDisplayProps {
|
interface TimerDisplayProps {
|
||||||
time?: number | null;
|
time?: number | null;
|
||||||
small?: boolean;
|
|
||||||
hideZeroHours?: boolean;
|
|
||||||
className?: string;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -17,15 +14,19 @@ interface TimerDisplayProps {
|
|||||||
* @constructor
|
* @constructor
|
||||||
*/
|
*/
|
||||||
const TimerDisplay = (props: TimerDisplayProps) => {
|
const TimerDisplay = (props: TimerDisplayProps) => {
|
||||||
const { time, small, hideZeroHours, className = '' } = props;
|
const { time } = props;
|
||||||
|
|
||||||
const display =
|
let display = '';
|
||||||
(time === null || typeof time === 'undefined' || isNaN(time))
|
|
||||||
? '-- : -- : --'
|
if (time === null || typeof time === 'undefined' || isNaN(time)) {
|
||||||
: formatDisplay(millisToSeconds(time), hideZeroHours);
|
display = '-- : -- : --';
|
||||||
|
} else {
|
||||||
|
display = formatDisplay(millisToSeconds(time));
|
||||||
|
}
|
||||||
|
|
||||||
const isNegative = (time ?? 0) < 0;
|
const isNegative = (time ?? 0) < 0;
|
||||||
const classes = `timer ${small ? 'timer--small' : ''} ${isNegative ? 'timer--finished' : ''} ${className}`;
|
|
||||||
|
const classes = `timer ${isNegative ? 'timer--finished' : ''}`;
|
||||||
|
|
||||||
return <div className={classes}>{display}</div>;
|
return <div className={classes}>{display}</div>;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,9 +1,12 @@
|
|||||||
import { Playback } from 'ontime-types';
|
import { Playback, TimerType } from 'ontime-types';
|
||||||
|
|
||||||
export type TimeManagerType = {
|
export type TimeManagerType = {
|
||||||
clock: number;
|
clock: number;
|
||||||
current: null | number;
|
current: null | number;
|
||||||
elapsed: null | number;
|
elapsed: null | number;
|
||||||
|
duration: null | number;
|
||||||
|
timerBehaviour?: string;
|
||||||
|
timerType: TimerType;
|
||||||
expectedFinish: null | number;
|
expectedFinish: null | number;
|
||||||
addedTime: number;
|
addedTime: number;
|
||||||
startedAt: null | number;
|
startedAt: null | number;
|
||||||
|
|||||||
@@ -44,7 +44,7 @@ export default function PlaybackTimer(props: PlaybackTimerProps) {
|
|||||||
</Tooltip>
|
</Tooltip>
|
||||||
</div>
|
</div>
|
||||||
<div className={style.timer}>
|
<div className={style.timer}>
|
||||||
<TimerDisplay time={isWaiting ? timerData.secondaryTimer : timerData.current} small />
|
<TimerDisplay time={isWaiting ? timerData.secondaryTimer : timerData.current} />
|
||||||
</div>
|
</div>
|
||||||
{isWaiting ? (
|
{isWaiting ? (
|
||||||
<div className={style.roll}>
|
<div className={style.roll}>
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { useCallback, useContext, useEffect, useState } from 'react';
|
|||||||
import { Button, Select, Switch } from '@chakra-ui/react';
|
import { Button, Select, Switch } from '@chakra-ui/react';
|
||||||
import { IoBan } from '@react-icons/all-files/io5/IoBan';
|
import { IoBan } from '@react-icons/all-files/io5/IoBan';
|
||||||
import { useAtom } from 'jotai';
|
import { useAtom } from 'jotai';
|
||||||
import { OntimeEvent } from 'ontime-types';
|
import { OntimeEvent, TimerType } from 'ontime-types';
|
||||||
|
|
||||||
import { editorEventId } from '../../common/atoms/LocalEventSettings';
|
import { editorEventId } from '../../common/atoms/LocalEventSettings';
|
||||||
import CopyTag from '../../common/components/copy-tag/CopyTag';
|
import CopyTag from '../../common/components/copy-tag/CopyTag';
|
||||||
@@ -84,7 +84,8 @@ export default function EventEditor() {
|
|||||||
[emitError, event, updateEvent],
|
[emitError, event, updateEvent],
|
||||||
);
|
);
|
||||||
|
|
||||||
const timerValidationHandler = useCallback((entry: TimeEntryField, val: number) => {
|
const timerValidationHandler = useCallback(
|
||||||
|
(entry: TimeEntryField, val: number) => {
|
||||||
if (!event) {
|
if (!event) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -97,7 +98,15 @@ export default function EventEditor() {
|
|||||||
[event, emitWarning],
|
[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) {
|
if (!event) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -111,9 +120,7 @@ export default function EventEditor() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const delayed = delay !== 0;
|
const delayed = delay !== 0;
|
||||||
const addedTime = delayed
|
const addedTime = delayed ? `${delay >= 0 ? '+' : '-'} ${millisToMinutes(Math.abs(delay))} minutes` : null;
|
||||||
? `${delay >= 0 ? '+' : '-'} ${millisToMinutes(Math.abs(delay))} minutes`
|
|
||||||
: null;
|
|
||||||
const newStart = delayed ? `New start ${stringFromMillis(event.timeStart + delay)}` : null;
|
const newStart = delayed ? `New start ${stringFromMillis(event.timeStart + delay)}` : null;
|
||||||
const newEnd = delayed ? `New end ${stringFromMillis(event.timeEnd + delay)}` : null;
|
const newEnd = delayed ? `New end ${stringFromMillis(event.timeEnd + delay)}` : null;
|
||||||
|
|
||||||
@@ -162,26 +169,32 @@ export default function EventEditor() {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className={style.timeSettings}>
|
<div className={style.timeSettings}>
|
||||||
<label className={style.inputLabel}>Timer type</label>
|
<label className={style.inputLabel}>Timer Behaviour</label>
|
||||||
<Select size='sm' variant='ontime'>
|
<Select
|
||||||
<option value='option1'>Start to end</option>
|
size='sm'
|
||||||
<option value='option2'>Duration</option>
|
name='timerBehaviour'
|
||||||
<option value='option3'>Follow previous</option>
|
value={event.timerBehaviour}
|
||||||
<option value='option3'>Start only</option>
|
onChange={(event) => handleChange('timerBehaviour', event.target.value)}
|
||||||
|
>
|
||||||
|
<option value='start-end'>Start to end</option>
|
||||||
|
<option value='duration'>Duration</option>
|
||||||
|
<option value='follow-previous'>Follow previous</option>
|
||||||
|
<option value='start-only'>Start only</option>
|
||||||
</Select>
|
</Select>
|
||||||
<label className={style.inputLabel}>Countdown style</label>
|
<label className={style.inputLabel}>Timer Type</label>
|
||||||
<Select size='sm' variant='ontime'>
|
<Select
|
||||||
<option value='option1'>Count down</option>
|
size='sm'
|
||||||
<option value='option2'>Count up</option>
|
name='timerType'
|
||||||
<option value='option3'>Clock</option>
|
value={event.timerType}
|
||||||
|
onChange={(event) => handleChange('timerType', event.target.value)}
|
||||||
|
>
|
||||||
|
<option value={TimerType.CountDown}>Count down</option>
|
||||||
|
<option value={TimerType.CountUp}>Count up</option>
|
||||||
|
<option value={TimerType.Clock}>Clock</option>
|
||||||
</Select>
|
</Select>
|
||||||
<span className={style.spacer} />
|
<span className={style.spacer} />
|
||||||
<label className={`${style.inputLabel} ${style.publicToggle}`}>
|
<label className={`${style.inputLabel} ${style.publicToggle}`}>
|
||||||
<Switch
|
<Switch isChecked={event.isPublic} onChange={() => togglePublic(event.isPublic)} variant='ontime' />
|
||||||
isChecked={event.isPublic}
|
|
||||||
onChange={() => togglePublic(event.isPublic)}
|
|
||||||
variant='ontime'
|
|
||||||
/>
|
|
||||||
Event is public
|
Event is public
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
@@ -194,11 +207,7 @@ export default function EventEditor() {
|
|||||||
</div>
|
</div>
|
||||||
<div className={style.column}>
|
<div className={style.column}>
|
||||||
<label className={style.inputLabel}>Presenter</label>
|
<label className={style.inputLabel}>Presenter</label>
|
||||||
<TextInput
|
<TextInput field='presenter' initialText={event.presenter} submitHandler={handleSubmit} />
|
||||||
field='presenter'
|
|
||||||
initialText={event.presenter}
|
|
||||||
submitHandler={handleSubmit}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
<div className={style.column}>
|
<div className={style.column}>
|
||||||
<label className={style.inputLabel}>Subtitle</label>
|
<label className={style.inputLabel}>Subtitle</label>
|
||||||
@@ -209,30 +218,15 @@ export default function EventEditor() {
|
|||||||
<div className={style.column}>
|
<div className={style.column}>
|
||||||
<label className={style.inputLabel}>Colour</label>
|
<label className={style.inputLabel}>Colour</label>
|
||||||
<div className={style.inline}>
|
<div className={style.inline}>
|
||||||
<ColourInput
|
<ColourInput name='colour' value={event?.colour} handleChange={handleSubmit} />
|
||||||
name="colour"
|
<Button leftIcon={<IoBan />} onClick={() => handleSubmit('colour', '')} variant='ontime-subtle' size='sm'>
|
||||||
value={event?.colour}
|
|
||||||
handleChange={handleSubmit}
|
|
||||||
/>
|
|
||||||
<Button
|
|
||||||
leftIcon={<IoBan />}
|
|
||||||
onClick={() => handleSubmit('colour', '')}
|
|
||||||
variant='ontime-subtle'
|
|
||||||
size='sm'
|
|
||||||
>
|
|
||||||
Clear colour
|
Clear colour
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className={`${style.column} ${style.fullHeight}`}>
|
<div className={`${style.column} ${style.fullHeight}`}>
|
||||||
<label className={style.inputLabel}>Note</label>
|
<label className={style.inputLabel}>Note</label>
|
||||||
<TextInput
|
<TextInput field='note' initialText={event.note} submitHandler={handleSubmit} isTextArea isFullHeight />
|
||||||
field='note'
|
|
||||||
initialText={event.note}
|
|
||||||
submitHandler={handleSubmit}
|
|
||||||
isTextArea
|
|
||||||
isFullHeight
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -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<TimeManagerType, 'timerType' | 'current' | 'elapsed' | 'clock'>;
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
import { useEffect } from 'react';
|
import { useEffect } from 'react';
|
||||||
import { useSearchParams } from 'react-router-dom';
|
import { useSearchParams } from 'react-router-dom';
|
||||||
import { useAtom } from 'jotai';
|
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 { overrideStylesURL } from '../../../common/api/apiConstants';
|
||||||
import { mirrorViewersAtom } from '../../../common/atoms/ViewerSettings';
|
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 { useRuntimeStylesheet } from '../../../common/hooks/useRuntimeStylesheet';
|
||||||
import { TimeManagerType } from '../../../common/models/TimeManager.type';
|
import { TimeManagerType } from '../../../common/models/TimeManager.type';
|
||||||
import { OverridableOptions } from '../../../common/models/View.types';
|
import { OverridableOptions } from '../../../common/models/View.types';
|
||||||
import { formatDisplay, millisToSeconds } from '../../../common/utils/dateConfig';
|
import { formatTimerDisplay, getTimerByType } from '../common/viewerUtils';
|
||||||
|
|
||||||
import './MinimalTimer.scss';
|
import './MinimalTimer.scss';
|
||||||
|
|
||||||
@@ -128,23 +128,24 @@ export default function MinimalTimer(props: MinimalTimerProps) {
|
|||||||
|
|
||||||
const showOverlay = pres.text !== '' && pres.visible;
|
const showOverlay = pres.text !== '' && pres.visible;
|
||||||
const isPlaying = time.playback !== 'pause';
|
const isPlaying = time.playback !== 'pause';
|
||||||
const isNegative = (time.current ?? 0) < 0;
|
const isNegative =
|
||||||
const showFinished = isNegative && !userOptions?.hideOvertime;
|
(time.current ?? 0) < 0 && time.timerType !== TimerType.Clock && time.timerType !== TimerType.CountUp;
|
||||||
const showEndMessage = time.current < 0 && general.endMessage && !hideEndMessage;
|
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' : ''}`;
|
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 (
|
return (
|
||||||
<div
|
<div
|
||||||
className={showFinished ? `${baseClasses} minimal-timer--finished` : baseClasses}
|
className={showFinished ? `${baseClasses} minimal-timer--finished` : baseClasses}
|
||||||
@@ -165,17 +166,17 @@ export default function MinimalTimer(props: MinimalTimerProps) {
|
|||||||
<div className='end-message'>{general.endMessage}</div>
|
<div className='end-message'>{general.endMessage}</div>
|
||||||
) : (
|
) : (
|
||||||
<div
|
<div
|
||||||
className={`timer ${!isPlaying ? 'timer--paused' : ''} ${showFinished ? 'timer--finished' : ''}`}
|
className={timerClasseNames}
|
||||||
style={{
|
style={{
|
||||||
color: userOptions.textColour,
|
color: userOptions.textColour,
|
||||||
fontSize: `${(89 / (stageTimerCharacters - 1)) * (userOptions.size || 1)}vw`,
|
fontSize: `${timerFontSize}vw`,
|
||||||
fontFamily: userOptions.font,
|
fontFamily: userOptions.font,
|
||||||
top: userOptions.top,
|
top: userOptions.top,
|
||||||
left: userOptions.left,
|
left: userOptions.left,
|
||||||
backgroundColor: userOptions.textBackground,
|
backgroundColor: userOptions.textBackground,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{stageTimer}
|
{display}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -35,6 +35,7 @@
|
|||||||
grid-area: clock;
|
grid-area: clock;
|
||||||
margin-left: auto;
|
margin-left: auto;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
|
transition: opacity $viewer-transition-time;
|
||||||
|
|
||||||
.label {
|
.label {
|
||||||
font-size: clamp(16px, 1.5vw, 24px);
|
font-size: clamp(16px, 1.5vw, 24px);
|
||||||
@@ -47,6 +48,10 @@
|
|||||||
color: var(--secondary-color-override, $viewer-secondary-color);
|
color: var(--secondary-color-override, $viewer-secondary-color);
|
||||||
letter-spacing: 0.05em;
|
letter-spacing: 0.05em;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&--hidden {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* =================== TITLES ===================*/
|
/* =================== TITLES ===================*/
|
||||||
@@ -71,7 +76,6 @@
|
|||||||
grid-area: timer;
|
grid-area: timer;
|
||||||
justify-self: center;
|
justify-self: center;
|
||||||
align-self: center;
|
align-self: center;
|
||||||
color: var(--timer-color-override, $timer-color);
|
|
||||||
|
|
||||||
.end-message {
|
.end-message {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
@@ -84,6 +88,22 @@
|
|||||||
|
|
||||||
.timer {
|
.timer {
|
||||||
opacity: 1;
|
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;
|
margin: 0 auto;
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
transition: $viewer-transition-time;
|
transition: $viewer-transition-time;
|
||||||
}
|
|
||||||
|
|
||||||
.timer--paused,
|
&--paused {
|
||||||
.progress-container--paused {
|
|
||||||
opacity: $viewer-opacity-disabled;
|
opacity: $viewer-opacity-disabled;
|
||||||
transition: $viewer-transition-time;
|
transition: $viewer-transition-time;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
/* =================== OVERLAY ===================*/
|
/* =================== OVERLAY ===================*/
|
||||||
|
|||||||
@@ -1,16 +1,17 @@
|
|||||||
import { useEffect } from 'react';
|
import { useEffect } from 'react';
|
||||||
import { AnimatePresence, motion } from 'framer-motion';
|
import { AnimatePresence, motion } from 'framer-motion';
|
||||||
import { useAtom } from 'jotai';
|
import { useAtom } from 'jotai';
|
||||||
|
import { TimerType } from 'ontime-types';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
|
|
||||||
import { overrideStylesURL } from '../../../common/api/apiConstants';
|
import { overrideStylesURL } from '../../../common/api/apiConstants';
|
||||||
import { mirrorViewersAtom } from '../../../common/atoms/ViewerSettings';
|
import { mirrorViewersAtom } from '../../../common/atoms/ViewerSettings';
|
||||||
import NavigationMenu from '../../../common/components/navigation-menu/NavigationMenu';
|
import NavigationMenu from '../../../common/components/navigation-menu/NavigationMenu';
|
||||||
import ProgressBar from '../../../common/components/progress-bar/ProgressBar';
|
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 TitleCard from '../../../common/components/title-card/TitleCard';
|
||||||
import { useRuntimeStylesheet } from '../../../common/hooks/useRuntimeStylesheet';
|
import { useRuntimeStylesheet } from '../../../common/hooks/useRuntimeStylesheet';
|
||||||
import { formatTime } from '../../../common/utils/time';
|
import { formatTime } from '../../../common/utils/time';
|
||||||
|
import { formatTimerDisplay, getTimerByType } from '../common/viewerUtils';
|
||||||
|
|
||||||
import './Timer.scss';
|
import './Timer.scss';
|
||||||
|
|
||||||
@@ -61,29 +62,49 @@ export default function Timer(props) {
|
|||||||
const clock = formatTime(time.clock, formatOptions);
|
const clock = formatTime(time.clock, formatOptions);
|
||||||
const showOverlay = pres.text !== '' && pres.visible;
|
const showOverlay = pres.text !== '' && pres.visible;
|
||||||
const isPlaying = time.playback !== 'pause';
|
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 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 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 (
|
return (
|
||||||
<div className={time.finished ? `${baseClasses} stage-timer--finished` : baseClasses} data-testid='timer-view'>
|
<div className={showFinished ? `${baseClasses} stage-timer--finished` : baseClasses} data-testid='timer-view'>
|
||||||
<NavigationMenu />
|
<NavigationMenu />
|
||||||
<div className={showOverlay ? 'message-overlay message-overlay--active' : 'message-overlay'}>
|
<div className={showOverlay ? 'message-overlay message-overlay--active' : 'message-overlay'}>
|
||||||
<div className='message'>{pres.text}</div>
|
<div className='message'>{pres.text}</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className='clock-container'>
|
<div className={`clock-container ${showClock ? '' : 'clock-container--hidden'}`}>
|
||||||
<div className='label'>Time Now</div>
|
<div className='label'>Time Now</div>
|
||||||
<div className='clock'>{clock}</div>
|
<div className='clock'>{clock}</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className='timer-container'>
|
<div className='timer-container'>
|
||||||
{time.finished ? (
|
{showEndMessage ? (
|
||||||
<div className='end-message'>
|
<div className='end-message'>{general.endMessage}</div>
|
||||||
{!general.endMessage ? <TimerDisplay time={time.current} hideZeroHours /> : general.endMessage}
|
|
||||||
</div>
|
|
||||||
) : (
|
) : (
|
||||||
<TimerDisplay time={normalisedTime} hideZeroHours className={isPlaying ? 'timer' : 'timer--paused'} />
|
<div
|
||||||
|
className={timerClasseNames}
|
||||||
|
style={{
|
||||||
|
fontSize: `${timerFontSize}vw`,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{display}
|
||||||
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -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',
|
|
||||||
};
|
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
import { OntimeBlock, OntimeDelay, OntimeEvent, SupportedEvent, TimerType } from 'ontime-types';
|
||||||
|
|
||||||
|
export const event: Omit<OntimeEvent, 'id'> = {
|
||||||
|
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<OntimeDelay, 'id'> = {
|
||||||
|
duration: 0,
|
||||||
|
type: SupportedEvent.Delay,
|
||||||
|
revision: 0,
|
||||||
|
};
|
||||||
|
|
||||||
|
export const block: Omit<OntimeBlock, 'id'> = {
|
||||||
|
type: SupportedEvent.Block,
|
||||||
|
};
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
import { TimerLifeCycle } from 'ontime-types';
|
import { TimerLifeCycle, TimerType } from 'ontime-types';
|
||||||
|
|
||||||
import { eventStore } from '../stores/EventStore.js';
|
import { eventStore } from '../stores/EventStore.js';
|
||||||
import { PlaybackService } from './PlaybackService.js';
|
import { PlaybackService } from './PlaybackService.js';
|
||||||
@@ -27,6 +27,7 @@ export class TimerService {
|
|||||||
secondaryTimer: number | null;
|
secondaryTimer: number | null;
|
||||||
selectedEventId: string | null;
|
selectedEventId: string | null;
|
||||||
duration: number | null;
|
duration: number | null;
|
||||||
|
timerType: TimerType | null;
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -91,6 +92,7 @@ export class TimerService {
|
|||||||
secondaryTimer: null,
|
secondaryTimer: null,
|
||||||
selectedEventId: null,
|
selectedEventId: null,
|
||||||
duration: null,
|
duration: null,
|
||||||
|
timerType: null,
|
||||||
};
|
};
|
||||||
this.loadedTimerId = null;
|
this.loadedTimerId = null;
|
||||||
this._pausedInterval = 0;
|
this._pausedInterval = 0;
|
||||||
@@ -121,6 +123,7 @@ export class TimerService {
|
|||||||
|
|
||||||
// update relevant information and force update
|
// update relevant information and force update
|
||||||
this.timer.duration = timer.duration;
|
this.timer.duration = timer.duration;
|
||||||
|
this.timer.timerType = timer.timerType;
|
||||||
|
|
||||||
// this might not be ideal
|
// this might not be ideal
|
||||||
this.timer.finishedAt = null;
|
this.timer.finishedAt = null;
|
||||||
@@ -138,7 +141,8 @@ export class TimerService {
|
|||||||
* @param {number} timer.timeStart
|
* @param {number} timer.timeStart
|
||||||
* @param {number} timer.timeEnd
|
* @param {number} timer.timeEnd
|
||||||
* @param {number} timer.duration
|
* @param {number} timer.duration
|
||||||
* @param {string} timer.timeType
|
* @param {string} timer.timerBehaviour
|
||||||
|
* @param {string} timer.timerType
|
||||||
* @param {boolean} timer.skip
|
* @param {boolean} timer.skip
|
||||||
*/
|
*/
|
||||||
load(timer) {
|
load(timer) {
|
||||||
@@ -152,6 +156,7 @@ export class TimerService {
|
|||||||
this.timer.duration = timer.duration;
|
this.timer.duration = timer.duration;
|
||||||
this.timer.current = timer.duration;
|
this.timer.current = timer.duration;
|
||||||
this.playback = 'armed';
|
this.playback = 'armed';
|
||||||
|
this.timer.timerType = timer.timerType;
|
||||||
this._pausedInterval = 0;
|
this._pausedInterval = 0;
|
||||||
this._pausedAt = 0;
|
this._pausedAt = 0;
|
||||||
|
|
||||||
|
|||||||
@@ -310,7 +310,8 @@ export const validateEvent = (eventArgs) => {
|
|||||||
presenter: makeString(e.presenter, d.presenter),
|
presenter: makeString(e.presenter, d.presenter),
|
||||||
timeStart: start,
|
timeStart: start,
|
||||||
timeEnd: end,
|
timeEnd: end,
|
||||||
timeType: 'start-end',
|
timerBehaviour: makeString(e.timerBehaviour, d.timerBehaviour),
|
||||||
|
timerType: makeString(e.timerType, d.timerType),
|
||||||
duration: validateDuration(start, end),
|
duration: validateDuration(start, end),
|
||||||
isPublic: typeof e.isPublic === 'boolean' ? e.isPublic : d.isPublic,
|
isPublic: typeof e.isPublic === 'boolean' ? e.isPublic : d.isPublic,
|
||||||
skip: typeof e.skip === 'boolean' ? e.skip : d.skip,
|
skip: typeof e.skip === 'boolean' ? e.skip : d.skip,
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
export enum TimerTypeType {
|
export enum TimerType {
|
||||||
CountDown = 'count-down',
|
CountDown = 'count-down',
|
||||||
CountUp = 'count-up',
|
CountUp = 'count-up',
|
||||||
Clock = 'clock'
|
Clock = 'clock'
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
import { TimerType } from '../TimerType.type.js';
|
||||||
|
|
||||||
export enum SupportedEvent {
|
export enum SupportedEvent {
|
||||||
Event = 'event',
|
Event = 'event',
|
||||||
Delay = 'delay',
|
Delay = 'delay',
|
||||||
@@ -26,7 +28,8 @@ export type OntimeEvent = OntimeBaseEvent & {
|
|||||||
subtitle: string;
|
subtitle: string;
|
||||||
presenter: string;
|
presenter: string;
|
||||||
note: string;
|
note: string;
|
||||||
timeType?: string;
|
timerBehaviour: 'start-end',
|
||||||
|
timerType: TimerType,
|
||||||
timeStart: number;
|
timeStart: number;
|
||||||
timeEnd: number;
|
timeEnd: number;
|
||||||
duration: number;
|
duration: number;
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ import { OSCSettings, OscSubscription } from './definitions/core/OscSettings.typ
|
|||||||
import { Playback } from './definitions/runtime/Playback.type.js';
|
import { Playback } from './definitions/runtime/Playback.type.js';
|
||||||
import { Settings } from './definitions/core/Settings.type.js';
|
import { Settings } from './definitions/core/Settings.type.js';
|
||||||
import { TimerLifeCycle } from './definitions/core/TimerLifecycle.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 { UserFields } from './definitions/core/UserFields.type.js';
|
||||||
import { ViewSettings } from './definitions/core/Views.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 };
|
export type { DatabaseModel };
|
||||||
|
|
||||||
// ---> Rundown
|
// ---> Rundown
|
||||||
export type { TimerTypeType };
|
export { TimerType };
|
||||||
|
export { SupportedEvent };
|
||||||
export type { OntimeBaseEvent, OntimeBlock, OntimeDelay, OntimeEvent };
|
export type { OntimeBaseEvent, OntimeBlock, OntimeDelay, OntimeEvent };
|
||||||
export type { OntimeRundown, OntimeRundownEntry };
|
export type { OntimeRundown, OntimeRundownEntry };
|
||||||
export { SupportedEvent };
|
|
||||||
|
|
||||||
// ---> Event
|
// ---> Event
|
||||||
export type { EventData };
|
export type { EventData };
|
||||||
|
|||||||
Reference in New Issue
Block a user