mirror of
https://github.com/cpvalente/ontime.git
synced 2026-08-10 09:53:48 +00:00
Feat/timer/stylechange (#341)
* feat: multi color progress bar * add color support to minimal timer Co-Authored-By: Fabian Posenau <19673098+kellhogs@users.noreply.github.com> --------- Co-authored-by: Fabian Posenau <fabian@fphome.de> Co-authored-by: Carlos Valente <34649812+cpvalente@users.noreply.github.com> Co-authored-by: Fabian Posenau <19673098+kellhogs@users.noreply.github.com>
This commit is contained in:
@@ -21,6 +21,7 @@
|
||||
"deepmerge": "^4.3.0",
|
||||
"framer-motion": "^10.10.0",
|
||||
"react": "^18.2.0",
|
||||
"react-colorful": "^5.6.1",
|
||||
"react-dom": "^18.2.0",
|
||||
"react-fast-compare": "^3.2.0",
|
||||
"react-hook-form": "^7.43.5",
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
@use "../../../../theme/_ontimeColours" as *;
|
||||
@use "../../../../theme/_v2Styles" as *;
|
||||
|
||||
.swatch {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
border-radius: 16px;
|
||||
border: 1px solid $gray-300;
|
||||
cursor: pointer;
|
||||
|
||||
transition-property: box-shadow;
|
||||
transition-duration: $transition-time-action;
|
||||
}
|
||||
|
||||
.swatch:hover {
|
||||
box-shadow: 0 0 0 1px $blue-300;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
import { HexAlphaColorPicker } from 'react-colorful';
|
||||
import { Popover, PopoverContent, PopoverTrigger } from '@chakra-ui/react';
|
||||
|
||||
import style from './PopoverPicker.module.scss';
|
||||
|
||||
interface PopoverPickerProps {
|
||||
color: string;
|
||||
onChange: (color: string) => void;
|
||||
}
|
||||
|
||||
export default function PopoverPicker(props: PopoverPickerProps) {
|
||||
const { color, onChange } = props;
|
||||
|
||||
return (
|
||||
<Popover>
|
||||
<PopoverTrigger>
|
||||
<div className={style.swatch} style={{ backgroundColor: color }} />
|
||||
</PopoverTrigger>
|
||||
<PopoverContent style={{ width: 'auto' }}>
|
||||
<HexAlphaColorPicker color={color} onChange={onChange} />
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
@use '../../../theme/viewerDefs' as *;
|
||||
|
||||
$progress-bar-size: 12px;
|
||||
$progress-bar-br: 3px;
|
||||
|
||||
.progress-bar {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: $progress-bar-size;
|
||||
border-radius: $progress-bar-br;
|
||||
background-color: var(--timer-progress-bg-override, $viewer-card-bg-color);
|
||||
display: flex;
|
||||
|
||||
&--hidden {
|
||||
display: none;
|
||||
transition: display 0.5s;
|
||||
}
|
||||
}
|
||||
|
||||
.progress-bar__indicator {
|
||||
position: absolute;
|
||||
height: inherit;
|
||||
background-color: black;
|
||||
opacity: 0.8;
|
||||
transition: 1s linear;
|
||||
transition-property: width;
|
||||
right: 0;
|
||||
}
|
||||
|
||||
.progress-bar__bg-normal {
|
||||
position: absolute;
|
||||
height: inherit;
|
||||
right: 0;
|
||||
border-radius: $progress-bar-br;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.progress-bar__bg-warning {
|
||||
position: absolute;
|
||||
height: inherit;
|
||||
right: 0;
|
||||
border-radius: 0 $progress-bar-br $progress-bar-br 0;
|
||||
}
|
||||
|
||||
.progress-bar__bg-danger {
|
||||
position: absolute;
|
||||
height: inherit;
|
||||
right: 0;
|
||||
border-radius: 0 $progress-bar-br $progress-bar-br 0;
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
import { clamp } from '../../utils/math';
|
||||
|
||||
import './MultiPartProgressBar.scss';
|
||||
|
||||
interface MultiPartProgressBar {
|
||||
now: number;
|
||||
complete: number;
|
||||
normalColor: string;
|
||||
warning: number;
|
||||
warningColor: string;
|
||||
danger: number;
|
||||
dangerColor: string;
|
||||
hidden?: boolean;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export default function MultiPartProgressBar(props: MultiPartProgressBar) {
|
||||
const { now, complete, normalColor, warning, warningColor, danger, dangerColor, hidden, className = '' } = props;
|
||||
|
||||
const percentComplete = 100 - clamp(100 - (Math.max(now, 0) * 100) / complete, 0, 100);
|
||||
|
||||
const dangerWidth = clamp((danger / complete) * 100, 0, 100);
|
||||
const warningWidth = clamp((warning / complete) * 100, 0, 100);
|
||||
|
||||
return (
|
||||
<div className={`progress-bar ${hidden ? 'progress-bar--hidden' : ''} ${className}`}>
|
||||
<div className='progress-bar__bg-normal' style={{ backgroundColor: normalColor }} />
|
||||
<div className='progress-bar__bg-warning' style={{ width: `${warningWidth}%`, backgroundColor: warningColor }} />
|
||||
<div className='progress-bar__bg-danger' style={{ width: `${dangerWidth}%`, backgroundColor: dangerColor }} />
|
||||
<div className='progress-bar__indicator' style={{ width: `${percentComplete}%` }} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -4,9 +4,9 @@ import './TitleCard.scss';
|
||||
|
||||
interface TitleCardProps {
|
||||
label: 'now' | 'next';
|
||||
title: string;
|
||||
subtitle: string;
|
||||
presenter: string;
|
||||
title: string | null;
|
||||
subtitle: string | null;
|
||||
presenter: string | null;
|
||||
}
|
||||
|
||||
export default function TitleCard(props: TitleCardProps) {
|
||||
|
||||
@@ -13,4 +13,5 @@ export type OverridableOptions = {
|
||||
hideMessagesOverlay?: boolean;
|
||||
hideEndMessage?: boolean;
|
||||
language?: string;
|
||||
showProgressBar?: boolean;
|
||||
};
|
||||
|
||||
@@ -2,5 +2,10 @@ import { ViewSettings } from 'ontime-types';
|
||||
|
||||
export const viewsSettingsPlaceholder: ViewSettings = {
|
||||
overrideStyles: false,
|
||||
normalColor: '#ffffffcc',
|
||||
warningColor: '#FFAB33',
|
||||
warningThreshold: 120000,
|
||||
dangerColor: '#ED3333',
|
||||
dangerThreshold: 60000,
|
||||
endMessage: '',
|
||||
};
|
||||
|
||||
@@ -52,6 +52,10 @@
|
||||
}
|
||||
}
|
||||
|
||||
.overflow {
|
||||
overflow-y: visible !important;
|
||||
}
|
||||
|
||||
/* Track */
|
||||
::-webkit-scrollbar-track {
|
||||
background: rgba($gray-50, 0.15);
|
||||
|
||||
@@ -1,140 +0,0 @@
|
||||
import { useCallback, useEffect, useState } from 'react';
|
||||
import { FormControl, FormLabel, Input, ModalBody } from '@chakra-ui/react';
|
||||
import { IoInformationCircleOutline } from '@react-icons/all-files/io5/IoInformationCircleOutline';
|
||||
|
||||
import { postView } from '../../common/api/ontimeApi';
|
||||
import EnableBtn from '../../common/components/buttons/EnableBtn';
|
||||
import useViewSettings from '../../common/hooks-query/useViewSettings';
|
||||
import { viewsSettingsPlaceholder } from '../../common/models/ViewSettings.type';
|
||||
import { useEmitLog } from '../../common/stores/logger';
|
||||
import { openLink } from '../../common/utils/linkUtils';
|
||||
import { inputProps } from '../../features/modals/modalHelper';
|
||||
|
||||
import SubmitContainer from './SubmitContainer';
|
||||
|
||||
import style from './Modals.module.scss';
|
||||
|
||||
export default function ViewsSettingsModal() {
|
||||
const { data, status, refetch } = useViewSettings();
|
||||
|
||||
const { emitError } = useEmitLog();
|
||||
const [formData, setFormData] = useState(viewsSettingsPlaceholder);
|
||||
const [changed, setChanged] = useState(false);
|
||||
const [submitting, setSubmitting] = useState(false);
|
||||
|
||||
/**
|
||||
* Set formdata from server state
|
||||
*/
|
||||
useEffect(() => {
|
||||
if (data == null) return;
|
||||
if (changed) return;
|
||||
setFormData({ ...data });
|
||||
}, [changed, data]);
|
||||
|
||||
/**
|
||||
* Validate and submit data
|
||||
*/
|
||||
const submitHandler = useCallback(
|
||||
async (event) => {
|
||||
event.preventDefault();
|
||||
setSubmitting(true);
|
||||
try {
|
||||
await postView(formData);
|
||||
} catch (error) {
|
||||
emitError(`Error view settings: ${error}`);
|
||||
} finally {
|
||||
await refetch();
|
||||
setChanged(false);
|
||||
}
|
||||
setSubmitting(false);
|
||||
},
|
||||
[emitError, formData, refetch],
|
||||
);
|
||||
|
||||
/**
|
||||
* Reverts local state equals to server state
|
||||
*/
|
||||
const revert = useCallback(async () => {
|
||||
setChanged(false);
|
||||
await refetch();
|
||||
}, [refetch]);
|
||||
|
||||
/**
|
||||
* Handles change of input field in local state
|
||||
* @param {string} field - object parameter to update
|
||||
* @param {(string | number | boolean)} value - new object parameter value
|
||||
*/
|
||||
const handleChange = useCallback(
|
||||
(field, value) => {
|
||||
const temp = { ...formData };
|
||||
temp[field] = value;
|
||||
setFormData(temp);
|
||||
setChanged(true);
|
||||
},
|
||||
[formData],
|
||||
);
|
||||
|
||||
return (
|
||||
<ModalBody className={style.modalBody}>
|
||||
<p className={style.notes}>
|
||||
Options related to the viewers
|
||||
<br />
|
||||
🔥 Changes take effect immediately 🔥
|
||||
</p>
|
||||
<form onSubmit={submitHandler}>
|
||||
<div className={style.hSeparator}>Timer end message</div>
|
||||
<div className={style.spacedEntry}>
|
||||
<FormLabel htmlFor='endMessage'>
|
||||
End Message
|
||||
<span className={style.labelNote}>
|
||||
<br />
|
||||
Shown on presenter view when time is finished
|
||||
</span>
|
||||
</FormLabel>
|
||||
<Input
|
||||
{...inputProps}
|
||||
maxLength={50}
|
||||
name='endMessage'
|
||||
placeholder='Empty message shows elapsed time'
|
||||
value={formData.endMessage}
|
||||
onChange={(event) => handleChange('endMessage', event.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<div className={style.hSeparator}>Style Options</div>
|
||||
<div className={style.blockNotes}>
|
||||
<span className={style.inlineFlex}>
|
||||
<IoInformationCircleOutline color='#2b6cb0' fontSize='2em' />
|
||||
CSS Style Overrides
|
||||
</span>
|
||||
This feature allows user defined CSS to customise viewers appearance. <br />
|
||||
<a
|
||||
href='#!'
|
||||
onClick={() => openLink('https://cpvalente.gitbook.io/ontime/features/custom-styling')}
|
||||
className={style.if}
|
||||
>
|
||||
For details on the styling and file location please refer to documentation
|
||||
</a>
|
||||
</div>
|
||||
<div className={style.modalFields}>
|
||||
<div className={style.modalInline}>
|
||||
<FormControl>
|
||||
<FormLabel htmlFor='overrideStyles'>
|
||||
Override CSS Styles
|
||||
<span className={style.labelNote}>
|
||||
<br />
|
||||
Enable / Disable override
|
||||
</span>
|
||||
</FormLabel>
|
||||
<EnableBtn
|
||||
active={formData.overrideStyles}
|
||||
text={formData.overrideStyles ? 'Style Override Enabled' : 'Style Override Disabled'}
|
||||
actionHandler={() => handleChange('overrideStyles', !formData.overrideStyles)}
|
||||
/>
|
||||
</FormControl>
|
||||
</div>
|
||||
<SubmitContainer revert={revert} submitting={submitting} changed={changed} status={status} />
|
||||
</div>
|
||||
</form>
|
||||
</ModalBody>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,197 @@
|
||||
import { ChangeEvent, FormEvent, useCallback, useEffect, useState } from 'react';
|
||||
import { FormControl, FormLabel, Input, ModalBody, Switch } from '@chakra-ui/react';
|
||||
import { ViewSettings } from 'ontime-types';
|
||||
|
||||
import { postView } from '../../common/api/ontimeApi';
|
||||
import PopoverPicker from '../../common/components/input/popover-picker/PopoverPicker';
|
||||
import useViewSettings from '../../common/hooks-query/useViewSettings';
|
||||
import { viewsSettingsPlaceholder } from '../../common/models/ViewSettings.type';
|
||||
import { useEmitLog } from '../../common/stores/logger';
|
||||
import { forgivingStringToMillis, millisToMinutes } from '../../common/utils/dateConfig';
|
||||
|
||||
import { inputProps } from './modalHelper';
|
||||
import SubmitContainer from './SubmitContainer';
|
||||
|
||||
import styles from './Modal.module.scss';
|
||||
import style from './Modals.module.scss';
|
||||
|
||||
const stylingDocsUrl = 'https://cpvalente.gitbook.io/ontime/features/custom-styling';
|
||||
|
||||
export default function ViewsSettingsModal() {
|
||||
const { data, status, refetch } = useViewSettings();
|
||||
|
||||
const { emitError } = useEmitLog();
|
||||
const [formData, setFormData] = useState(viewsSettingsPlaceholder);
|
||||
const [changed, setChanged] = useState(false);
|
||||
const [submitting, setSubmitting] = useState(false);
|
||||
|
||||
/**
|
||||
* Set formdata from server state
|
||||
*/
|
||||
useEffect(() => {
|
||||
if (data == null) return;
|
||||
if (changed) return;
|
||||
setFormData({ ...data });
|
||||
}, [changed, data]);
|
||||
|
||||
/**
|
||||
* Validate and submit data
|
||||
*/
|
||||
const submitHandler = useCallback(
|
||||
async (event: FormEvent) => {
|
||||
event.preventDefault();
|
||||
setSubmitting(true);
|
||||
try {
|
||||
await postView(formData);
|
||||
} catch (error) {
|
||||
emitError(`Error view settings: ${error}`);
|
||||
} finally {
|
||||
await refetch();
|
||||
setChanged(false);
|
||||
}
|
||||
setSubmitting(false);
|
||||
},
|
||||
[emitError, formData, refetch],
|
||||
);
|
||||
|
||||
/**
|
||||
* Reverts local state equals to server state
|
||||
*/
|
||||
const revert = useCallback(async () => {
|
||||
setChanged(false);
|
||||
await refetch();
|
||||
}, [refetch]);
|
||||
|
||||
/**
|
||||
* Handles change of input field in local state
|
||||
* @param {string} field - object parameter to update
|
||||
* @param {(string | number | boolean)} value - new object parameter value
|
||||
*/
|
||||
const handleChange = useCallback(
|
||||
(field: string, value: string | number | boolean) => {
|
||||
const temp = { ...formData };
|
||||
//@ts-expect-error - TODO: dont know what to type here
|
||||
temp[field] = value;
|
||||
setFormData(temp);
|
||||
setChanged(true);
|
||||
},
|
||||
[formData],
|
||||
);
|
||||
|
||||
/**
|
||||
* Handles change of number input field in local state
|
||||
* @param {string} field - object parameter to update
|
||||
* @param {(ChangeEvent<HTMLInputElement>)} value - new object parameter value
|
||||
*/
|
||||
function handleThresholdChange(field: string, event: ChangeEvent<HTMLInputElement>) {
|
||||
const temp: ViewSettings = { ...formData };
|
||||
const converted: number = forgivingStringToMillis(event.target.value);
|
||||
//@ts-expect-error - TODO: dont know what to type here
|
||||
temp[field] = converted;
|
||||
setFormData(temp);
|
||||
setChanged(true);
|
||||
}
|
||||
|
||||
const dangerThresholdValue = millisToMinutes(formData.dangerThreshold);
|
||||
const warningThresholdValue = millisToMinutes(formData.warningThreshold);
|
||||
|
||||
return (
|
||||
<form onSubmit={submitHandler} className={styles.sectionContainer} id='viewSettings'>
|
||||
<ModalBody className={style.modalBody}>
|
||||
<p className={style.notes}>
|
||||
Options related to the viewers
|
||||
<br />
|
||||
<a href={stylingDocsUrl} target='_blank' rel='noreferrer'>
|
||||
Read the docs
|
||||
</a>
|
||||
</p>
|
||||
<div className={style.hSeparator}>Timer end message</div>
|
||||
<div className={style.spacedEntry}>
|
||||
<FormLabel htmlFor='endMessage'>
|
||||
End Message
|
||||
<span className={style.labelNote}>
|
||||
<br />
|
||||
Shown on presenter view when time is finished
|
||||
</span>
|
||||
</FormLabel>
|
||||
<Input
|
||||
{...inputProps}
|
||||
maxLength={50}
|
||||
name='endMessage'
|
||||
placeholder='Empty message shows elapsed time'
|
||||
value={formData.endMessage}
|
||||
onChange={(event) => handleChange('endMessage', event.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<div className={style.hSeparator}>Style Options</div>
|
||||
<div className={styles.splitSection}>
|
||||
<div>
|
||||
<span className={`${styles.sectionTitle} ${styles.main}`}>Override CSS Styles</span>
|
||||
<span className={styles.sectionSubtitle}>Enable / Disable override</span>
|
||||
</div>
|
||||
<Switch onChange={() => handleChange('overrideStyles', !formData.overrideStyles)} variant='ontime-on-light' />
|
||||
</div>
|
||||
<hr className={styles.divider} />
|
||||
<div className={style.hSeparator}>Timer Options</div>
|
||||
<div className={styles.splitSection}>
|
||||
<div>
|
||||
<span className={`${styles.sectionTitle} ${styles.main}`}>Normal Color</span>
|
||||
<span className={styles.sectionSubtitle}>Change timer Normal Color</span>
|
||||
</div>
|
||||
<PopoverPicker color={formData.normalColor} onChange={(event) => handleChange('normalColor', event)} />
|
||||
</div>
|
||||
<div className={styles.splitSection}>
|
||||
<div>
|
||||
<span className={`${styles.sectionTitle} ${styles.main}`}>Warning Color</span>
|
||||
<span className={styles.sectionSubtitle}>Change timer Warning Color</span>
|
||||
</div>
|
||||
<PopoverPicker color={formData.warningColor} onChange={(event) => handleChange('warningColor', event)} />
|
||||
</div>
|
||||
<FormControl className={styles.splitSection}>
|
||||
<label htmlFor='warningThreshold'>
|
||||
<span className={styles.sectionTitle}>Warning Time</span>
|
||||
<span className={styles.sectionSubtitle}>The time (in minutes) when the color changes</span>
|
||||
</label>
|
||||
<Input
|
||||
{...inputProps}
|
||||
id='warningThreshold'
|
||||
variant='ontime-filled-on-light'
|
||||
value={warningThresholdValue}
|
||||
onChange={(event) => handleThresholdChange('warningThreshold', event)}
|
||||
width='75px'
|
||||
size='sm'
|
||||
textAlign='right'
|
||||
maxLength={3}
|
||||
/>
|
||||
</FormControl>
|
||||
<div className={styles.splitSection}>
|
||||
<div>
|
||||
<span className={`${styles.sectionTitle} ${styles.main}`}>Danger Color</span>
|
||||
<span className={styles.sectionSubtitle}>Change timer Danger Color</span>
|
||||
</div>
|
||||
<PopoverPicker color={formData.dangerColor} onChange={(event) => handleChange('dangerColor', event)} />
|
||||
</div>
|
||||
<FormControl className={styles.splitSection}>
|
||||
<label htmlFor='dangerThreshold'>
|
||||
<span className={styles.sectionTitle}>Danger Time</span>
|
||||
<span className={styles.sectionSubtitle}>The time (in minutes) when the color changes</span>
|
||||
</label>
|
||||
<Input
|
||||
{...inputProps}
|
||||
id='dangerThreshold'
|
||||
variant='ontime-filled-on-light'
|
||||
value={dangerThresholdValue}
|
||||
onChange={(event) => handleThresholdChange('dangerThreshold', event)}
|
||||
width='75px'
|
||||
size='sm'
|
||||
textAlign='right'
|
||||
maxLength={3}
|
||||
/>
|
||||
</FormControl>
|
||||
<div className={style.modalFields}>
|
||||
<SubmitContainer revert={revert} submitting={submitting} changed={changed} status={status} />
|
||||
</div>
|
||||
</ModalBody>
|
||||
</form>
|
||||
);
|
||||
}
|
||||
@@ -133,6 +133,17 @@ export default function MinimalTimer(props: MinimalTimerProps) {
|
||||
const showFinished =
|
||||
time.finished && !userOptions?.hideOvertime && (time.timerType !== TimerType.Clock || showEndMessage);
|
||||
|
||||
const showProgress = time.playback !== Playback.Stop;
|
||||
const showWarning = (time.current ?? 1) < viewSettings.warningThreshold;
|
||||
const showDanger = (time.current ?? 1) < viewSettings.dangerThreshold;
|
||||
const timerColor = userOptions.textColour
|
||||
? userOptions.textColour
|
||||
: showProgress && showDanger
|
||||
? viewSettings.dangerColor
|
||||
: showProgress && showWarning
|
||||
? viewSettings.warningColor
|
||||
: viewSettings.normalColor;
|
||||
|
||||
const stageTimer = getTimerByType(time);
|
||||
let display = formatTimerDisplay(stageTimer);
|
||||
if (isNegative) {
|
||||
@@ -167,7 +178,7 @@ export default function MinimalTimer(props: MinimalTimerProps) {
|
||||
<div
|
||||
className={timerClasses}
|
||||
style={{
|
||||
color: userOptions.textColour,
|
||||
color: timerColor,
|
||||
fontSize: `${timerFontSize}vw`,
|
||||
fontFamily: userOptions.font,
|
||||
top: userOptions.top,
|
||||
|
||||
@@ -3,8 +3,8 @@ import { AnimatePresence, motion } from 'framer-motion';
|
||||
import { EventData, Message, Playback, TimerType, ViewSettings } from 'ontime-types';
|
||||
|
||||
import { overrideStylesURL } from '../../../common/api/apiConstants';
|
||||
import MultiPartProgressBar from '../../../common/components/multi-part-progress-bar/MultiPartProgressBar';
|
||||
import NavigationMenu from '../../../common/components/navigation-menu/NavigationMenu';
|
||||
import ProgressBar from '../../../common/components/progress-bar/ProgressBar';
|
||||
import TitleCard from '../../../common/components/title-card/TitleCard';
|
||||
import { useRuntimeStylesheet } from '../../../common/hooks/useRuntimeStylesheet';
|
||||
import { TimeManagerType } from '../../../common/models/TimeManager.type';
|
||||
@@ -68,6 +68,14 @@ export default function Timer(props: TimerProps) {
|
||||
const showEndMessage = (time.current ?? 1) < 0 && viewSettings.endMessage;
|
||||
const showProgress = time.playback !== Playback.Stop;
|
||||
const showFinished = time.finished && (time.timerType !== TimerType.Clock || showEndMessage);
|
||||
const showWarning = (time.current ?? 1) < viewSettings.warningThreshold;
|
||||
const showDanger = (time.current ?? 1) < viewSettings.dangerThreshold;
|
||||
const timerColor =
|
||||
showProgress && showDanger
|
||||
? viewSettings.dangerColor
|
||||
: showProgress && showWarning
|
||||
? viewSettings.warningColor
|
||||
: viewSettings.normalColor;
|
||||
const showClock = time.timerType !== TimerType.Clock;
|
||||
const baseClasses = `stage-timer ${isMirrored ? 'mirror' : ''}`;
|
||||
|
||||
@@ -101,6 +109,7 @@ export default function Timer(props: TimerProps) {
|
||||
className={timerClasses}
|
||||
style={{
|
||||
fontSize: `${timerFontSize}vw`,
|
||||
color: timerColor,
|
||||
}}
|
||||
>
|
||||
{display}
|
||||
@@ -108,10 +117,15 @@ export default function Timer(props: TimerProps) {
|
||||
)}
|
||||
</div>
|
||||
|
||||
<ProgressBar
|
||||
<MultiPartProgressBar
|
||||
className={isPlaying ? 'progress-container' : 'progress-container progress-container--paused'}
|
||||
now={time.current || 0}
|
||||
complete={time.duration || 0}
|
||||
normalColor={viewSettings.normalColor}
|
||||
warning={viewSettings.warningThreshold}
|
||||
warningColor={viewSettings.warningColor}
|
||||
danger={viewSettings.dangerThreshold}
|
||||
dangerColor={viewSettings.dangerColor}
|
||||
hidden={!showProgress}
|
||||
/>
|
||||
|
||||
|
||||
@@ -246,6 +246,11 @@ export const postViewSettings = async (req, res) => {
|
||||
const newData = {
|
||||
overrideStyles: req.body.overrideStyles,
|
||||
endMessage: req.body?.endMessage || '',
|
||||
normalColor: req.body.normalColor,
|
||||
warningColor: req.body.warningColor,
|
||||
warningThreshold: req.body.warningThreshold,
|
||||
dangerColor: req.body.dangerColor,
|
||||
dangerThreshold: req.body.dangerThreshold,
|
||||
};
|
||||
await DataProvider.setViewSettings(newData);
|
||||
res.status(200).send(newData);
|
||||
|
||||
@@ -19,6 +19,11 @@ export const dbModel: DatabaseModel = {
|
||||
},
|
||||
viewSettings: {
|
||||
overrideStyles: false,
|
||||
normalColor: '#ffffffcc',
|
||||
warningColor: '#FFAB33',
|
||||
warningThreshold: 120000,
|
||||
dangerColor: '#ED3333',
|
||||
dangerThreshold: 60000,
|
||||
endMessage: '',
|
||||
},
|
||||
aliases: [],
|
||||
|
||||
@@ -144,6 +144,11 @@ export const parseViewSettings = (data, enforce): ViewSettings => {
|
||||
|
||||
const viewSettings = {
|
||||
overrideStyles: v.overrideStyles ?? dbModel.viewSettings.overrideStyles,
|
||||
normalColor: v.normalColor ?? dbModel.viewSettings.normalColor,
|
||||
warningColor: v.warningColor ?? dbModel.viewSettings.warningColor,
|
||||
warningThreshold: v.warningThreshold ?? dbModel.viewSettings.warningThreshold,
|
||||
dangerColor: v.dangerColor ?? dbModel.viewSettings.dangerColor,
|
||||
dangerThreshold: v.dangerThreshold ?? dbModel.viewSettings.dangerThreshold,
|
||||
endMessage: v.endMessage ?? dbModel.viewSettings.endMessage,
|
||||
};
|
||||
|
||||
|
||||
@@ -1,4 +1,9 @@
|
||||
export type ViewSettings = {
|
||||
overrideStyles: boolean;
|
||||
endMessage: string;
|
||||
normalColor: string;
|
||||
warningColor: string;
|
||||
warningThreshold: number;
|
||||
dangerColor: string;
|
||||
dangerThreshold: number;
|
||||
}
|
||||
|
||||
Generated
+12
@@ -75,6 +75,7 @@ importers:
|
||||
prettier: ^2.8.3
|
||||
prop-types: ^15.8.1
|
||||
react: ^18.2.0
|
||||
react-colorful: ^5.6.1
|
||||
react-dom: ^18.2.0
|
||||
react-fast-compare: ^3.2.0
|
||||
react-hook-form: ^7.43.5
|
||||
@@ -113,6 +114,7 @@ importers:
|
||||
deepmerge: 4.3.0
|
||||
framer-motion: 10.11.2_biqbaboplfbrettd7655fr4n2y
|
||||
react: 18.2.0
|
||||
react-colorful: 5.6.1_biqbaboplfbrettd7655fr4n2y
|
||||
react-dom: 18.2.0_react@18.2.0
|
||||
react-fast-compare: 3.2.0
|
||||
react-hook-form: 7.43.5_react@18.2.0
|
||||
@@ -7016,6 +7018,16 @@ packages:
|
||||
react: 18.2.0
|
||||
dev: false
|
||||
|
||||
/react-colorful/5.6.1_biqbaboplfbrettd7655fr4n2y:
|
||||
resolution: {integrity: sha512-1exovf0uGTGyq5mXQT0zgQ80uvj2PCwvF8zY1RN9/vbJVSjSo3fsB/4L3ObbF7u70NduSiK4xu4Y6q1MHoUGEw==}
|
||||
peerDependencies:
|
||||
react: '>=16.8.0'
|
||||
react-dom: '>=16.8.0'
|
||||
dependencies:
|
||||
react: 18.2.0
|
||||
react-dom: 18.2.0_react@18.2.0
|
||||
dev: false
|
||||
|
||||
/react-dom/18.2.0_react@18.2.0:
|
||||
resolution: {integrity: sha512-6IMTriUmvsjHUjNtEDudZfuDQUoWXVxKHhlEGSk81n4YFS+r/Kl99wXiwlVXtPBtJenozv2P+hxDsw9eA7Xo6g==}
|
||||
peerDependencies:
|
||||
|
||||
Reference in New Issue
Block a user