mirror of
https://github.com/cpvalente/ontime.git
synced 2026-08-25 00:49:10 +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",
|
"deepmerge": "^4.3.0",
|
||||||
"framer-motion": "^10.10.0",
|
"framer-motion": "^10.10.0",
|
||||||
"react": "^18.2.0",
|
"react": "^18.2.0",
|
||||||
|
"react-colorful": "^5.6.1",
|
||||||
"react-dom": "^18.2.0",
|
"react-dom": "^18.2.0",
|
||||||
"react-fast-compare": "^3.2.0",
|
"react-fast-compare": "^3.2.0",
|
||||||
"react-hook-form": "^7.43.5",
|
"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 {
|
interface TitleCardProps {
|
||||||
label: 'now' | 'next';
|
label: 'now' | 'next';
|
||||||
title: string;
|
title: string | null;
|
||||||
subtitle: string;
|
subtitle: string | null;
|
||||||
presenter: string;
|
presenter: string | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function TitleCard(props: TitleCardProps) {
|
export default function TitleCard(props: TitleCardProps) {
|
||||||
|
|||||||
@@ -13,4 +13,5 @@ export type OverridableOptions = {
|
|||||||
hideMessagesOverlay?: boolean;
|
hideMessagesOverlay?: boolean;
|
||||||
hideEndMessage?: boolean;
|
hideEndMessage?: boolean;
|
||||||
language?: string;
|
language?: string;
|
||||||
|
showProgressBar?: boolean;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -2,5 +2,10 @@ import { ViewSettings } from 'ontime-types';
|
|||||||
|
|
||||||
export const viewsSettingsPlaceholder: ViewSettings = {
|
export const viewsSettingsPlaceholder: ViewSettings = {
|
||||||
overrideStyles: false,
|
overrideStyles: false,
|
||||||
|
normalColor: '#ffffffcc',
|
||||||
|
warningColor: '#FFAB33',
|
||||||
|
warningThreshold: 120000,
|
||||||
|
dangerColor: '#ED3333',
|
||||||
|
dangerThreshold: 60000,
|
||||||
endMessage: '',
|
endMessage: '',
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -52,6 +52,10 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.overflow {
|
||||||
|
overflow-y: visible !important;
|
||||||
|
}
|
||||||
|
|
||||||
/* Track */
|
/* Track */
|
||||||
::-webkit-scrollbar-track {
|
::-webkit-scrollbar-track {
|
||||||
background: rgba($gray-50, 0.15);
|
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 =
|
const showFinished =
|
||||||
time.finished && !userOptions?.hideOvertime && (time.timerType !== TimerType.Clock || showEndMessage);
|
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);
|
const stageTimer = getTimerByType(time);
|
||||||
let display = formatTimerDisplay(stageTimer);
|
let display = formatTimerDisplay(stageTimer);
|
||||||
if (isNegative) {
|
if (isNegative) {
|
||||||
@@ -167,7 +178,7 @@ export default function MinimalTimer(props: MinimalTimerProps) {
|
|||||||
<div
|
<div
|
||||||
className={timerClasses}
|
className={timerClasses}
|
||||||
style={{
|
style={{
|
||||||
color: userOptions.textColour,
|
color: timerColor,
|
||||||
fontSize: `${timerFontSize}vw`,
|
fontSize: `${timerFontSize}vw`,
|
||||||
fontFamily: userOptions.font,
|
fontFamily: userOptions.font,
|
||||||
top: userOptions.top,
|
top: userOptions.top,
|
||||||
|
|||||||
@@ -3,8 +3,8 @@ import { AnimatePresence, motion } from 'framer-motion';
|
|||||||
import { EventData, Message, Playback, TimerType, ViewSettings } from 'ontime-types';
|
import { EventData, Message, Playback, TimerType, ViewSettings } from 'ontime-types';
|
||||||
|
|
||||||
import { overrideStylesURL } from '../../../common/api/apiConstants';
|
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 NavigationMenu from '../../../common/components/navigation-menu/NavigationMenu';
|
||||||
import ProgressBar from '../../../common/components/progress-bar/ProgressBar';
|
|
||||||
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 { TimeManagerType } from '../../../common/models/TimeManager.type';
|
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 showEndMessage = (time.current ?? 1) < 0 && viewSettings.endMessage;
|
||||||
const showProgress = time.playback !== Playback.Stop;
|
const showProgress = time.playback !== Playback.Stop;
|
||||||
const showFinished = time.finished && (time.timerType !== TimerType.Clock || showEndMessage);
|
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 showClock = time.timerType !== TimerType.Clock;
|
||||||
const baseClasses = `stage-timer ${isMirrored ? 'mirror' : ''}`;
|
const baseClasses = `stage-timer ${isMirrored ? 'mirror' : ''}`;
|
||||||
|
|
||||||
@@ -101,6 +109,7 @@ export default function Timer(props: TimerProps) {
|
|||||||
className={timerClasses}
|
className={timerClasses}
|
||||||
style={{
|
style={{
|
||||||
fontSize: `${timerFontSize}vw`,
|
fontSize: `${timerFontSize}vw`,
|
||||||
|
color: timerColor,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{display}
|
{display}
|
||||||
@@ -108,10 +117,15 @@ export default function Timer(props: TimerProps) {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<ProgressBar
|
<MultiPartProgressBar
|
||||||
className={isPlaying ? 'progress-container' : 'progress-container progress-container--paused'}
|
className={isPlaying ? 'progress-container' : 'progress-container progress-container--paused'}
|
||||||
now={time.current || 0}
|
now={time.current || 0}
|
||||||
complete={time.duration || 0}
|
complete={time.duration || 0}
|
||||||
|
normalColor={viewSettings.normalColor}
|
||||||
|
warning={viewSettings.warningThreshold}
|
||||||
|
warningColor={viewSettings.warningColor}
|
||||||
|
danger={viewSettings.dangerThreshold}
|
||||||
|
dangerColor={viewSettings.dangerColor}
|
||||||
hidden={!showProgress}
|
hidden={!showProgress}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
|||||||
@@ -246,6 +246,11 @@ export const postViewSettings = async (req, res) => {
|
|||||||
const newData = {
|
const newData = {
|
||||||
overrideStyles: req.body.overrideStyles,
|
overrideStyles: req.body.overrideStyles,
|
||||||
endMessage: req.body?.endMessage || '',
|
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);
|
await DataProvider.setViewSettings(newData);
|
||||||
res.status(200).send(newData);
|
res.status(200).send(newData);
|
||||||
|
|||||||
@@ -19,6 +19,11 @@ export const dbModel: DatabaseModel = {
|
|||||||
},
|
},
|
||||||
viewSettings: {
|
viewSettings: {
|
||||||
overrideStyles: false,
|
overrideStyles: false,
|
||||||
|
normalColor: '#ffffffcc',
|
||||||
|
warningColor: '#FFAB33',
|
||||||
|
warningThreshold: 120000,
|
||||||
|
dangerColor: '#ED3333',
|
||||||
|
dangerThreshold: 60000,
|
||||||
endMessage: '',
|
endMessage: '',
|
||||||
},
|
},
|
||||||
aliases: [],
|
aliases: [],
|
||||||
|
|||||||
@@ -144,6 +144,11 @@ export const parseViewSettings = (data, enforce): ViewSettings => {
|
|||||||
|
|
||||||
const viewSettings = {
|
const viewSettings = {
|
||||||
overrideStyles: v.overrideStyles ?? dbModel.viewSettings.overrideStyles,
|
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,
|
endMessage: v.endMessage ?? dbModel.viewSettings.endMessage,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,9 @@
|
|||||||
export type ViewSettings = {
|
export type ViewSettings = {
|
||||||
overrideStyles: boolean;
|
overrideStyles: boolean;
|
||||||
endMessage: string;
|
endMessage: string;
|
||||||
|
normalColor: string;
|
||||||
|
warningColor: string;
|
||||||
|
warningThreshold: number;
|
||||||
|
dangerColor: string;
|
||||||
|
dangerThreshold: number;
|
||||||
}
|
}
|
||||||
|
|||||||
Generated
+12
@@ -75,6 +75,7 @@ importers:
|
|||||||
prettier: ^2.8.3
|
prettier: ^2.8.3
|
||||||
prop-types: ^15.8.1
|
prop-types: ^15.8.1
|
||||||
react: ^18.2.0
|
react: ^18.2.0
|
||||||
|
react-colorful: ^5.6.1
|
||||||
react-dom: ^18.2.0
|
react-dom: ^18.2.0
|
||||||
react-fast-compare: ^3.2.0
|
react-fast-compare: ^3.2.0
|
||||||
react-hook-form: ^7.43.5
|
react-hook-form: ^7.43.5
|
||||||
@@ -113,6 +114,7 @@ importers:
|
|||||||
deepmerge: 4.3.0
|
deepmerge: 4.3.0
|
||||||
framer-motion: 10.11.2_biqbaboplfbrettd7655fr4n2y
|
framer-motion: 10.11.2_biqbaboplfbrettd7655fr4n2y
|
||||||
react: 18.2.0
|
react: 18.2.0
|
||||||
|
react-colorful: 5.6.1_biqbaboplfbrettd7655fr4n2y
|
||||||
react-dom: 18.2.0_react@18.2.0
|
react-dom: 18.2.0_react@18.2.0
|
||||||
react-fast-compare: 3.2.0
|
react-fast-compare: 3.2.0
|
||||||
react-hook-form: 7.43.5_react@18.2.0
|
react-hook-form: 7.43.5_react@18.2.0
|
||||||
@@ -7016,6 +7018,16 @@ packages:
|
|||||||
react: 18.2.0
|
react: 18.2.0
|
||||||
dev: false
|
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:
|
/react-dom/18.2.0_react@18.2.0:
|
||||||
resolution: {integrity: sha512-6IMTriUmvsjHUjNtEDudZfuDQUoWXVxKHhlEGSk81n4YFS+r/Kl99wXiwlVXtPBtJenozv2P+hxDsw9eA7Xo6g==}
|
resolution: {integrity: sha512-6IMTriUmvsjHUjNtEDudZfuDQUoWXVxKHhlEGSk81n4YFS+r/Kl99wXiwlVXtPBtJenozv2P+hxDsw9eA7Xo6g==}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
|
|||||||
Reference in New Issue
Block a user