diff --git a/apps/client/package.json b/apps/client/package.json
index 0e1eb9cec..be9717b25 100644
--- a/apps/client/package.json
+++ b/apps/client/package.json
@@ -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",
diff --git a/apps/client/src/common/components/input/popover-picker/PopoverPicker.module.scss b/apps/client/src/common/components/input/popover-picker/PopoverPicker.module.scss
new file mode 100644
index 000000000..67ea73722
--- /dev/null
+++ b/apps/client/src/common/components/input/popover-picker/PopoverPicker.module.scss
@@ -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;
+}
+
diff --git a/apps/client/src/common/components/input/popover-picker/PopoverPicker.tsx b/apps/client/src/common/components/input/popover-picker/PopoverPicker.tsx
new file mode 100644
index 000000000..54bda14c6
--- /dev/null
+++ b/apps/client/src/common/components/input/popover-picker/PopoverPicker.tsx
@@ -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 (
+
+
+
+
+
+
+
+
+ );
+}
diff --git a/apps/client/src/common/components/multi-part-progress-bar/MultiPartProgressBar.scss b/apps/client/src/common/components/multi-part-progress-bar/MultiPartProgressBar.scss
new file mode 100644
index 000000000..98c97f2bc
--- /dev/null
+++ b/apps/client/src/common/components/multi-part-progress-bar/MultiPartProgressBar.scss
@@ -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;
+}
\ No newline at end of file
diff --git a/apps/client/src/common/components/multi-part-progress-bar/MultiPartProgressBar.tsx b/apps/client/src/common/components/multi-part-progress-bar/MultiPartProgressBar.tsx
new file mode 100644
index 000000000..9ccd03380
--- /dev/null
+++ b/apps/client/src/common/components/multi-part-progress-bar/MultiPartProgressBar.tsx
@@ -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 (
+
+ );
+}
diff --git a/apps/client/src/common/components/title-card/TitleCard.tsx b/apps/client/src/common/components/title-card/TitleCard.tsx
index fa785827f..f6b9a43f0 100644
--- a/apps/client/src/common/components/title-card/TitleCard.tsx
+++ b/apps/client/src/common/components/title-card/TitleCard.tsx
@@ -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) {
diff --git a/apps/client/src/common/models/View.types.ts b/apps/client/src/common/models/View.types.ts
index 6ecbeba46..8085a93d0 100644
--- a/apps/client/src/common/models/View.types.ts
+++ b/apps/client/src/common/models/View.types.ts
@@ -13,4 +13,5 @@ export type OverridableOptions = {
hideMessagesOverlay?: boolean;
hideEndMessage?: boolean;
language?: string;
+ showProgressBar?: boolean;
};
diff --git a/apps/client/src/common/models/ViewSettings.type.ts b/apps/client/src/common/models/ViewSettings.type.ts
index 58b31d5f7..d2d72b519 100644
--- a/apps/client/src/common/models/ViewSettings.type.ts
+++ b/apps/client/src/common/models/ViewSettings.type.ts
@@ -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: '',
};
diff --git a/apps/client/src/features/modals/Modals.module.scss b/apps/client/src/features/modals/Modals.module.scss
index 2ca04cee4..9ed25fee7 100644
--- a/apps/client/src/features/modals/Modals.module.scss
+++ b/apps/client/src/features/modals/Modals.module.scss
@@ -52,6 +52,10 @@
}
}
+ .overflow {
+ overflow-y: visible !important;
+ }
+
/* Track */
::-webkit-scrollbar-track {
background: rgba($gray-50, 0.15);
diff --git a/apps/client/src/features/modals/ViewsSettingsModal.jsx b/apps/client/src/features/modals/ViewsSettingsModal.jsx
deleted file mode 100644
index a2eebae6d..000000000
--- a/apps/client/src/features/modals/ViewsSettingsModal.jsx
+++ /dev/null
@@ -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 (
-
-
- Options related to the viewers
-
- 🔥 Changes take effect immediately 🔥
-
-
-
- );
-}
diff --git a/apps/client/src/features/modals/ViewsSettingsModal.tsx b/apps/client/src/features/modals/ViewsSettingsModal.tsx
new file mode 100644
index 000000000..c4b4ad622
--- /dev/null
+++ b/apps/client/src/features/modals/ViewsSettingsModal.tsx
@@ -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)} value - new object parameter value
+ */
+ function handleThresholdChange(field: string, event: ChangeEvent) {
+ 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 (
+
+ );
+}
diff --git a/apps/client/src/features/viewers/minimal-timer/MinimalTimer.tsx b/apps/client/src/features/viewers/minimal-timer/MinimalTimer.tsx
index 4ca3ea6c9..54beb3e1a 100644
--- a/apps/client/src/features/viewers/minimal-timer/MinimalTimer.tsx
+++ b/apps/client/src/features/viewers/minimal-timer/MinimalTimer.tsx
@@ -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) {
{display}
@@ -108,10 +117,15 @@ export default function Timer(props: TimerProps) {
)}
-
diff --git a/apps/server/src/controllers/ontimeController.ts b/apps/server/src/controllers/ontimeController.ts
index f7507d203..ba2dd38b8 100644
--- a/apps/server/src/controllers/ontimeController.ts
+++ b/apps/server/src/controllers/ontimeController.ts
@@ -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);
diff --git a/apps/server/src/models/dataModel.ts b/apps/server/src/models/dataModel.ts
index 9209f4f23..1e895ac73 100644
--- a/apps/server/src/models/dataModel.ts
+++ b/apps/server/src/models/dataModel.ts
@@ -19,6 +19,11 @@ export const dbModel: DatabaseModel = {
},
viewSettings: {
overrideStyles: false,
+ normalColor: '#ffffffcc',
+ warningColor: '#FFAB33',
+ warningThreshold: 120000,
+ dangerColor: '#ED3333',
+ dangerThreshold: 60000,
endMessage: '',
},
aliases: [],
diff --git a/apps/server/src/utils/parserFunctions.ts b/apps/server/src/utils/parserFunctions.ts
index ef5f88df4..ef0210731 100644
--- a/apps/server/src/utils/parserFunctions.ts
+++ b/apps/server/src/utils/parserFunctions.ts
@@ -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,
};
diff --git a/packages/types/src/definitions/core/Views.type.ts b/packages/types/src/definitions/core/Views.type.ts
index d7cb6370d..f4e8ecdd9 100644
--- a/packages/types/src/definitions/core/Views.type.ts
+++ b/packages/types/src/definitions/core/Views.type.ts
@@ -1,4 +1,9 @@
export type ViewSettings = {
overrideStyles: boolean;
endMessage: string;
+ normalColor: string;
+ warningColor: string;
+ warningThreshold: number;
+ dangerColor: string;
+ dangerThreshold: number;
}
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index e42aba488..c60e8d574 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -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: