mirror of
https://github.com/cpvalente/ontime.git
synced 2026-08-12 02:43:50 +00:00
@@ -8,25 +8,12 @@
|
||||
"jest": true
|
||||
},
|
||||
"parser": "@typescript-eslint/parser",
|
||||
"extends": [
|
||||
"eslint:recommended",
|
||||
"plugin:@typescript-eslint/recommended",
|
||||
"prettier",
|
||||
"eslint-config-prettier"
|
||||
],
|
||||
"plugins": [
|
||||
"@typescript-eslint",
|
||||
"prettier"
|
||||
],
|
||||
"extends": ["eslint:recommended", "plugin:@typescript-eslint/recommended", "prettier", "eslint-config-prettier"],
|
||||
"plugins": ["@typescript-eslint", "prettier"],
|
||||
"overrides": [
|
||||
{
|
||||
"files": [
|
||||
"e2e/**/**.spec.ts",
|
||||
"e2e/**/**.test.ts"
|
||||
],
|
||||
"extends": [
|
||||
"plugin:playwright/playwright-test"
|
||||
]
|
||||
"files": ["e2e/**/**.spec.ts", "e2e/**/**.test.ts"],
|
||||
"extends": ["plugin:playwright/playwright-test"]
|
||||
}
|
||||
],
|
||||
"rules": {
|
||||
@@ -35,10 +22,13 @@
|
||||
"no-console": [
|
||||
"warn",
|
||||
{
|
||||
"allow": [
|
||||
"warn",
|
||||
"error"
|
||||
]
|
||||
"allow": ["warn", "error"]
|
||||
}
|
||||
],
|
||||
"no-restricted-imports": [
|
||||
"error",
|
||||
{
|
||||
"patterns": ["ontime-types/src/*", "ontime-utils/src/*"]
|
||||
}
|
||||
],
|
||||
"@typescript-eslint/no-non-null-assertion": "warn",
|
||||
@@ -59,4 +49,4 @@
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
import { UserFields } from 'ontime-types';
|
||||
import { TimeFormat } from 'ontime-types/src/definitions/core/TimeFormat.type';
|
||||
import { TimeFormat, UserFields } from 'ontime-types';
|
||||
|
||||
import { ParamField } from './types';
|
||||
|
||||
export const getTimeOption = (timeFormat: TimeFormat): ParamField => ({
|
||||
const getTimeOption = (timeFormat: TimeFormat): ParamField => ({
|
||||
id: 'format',
|
||||
title: '12 / 24 hour timer',
|
||||
description: 'Whether to show the time in 12 or 24 hour mode. Overrides the global setting from preferences',
|
||||
@@ -12,6 +11,22 @@ export const getTimeOption = (timeFormat: TimeFormat): ParamField => ({
|
||||
defaultValue: timeFormat,
|
||||
});
|
||||
|
||||
const hideClockSeconds: ParamField = {
|
||||
id: 'hideClockSeconds',
|
||||
title: 'Hide seconds in clocks',
|
||||
description: 'Whether to hide seconds in auxiliar time fields (not the running timer)',
|
||||
type: 'boolean',
|
||||
defaultValue: false,
|
||||
};
|
||||
|
||||
const hideTimerSeconds: ParamField = {
|
||||
id: 'hideTimerSeconds',
|
||||
title: 'Hide seconds in timer',
|
||||
description: 'Whether to hide seconds in the running timer',
|
||||
type: 'boolean',
|
||||
defaultValue: false,
|
||||
};
|
||||
|
||||
export const getClockOptions = (timeFormat: TimeFormat): ParamField[] => [
|
||||
getTimeOption(timeFormat),
|
||||
{
|
||||
@@ -86,6 +101,8 @@ export const getClockOptions = (timeFormat: TimeFormat): ParamField[] => [
|
||||
|
||||
export const getTimerOptions = (timeFormat: TimeFormat): ParamField[] => [
|
||||
getTimeOption(timeFormat),
|
||||
hideTimerSeconds,
|
||||
hideClockSeconds,
|
||||
{
|
||||
id: 'hideClock',
|
||||
title: 'Hide Time Now',
|
||||
@@ -124,6 +141,7 @@ export const getTimerOptions = (timeFormat: TimeFormat): ParamField[] => [
|
||||
];
|
||||
|
||||
export const MINIMAL_TIMER_OPTIONS: ParamField[] = [
|
||||
hideTimerSeconds,
|
||||
{
|
||||
id: 'key',
|
||||
title: 'Key Colour',
|
||||
@@ -265,6 +283,7 @@ export const LOWER_THIRDS_OPTIONS: ParamField[] = [
|
||||
|
||||
export const getBackstageOptions = (timeFormat: TimeFormat): ParamField[] => [
|
||||
getTimeOption(timeFormat),
|
||||
hideClockSeconds,
|
||||
{
|
||||
id: 'hidePast',
|
||||
title: 'Hide past events',
|
||||
@@ -290,6 +309,7 @@ export const getBackstageOptions = (timeFormat: TimeFormat): ParamField[] => [
|
||||
|
||||
export const getPublicOptions = (timeFormat: TimeFormat): ParamField[] => [
|
||||
getTimeOption(timeFormat),
|
||||
hideClockSeconds,
|
||||
{
|
||||
id: 'hidePast',
|
||||
title: 'Hide past events',
|
||||
@@ -314,6 +334,7 @@ export const getPublicOptions = (timeFormat: TimeFormat): ParamField[] => [
|
||||
];
|
||||
export const getStudioClockOptions = (timeFormat: TimeFormat): ParamField[] => [
|
||||
getTimeOption(timeFormat),
|
||||
hideClockSeconds,
|
||||
{
|
||||
id: 'seconds',
|
||||
title: 'Show Seconds',
|
||||
@@ -389,3 +410,9 @@ export const getOperatorOptions = (userFields: UserFields, timeFormat: TimeForma
|
||||
},
|
||||
];
|
||||
};
|
||||
|
||||
export const getCountdownOptions = (timeFormat: TimeFormat): ParamField[] => [
|
||||
getTimeOption(timeFormat),
|
||||
hideTimerSeconds,
|
||||
hideClockSeconds,
|
||||
];
|
||||
|
||||
@@ -1,7 +1,4 @@
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
//@ts-nocheck -- working on it
|
||||
import { useMutation, useQuery } from '@tanstack/react-query';
|
||||
import { OSCSettings } from 'ontime-types';
|
||||
|
||||
import { queryRefetchIntervalSlow } from '../../ontimeConfig';
|
||||
import { OSC_SETTINGS } from '../api/apiConstants';
|
||||
@@ -13,7 +10,10 @@ import { ontimeQueryClient } from '../queryClient';
|
||||
export default function useOscSettings() {
|
||||
const { data, status, isFetching, isError, refetch } = useQuery({
|
||||
queryKey: OSC_SETTINGS,
|
||||
queryFn: getOSC,
|
||||
queryFn: async () => {
|
||||
const oscData = await getOSC();
|
||||
return { ...oscData, portIn: String(oscData.portIn), portOut: String(oscData.portOut) };
|
||||
},
|
||||
placeholderData: oscPlaceholderSettings,
|
||||
retry: 5,
|
||||
retryDelay: (attempt: number) => attempt * 2500,
|
||||
@@ -21,8 +21,7 @@ export default function useOscSettings() {
|
||||
networkMode: 'always',
|
||||
});
|
||||
|
||||
// we need to jump through some hoops because of the type op port
|
||||
return { data: data! as unknown as OSCSettings, status, isFetching, isError, refetch };
|
||||
return { data: data ?? oscPlaceholderSettings, status, isFetching, isError, refetch };
|
||||
}
|
||||
|
||||
export function useOscSettingsMutation() {
|
||||
|
||||
@@ -14,4 +14,5 @@ export type OverridableOptions = {
|
||||
hideEndMessage?: boolean;
|
||||
language?: string;
|
||||
showProgressBar?: boolean;
|
||||
hideTimerSeconds?: boolean;
|
||||
};
|
||||
|
||||
@@ -36,4 +36,14 @@ describe('formatTime()', () => {
|
||||
const time = formatTime(ms, options, () => '12');
|
||||
expect(time).toStrictEqual('01:00 PM');
|
||||
});
|
||||
|
||||
it('handles negative times', () => {
|
||||
const ms = 1 * 60 * 60 * 1000;
|
||||
const options = {
|
||||
showSeconds: false,
|
||||
format: 'hh:mm:ss',
|
||||
};
|
||||
const time = formatTime(ms * -1, options, () => '24');
|
||||
expect(time).toStrictEqual('-01:00');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -46,12 +46,21 @@ type FormatOptions = {
|
||||
* @param {function} resolver
|
||||
* @return {string}
|
||||
*/
|
||||
export const formatTime = (milliseconds: number | null, options?: FormatOptions, resolver = resolveTimeFormat) => {
|
||||
export const formatTime = (
|
||||
milliseconds: number | null,
|
||||
options?: FormatOptions,
|
||||
resolver = resolveTimeFormat,
|
||||
): string => {
|
||||
if (milliseconds === null) {
|
||||
return '...';
|
||||
}
|
||||
const timeFormat = resolver();
|
||||
const fallback = options?.showSeconds ? 'hh:mm:ss a' : 'hh:mm a';
|
||||
const { showSeconds = false, format: formatString = fallback } = options || {};
|
||||
return timeFormat === '12' ? formatFromMillis(milliseconds, formatString) : millisToString(milliseconds, showSeconds);
|
||||
const isNegative = (milliseconds ?? 0) < 0;
|
||||
const display =
|
||||
timeFormat === '12'
|
||||
? formatFromMillis(Math.abs(milliseconds), formatString)
|
||||
: millisToString(Math.abs(milliseconds), showSeconds);
|
||||
return `${isNegative ? '-' : ''}${display}`;
|
||||
};
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
//@ts-nocheck -- working on it
|
||||
import { useEffect } from 'react';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import { FormControl, Input, Switch } from '@chakra-ui/react';
|
||||
@@ -59,7 +57,6 @@ export default function OscSettings() {
|
||||
};
|
||||
|
||||
const resetForm = () => {
|
||||
// @ts-expect-error -- we know the types dont match
|
||||
reset(data);
|
||||
};
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import QRCode from 'react-qr-code';
|
||||
import { useSearchParams } from 'react-router-dom';
|
||||
import { AnimatePresence, motion } from 'framer-motion';
|
||||
import { Message, OntimeEvent, ProjectData, Settings, SupportedEvent, ViewSettings } from 'ontime-types';
|
||||
import { formatDisplay } from 'ontime-utils';
|
||||
@@ -16,17 +17,13 @@ import ViewParamsEditor from '../../../common/components/view-params-editor/View
|
||||
import { useRuntimeStylesheet } from '../../../common/hooks/useRuntimeStylesheet';
|
||||
import { TimeManagerType } from '../../../common/models/TimeManager.type';
|
||||
import { formatTime } from '../../../common/utils/time';
|
||||
import { isStringBoolean } from '../../../common/utils/viewUtils';
|
||||
import { useTranslation } from '../../../translation/TranslationProvider';
|
||||
import { titleVariants } from '../common/animation';
|
||||
import SuperscriptTime from '../common/superscript-time/SuperscriptTime';
|
||||
|
||||
import './Backstage.scss';
|
||||
|
||||
const formatOptions = {
|
||||
showSeconds: true,
|
||||
format: 'hh:mm:ss a',
|
||||
};
|
||||
|
||||
interface BackstageProps {
|
||||
isMirrored: boolean;
|
||||
publ: Message;
|
||||
@@ -43,6 +40,9 @@ interface BackstageProps {
|
||||
export default function Backstage(props: BackstageProps) {
|
||||
const { isMirrored, publ, eventNow, eventNext, time, backstageEvents, selectedId, general, viewSettings, settings } =
|
||||
props;
|
||||
|
||||
const [searchParams] = useSearchParams();
|
||||
|
||||
const { shouldRender } = useRuntimeStylesheet(viewSettings?.overrideStyles && overrideStylesURL);
|
||||
const { getLocalizedString } = useTranslation();
|
||||
const [blinkClass, setBlinkClass] = useState(false);
|
||||
@@ -68,6 +68,12 @@ export default function Backstage(props: BackstageProps) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const hideSeconds = isStringBoolean(searchParams.get('hideSeconds'));
|
||||
const formatOptions = {
|
||||
showSeconds: !hideSeconds,
|
||||
format: 'hh:mm:ss a',
|
||||
};
|
||||
|
||||
const clock = formatTime(time.clock, formatOptions);
|
||||
const startedAt = formatTime(time.startedAt, formatOptions);
|
||||
const isNegative = (time.current ?? 0) < 0;
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
import { removePrependedZero } from '../viewerUtils.js';
|
||||
|
||||
describe('removePrependedZero', () => {
|
||||
it('should remove "00:" from the start of the string', () => {
|
||||
const timer = '00:10:10';
|
||||
const result = removePrependedZero(timer);
|
||||
expect(result).toBe('10:10');
|
||||
});
|
||||
|
||||
it('should remove "00:0:" from the start of the string', () => {
|
||||
const timer = '00:01:10';
|
||||
const result = removePrependedZero(timer);
|
||||
expect(result).toBe('1:10');
|
||||
});
|
||||
|
||||
it('should not modify the string if it does not start with "00:" or "00:0:"', () => {
|
||||
const timer = '10:10:10';
|
||||
const result = removePrependedZero(timer);
|
||||
expect(result).toBe(timer);
|
||||
});
|
||||
});
|
||||
@@ -8,6 +8,9 @@ interface SuperscriptTimeProps {
|
||||
style?: CSSProperties;
|
||||
}
|
||||
|
||||
/**
|
||||
* @description receives a string like 12:00 AM and adds the period part to the superscript
|
||||
*/
|
||||
export default function SuperscriptTime(props: SuperscriptTimeProps) {
|
||||
const { time, className, style } = props;
|
||||
|
||||
|
||||
@@ -1,43 +1,39 @@
|
||||
import { TimerType } from 'ontime-types';
|
||||
import { formatDisplay } from 'ontime-utils';
|
||||
|
||||
import { TimeManagerType } from '../../../common/models/TimeManager.type';
|
||||
import { formatTime } from '../../../common/utils/time';
|
||||
|
||||
const formatOptions = {
|
||||
showSeconds: true,
|
||||
format: 'hh:mm:ss a',
|
||||
};
|
||||
import type { TimeManagerType } from '../../../common/models/TimeManager.type';
|
||||
|
||||
type TimerTypeParams = Pick<TimeManagerType, 'timerType' | 'current' | 'elapsed' | 'clock'>;
|
||||
|
||||
export function getTimerByType(timerObject?: TimerTypeParams): string | number | null {
|
||||
let timer = null;
|
||||
export function getTimerByType(timerObject?: TimerTypeParams): number | null {
|
||||
if (!timerObject) {
|
||||
return timer;
|
||||
return null;
|
||||
}
|
||||
|
||||
if (timerObject.timerType === TimerType.CountDown || timerObject.timerType === TimerType.TimeToEnd) {
|
||||
timer = timerObject.current;
|
||||
} else if (timerObject.timerType === TimerType.CountUp) {
|
||||
timer = timerObject.elapsed;
|
||||
} else if (timerObject.timerType === TimerType.Clock) {
|
||||
timer = formatTime(timerObject.clock, formatOptions);
|
||||
switch (timerObject.timerType) {
|
||||
case TimerType.CountDown:
|
||||
case TimerType.TimeToEnd:
|
||||
return timerObject.current;
|
||||
case TimerType.CountUp:
|
||||
return Math.abs(timerObject.elapsed ?? 0);
|
||||
case TimerType.Clock:
|
||||
return timerObject.clock;
|
||||
default: {
|
||||
const exhaustiveCheck: never = timerObject.timerType;
|
||||
return exhaustiveCheck;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Receives a string such as 00:10:10 and removes the hours field if it is 00
|
||||
* @param timer
|
||||
*/
|
||||
export const removePrependedZero = (timer: string): string => {
|
||||
if (timer.startsWith('00:0')) {
|
||||
return timer.slice(4);
|
||||
}
|
||||
if (timer.startsWith('00:')) {
|
||||
return timer.slice(3);
|
||||
}
|
||||
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(timer, true);
|
||||
}
|
||||
|
||||
return display;
|
||||
}
|
||||
};
|
||||
|
||||
@@ -5,11 +5,12 @@ import { formatDisplay } from 'ontime-utils';
|
||||
|
||||
import { overrideStylesURL } from '../../../common/api/apiConstants';
|
||||
import NavigationMenu from '../../../common/components/navigation-menu/NavigationMenu';
|
||||
import { getTimeOption } from '../../../common/components/view-params-editor/constants';
|
||||
import { getCountdownOptions } from '../../../common/components/view-params-editor/constants';
|
||||
import ViewParamsEditor from '../../../common/components/view-params-editor/ViewParamsEditor';
|
||||
import { useRuntimeStylesheet } from '../../../common/hooks/useRuntimeStylesheet';
|
||||
import { TimeManagerType } from '../../../common/models/TimeManager.type';
|
||||
import { formatTime } from '../../../common/utils/time';
|
||||
import { isStringBoolean } from '../../../common/utils/viewUtils';
|
||||
import { useTranslation } from '../../../translation/TranslationProvider';
|
||||
import SuperscriptTime from '../common/superscript-time/SuperscriptTime';
|
||||
|
||||
@@ -18,11 +19,6 @@ import CountdownSelect from './CountdownSelect';
|
||||
|
||||
import './Countdown.scss';
|
||||
|
||||
const formatOptions = {
|
||||
showSeconds: true,
|
||||
format: 'hh:mm:ss a',
|
||||
};
|
||||
|
||||
const formatOptionsFinished = {
|
||||
showSeconds: false,
|
||||
format: 'hh:mm a',
|
||||
@@ -99,6 +95,12 @@ export default function Countdown(props: CountdownProps) {
|
||||
const isSelected = runningMessage === TimerMessage.running;
|
||||
const delayedTimerStyles = delay > 0 ? 'aux-timers__value--delayed' : '';
|
||||
|
||||
const hideSeconds = isStringBoolean(searchParams.get('hideSeconds'));
|
||||
const formatOptions = {
|
||||
showSeconds: !hideSeconds,
|
||||
format: 'hh:mm:ss a',
|
||||
};
|
||||
|
||||
const clock = formatTime(time.clock, formatOptions);
|
||||
const startTime = follow === null ? '...' : formatTime(follow.timeStart + delay, formatOptions);
|
||||
const endTime = follow === null ? '...' : formatTime(follow.timeEnd + delay, formatOptions);
|
||||
@@ -110,12 +112,12 @@ export default function Countdown(props: CountdownProps) {
|
||||
isSelected || runningMessage === TimerMessage.waiting,
|
||||
);
|
||||
|
||||
const timeOption = getTimeOption(settings?.timeFormat ?? '24');
|
||||
const timeOption = getCountdownOptions(settings?.timeFormat ?? '24');
|
||||
|
||||
return (
|
||||
<div className={`countdown ${isMirrored ? 'mirror' : ''}`} data-testid='countdown-view'>
|
||||
<NavigationMenu />
|
||||
<ViewParamsEditor paramFields={[timeOption]} />
|
||||
<ViewParamsEditor paramFields={timeOption} />
|
||||
{follow === null ? (
|
||||
<CountdownSelect events={backstageEvents} />
|
||||
) : (
|
||||
|
||||
@@ -9,8 +9,10 @@ import ViewParamsEditor from '../../../common/components/view-params-editor/View
|
||||
import { useRuntimeStylesheet } from '../../../common/hooks/useRuntimeStylesheet';
|
||||
import { TimeManagerType } from '../../../common/models/TimeManager.type';
|
||||
import { OverridableOptions } from '../../../common/models/View.types';
|
||||
import { formatTime } from '../../../common/utils/time';
|
||||
import { isStringBoolean } from '../../../common/utils/viewUtils';
|
||||
import { formatTimerDisplay, getTimerByType } from '../common/viewerUtils';
|
||||
import { useTranslation } from '../../../translation/TranslationProvider';
|
||||
import { getTimerByType, removePrependedZero } from '../common/viewerUtils';
|
||||
|
||||
import './MinimalTimer.scss';
|
||||
|
||||
@@ -24,6 +26,7 @@ interface MinimalTimerProps {
|
||||
export default function MinimalTimer(props: MinimalTimerProps) {
|
||||
const { isMirrored, pres, time, viewSettings } = props;
|
||||
const { shouldRender } = useRuntimeStylesheet(viewSettings?.overrideStyles && overrideStylesURL);
|
||||
const { getLocalizedString } = useTranslation();
|
||||
const [searchParams] = useSearchParams();
|
||||
|
||||
useEffect(() => {
|
||||
@@ -35,9 +38,8 @@ export default function MinimalTimer(props: MinimalTimerProps) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// get config from url: key, text, font, size, hideovertime
|
||||
// eg. http://localhost:3000/minimal?key=f00&text=fff
|
||||
// Check for user options
|
||||
// TODO: this should be tied to the params
|
||||
// USER OPTIONS
|
||||
const userOptions: OverridableOptions = {
|
||||
size: 1,
|
||||
};
|
||||
@@ -126,10 +128,12 @@ export default function MinimalTimer(props: MinimalTimerProps) {
|
||||
const hideEndMessage = searchParams.get('hideendmessage');
|
||||
userOptions.hideEndMessage = isStringBoolean(hideEndMessage);
|
||||
|
||||
const hideTimerSeconds = searchParams.get('hideTimerSeconds');
|
||||
userOptions.hideTimerSeconds = isStringBoolean(hideTimerSeconds);
|
||||
|
||||
const showOverlay = pres.text !== '' && pres.visible;
|
||||
const isPlaying = time.playback !== Playback.Pause;
|
||||
const isNegative =
|
||||
(time.current ?? 0) < 0 && time.timerType !== TimerType.Clock && time.timerType !== TimerType.CountUp;
|
||||
|
||||
const showEndMessage = (time.current ?? 0) < 0 && viewSettings.endMessage && !hideEndMessage;
|
||||
const finished = time.playback === Playback.Play && (time.current ?? 0) < 0 && time.startedAt;
|
||||
const showFinished = finished && !userOptions?.hideOvertime && (time.timerType !== TimerType.Clock || showEndMessage);
|
||||
@@ -149,9 +153,16 @@ export default function MinimalTimer(props: MinimalTimerProps) {
|
||||
: viewSettings.normalColor;
|
||||
|
||||
const stageTimer = getTimerByType(time);
|
||||
let display = formatTimerDisplay(stageTimer);
|
||||
if (isNegative) {
|
||||
display = `-${display}`;
|
||||
let display = '-- : -- : --';
|
||||
if (stageTimer !== null) {
|
||||
display = formatTime(stageTimer, {
|
||||
showSeconds: !userOptions.hideTimerSeconds,
|
||||
format: 'hh:mm:ss a',
|
||||
});
|
||||
display = removePrependedZero(display);
|
||||
if (display.length < 3) {
|
||||
display = `${display} ${getLocalizedString('common.minutes')}`;
|
||||
}
|
||||
}
|
||||
const stageTimerCharacters = display.replace('/:/g', '').length;
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { useEffect } from 'react';
|
||||
import QRCode from 'react-qr-code';
|
||||
import { useSearchParams } from 'react-router-dom';
|
||||
import { AnimatePresence, motion } from 'framer-motion';
|
||||
import { Message, OntimeEvent, ProjectData, Settings, ViewSettings } from 'ontime-types';
|
||||
|
||||
@@ -14,17 +15,13 @@ import ViewParamsEditor from '../../../common/components/view-params-editor/View
|
||||
import { useRuntimeStylesheet } from '../../../common/hooks/useRuntimeStylesheet';
|
||||
import { TimeManagerType } from '../../../common/models/TimeManager.type';
|
||||
import { formatTime } from '../../../common/utils/time';
|
||||
import { isStringBoolean } from '../../../common/utils/viewUtils';
|
||||
import { useTranslation } from '../../../translation/TranslationProvider';
|
||||
import { titleVariants } from '../common/animation';
|
||||
import SuperscriptTime from '../common/superscript-time/SuperscriptTime';
|
||||
|
||||
import './Public.scss';
|
||||
|
||||
const formatOptions = {
|
||||
showSeconds: true,
|
||||
format: 'hh:mm:ss a',
|
||||
};
|
||||
|
||||
interface BackstageProps {
|
||||
isMirrored: boolean;
|
||||
publ: Message;
|
||||
@@ -51,6 +48,9 @@ export default function Public(props: BackstageProps) {
|
||||
viewSettings,
|
||||
settings,
|
||||
} = props;
|
||||
|
||||
const [searchParams] = useSearchParams();
|
||||
|
||||
const { shouldRender } = useRuntimeStylesheet(viewSettings?.overrideStyles && overrideStylesURL);
|
||||
const { getLocalizedString } = useTranslation();
|
||||
|
||||
@@ -64,6 +64,13 @@ export default function Public(props: BackstageProps) {
|
||||
}
|
||||
|
||||
const showPublicMessage = publ.text && publ.visible;
|
||||
|
||||
const hideSeconds = isStringBoolean(searchParams.get('hideSeconds'));
|
||||
const formatOptions = {
|
||||
showSeconds: !hideSeconds,
|
||||
format: 'hh:mm:ss a',
|
||||
};
|
||||
|
||||
const clock = formatTime(time.clock, formatOptions);
|
||||
const qrSize = Math.max(window.innerWidth / 15, 128);
|
||||
|
||||
|
||||
@@ -107,7 +107,7 @@ $orange-active: #f60;
|
||||
}
|
||||
}
|
||||
|
||||
.ampmIndicator {
|
||||
.clock__ampm {
|
||||
font-family: monospace;
|
||||
font-size: calc(var(--clock-size) / 20);
|
||||
position: relative;
|
||||
|
||||
@@ -14,17 +14,13 @@ import { TimeManagerType } from '../../../common/models/TimeManager.type';
|
||||
import { secondsInMillis } from '../../../common/utils/dateConfig';
|
||||
import { formatTime, resolveTimeFormat } from '../../../common/utils/time';
|
||||
import { mth } from '../../../common/utils/timeConstants';
|
||||
import { isStringBoolean } from '../../../common/utils/viewUtils';
|
||||
import SuperscriptTime from '../common/superscript-time/SuperscriptTime';
|
||||
|
||||
import { trimRundown } from './studioClock.utils';
|
||||
|
||||
import './StudioClock.scss';
|
||||
|
||||
const formatOptions = {
|
||||
showSeconds: false,
|
||||
format: 'hh:mm',
|
||||
};
|
||||
|
||||
interface StudioClockProps {
|
||||
isMirrored: boolean;
|
||||
eventNext: OntimeEvent | null;
|
||||
@@ -51,15 +47,17 @@ export default function StudioClock(props: StudioClockProps) {
|
||||
const MAX_TITLES = 11;
|
||||
|
||||
const [searchParams] = useSearchParams();
|
||||
const showSeconds = searchParams.get('seconds');
|
||||
const timeFormat = resolveTimeFormat();
|
||||
formatOptions.showSeconds = Boolean(showSeconds);
|
||||
formatOptions.format = `hh:mm${formatOptions.showSeconds ? ':ss' : ''}`;
|
||||
|
||||
useEffect(() => {
|
||||
document.title = 'ontime - Studio Clock';
|
||||
}, []);
|
||||
|
||||
const hideSeconds = isStringBoolean(searchParams.get('hideSeconds'));
|
||||
const formatOptions = {
|
||||
showSeconds: !hideSeconds,
|
||||
format: 'hh:mm:ss',
|
||||
};
|
||||
|
||||
const clock = formatTime(time.clock, formatOptions);
|
||||
const secondsNow = secondsInMillis(time.clock);
|
||||
const isNegative = (time.current ?? 0) < 0;
|
||||
@@ -70,14 +68,15 @@ export default function StudioClock(props: StudioClockProps) {
|
||||
const delayed = backstageEvents.filter((event) => isOntimeEvent(event)) as OntimeEvent[];
|
||||
const trimmedRundown = trimRundown(delayed, selectedId, MAX_TITLES);
|
||||
const isAm = time.clock / (mth * 12) > 12;
|
||||
const timeFormat = resolveTimeFormat();
|
||||
|
||||
return (
|
||||
<div className={`studio-clock ${isMirrored ? 'mirror' : ''}`} data-testid='studio-view'>
|
||||
<NavigationMenu />
|
||||
<ViewParamsEditor paramFields={studioClockOptions} />
|
||||
<div className='clock-container'>
|
||||
{timeFormat == '12' && <div className='ampmIndicator'>{isAm ? 'am' : 'pm'}</div>}
|
||||
<div className={`studio-timer ${showSeconds ? 'studio-timer--with-seconds' : ''}`}>{clock}</div>
|
||||
{timeFormat == '12' && <div className='clock__ampm'>{isAm ? 'am' : 'pm'}</div>}
|
||||
<div className={`studio-timer ${formatOptions.showSeconds ? 'studio-timer--with-seconds' : ''}`}>{clock}</div>
|
||||
<div
|
||||
ref={titleRef}
|
||||
className='next-title'
|
||||
|
||||
@@ -15,15 +15,10 @@ import { formatTime } from '../../../common/utils/time';
|
||||
import { isStringBoolean } from '../../../common/utils/viewUtils';
|
||||
import { useTranslation } from '../../../translation/TranslationProvider';
|
||||
import SuperscriptTime from '../common/superscript-time/SuperscriptTime';
|
||||
import { formatTimerDisplay, getTimerByType } from '../common/viewerUtils';
|
||||
import { getTimerByType, removePrependedZero } from '../common/viewerUtils';
|
||||
|
||||
import './Timer.scss';
|
||||
|
||||
const formatOptions = {
|
||||
showSeconds: true,
|
||||
format: 'hh:mm:ss a',
|
||||
};
|
||||
|
||||
// motion
|
||||
const titleVariants = {
|
||||
hidden: {
|
||||
@@ -72,6 +67,8 @@ export default function Timer(props: TimerProps) {
|
||||
hideCards: false,
|
||||
hideProgress: false,
|
||||
hideMessage: false,
|
||||
hideTimerSeconds: false,
|
||||
hideClockSeconds: false,
|
||||
};
|
||||
|
||||
const hideClock = searchParams.get('hideClock');
|
||||
@@ -86,12 +83,19 @@ export default function Timer(props: TimerProps) {
|
||||
const hideMessage = searchParams.get('hideMessage');
|
||||
userOptions.hideMessage = isStringBoolean(hideMessage);
|
||||
|
||||
const clock = formatTime(time.clock, formatOptions);
|
||||
const hideClockSeconds = searchParams.get('hideClockSeconds');
|
||||
userOptions.hideClockSeconds = isStringBoolean(hideClockSeconds);
|
||||
const clock = formatTime(time.clock, {
|
||||
showSeconds: !userOptions.hideClockSeconds,
|
||||
format: 'hh:mm:ss a',
|
||||
});
|
||||
|
||||
const hideTimerSeconds = searchParams.get('hideTimerSeconds');
|
||||
userOptions.hideTimerSeconds = isStringBoolean(hideTimerSeconds);
|
||||
|
||||
const showOverlay = pres.text !== '' && pres.visible;
|
||||
const isPlaying = time.playback !== Playback.Pause;
|
||||
|
||||
const isNegative =
|
||||
(time.current ?? 0) < 0 && time.timerType !== TimerType.Clock && time.timerType !== TimerType.CountUp;
|
||||
const finished = time.playback === Playback.Play && (time.current ?? 0) < 0 && time.startedAt;
|
||||
const totalTime = (time.duration ?? 0) + (time.addedTime ?? 0);
|
||||
|
||||
@@ -113,11 +117,18 @@ export default function Timer(props: TimerProps) {
|
||||
: viewSettings.normalColor;
|
||||
|
||||
const stageTimer = getTimerByType(time);
|
||||
let display = formatTimerDisplay(stageTimer);
|
||||
const stageTimerCharacters = display.replace('/:/g', '').length;
|
||||
if (isNegative) {
|
||||
display = `-${display}`;
|
||||
let display = '-- : -- : --';
|
||||
if (stageTimer !== null) {
|
||||
display = formatTime(stageTimer, {
|
||||
showSeconds: !userOptions.hideTimerSeconds,
|
||||
format: 'hh:mm:ss a',
|
||||
});
|
||||
display = removePrependedZero(display);
|
||||
if (display.length < 3) {
|
||||
display = `${display} ${getLocalizedString('common.minutes')}`;
|
||||
}
|
||||
}
|
||||
const stageTimerCharacters = display.replace('/:/g', '').length;
|
||||
|
||||
const baseClasses = `stage-timer ${isMirrored ? 'mirror' : ''} ${showBlackout ? 'blackout' : ''}`;
|
||||
let timerFontSize = 89 / (stageTimerCharacters - 1);
|
||||
|
||||
@@ -3,6 +3,7 @@ import { TranslationObject } from './en';
|
||||
export const langDe: TranslationObject = {
|
||||
'common.end_time': 'Endzeit',
|
||||
'common.expected_finish': 'Voraussichtliches Ende',
|
||||
'common.minutes': 'min',
|
||||
'common.now': 'Jetzt',
|
||||
'common.next': 'Nächste',
|
||||
'common.public_message': 'Öffentliche Nachricht',
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
export const langEn = {
|
||||
'common.end_time': 'End Time',
|
||||
'common.expected_finish': 'Expected Finish',
|
||||
'common.minutes': 'min',
|
||||
'common.now': 'Now',
|
||||
'common.next': 'Next',
|
||||
'common.public_message': 'Public message',
|
||||
|
||||
@@ -3,6 +3,7 @@ import { TranslationObject } from './en';
|
||||
export const langEs: TranslationObject = {
|
||||
'common.end_time': 'Hora de finalización',
|
||||
'common.expected_finish': 'Finalización esperada',
|
||||
'common.minutes': 'min',
|
||||
'common.now': 'Ahora',
|
||||
'common.next': 'Siguiente',
|
||||
'common.public_message': 'Mensaje público',
|
||||
|
||||
@@ -3,6 +3,7 @@ import { TranslationObject } from './en';
|
||||
export const langFr: TranslationObject = {
|
||||
'common.end_time': 'Termine à',
|
||||
'common.expected_finish': 'Fin estimée à',
|
||||
'common.minutes': 'min',
|
||||
'common.now': 'Maintenant',
|
||||
'common.next': 'A suivre',
|
||||
'common.public_message': 'Message public',
|
||||
|
||||
@@ -3,6 +3,7 @@ import { TranslationObject } from './en';
|
||||
export const langIt: TranslationObject = {
|
||||
'common.end_time': 'Ora di Fine',
|
||||
'common.expected_finish': 'Fine Prevista',
|
||||
'common.minutes': 'min',
|
||||
'common.now': 'Adesso',
|
||||
'common.next': 'Prossimo',
|
||||
'common.public_message': 'Messaggio pubblico',
|
||||
@@ -14,6 +15,6 @@ export const langIt: TranslationObject = {
|
||||
'countdown.running': 'Evento in corso',
|
||||
'countdown.select_event': 'Seleziona un evento da seguire',
|
||||
'countdown.to_start': 'Tempo alla partenza',
|
||||
'countdown.waiting': 'In attesa dell\'inizio dell\'evento',
|
||||
'countdown.waiting': "In attesa dell'inizio dell'evento",
|
||||
'countdown.overtime': 'in ritardo',
|
||||
};
|
||||
|
||||
@@ -3,6 +3,7 @@ import { TranslationObject } from './en';
|
||||
export const langNo: TranslationObject = {
|
||||
'common.end_time': 'Sluttid',
|
||||
'common.expected_finish': 'Forventet slutt',
|
||||
'common.minutes': 'min',
|
||||
'common.now': 'Nå',
|
||||
'common.next': 'Neste',
|
||||
'common.public_message': 'Offentlig beskjed',
|
||||
|
||||
@@ -3,6 +3,7 @@ import { TranslationObject } from './en';
|
||||
export const langPt: TranslationObject = {
|
||||
'common.end_time': 'Hora de término',
|
||||
'common.expected_finish': 'Término esperado',
|
||||
'common.minutes': 'min',
|
||||
'common.now': 'Agora',
|
||||
'common.next': 'Próximo',
|
||||
'common.public_message': 'Mensagem pública',
|
||||
|
||||
@@ -3,6 +3,7 @@ import { TranslationObject } from './en';
|
||||
export const langSv: TranslationObject = {
|
||||
'common.end_time': 'Sluttid',
|
||||
'common.expected_finish': 'Förväntat slut',
|
||||
'common.minutes': 'min',
|
||||
'common.now': 'Nu',
|
||||
'common.next': 'Nästa',
|
||||
'common.public_message': 'Offentligt meddelande',
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
import { LogOrigin, OntimeEvent } from 'ontime-types';
|
||||
import { LogOrigin, OntimeEvent, isKeyOfType, isOntimeEvent } from 'ontime-types';
|
||||
import { EventLoader } from '../classes/event-loader/EventLoader.js';
|
||||
import { editEvent } from '../services/rundown-service/RundownService.js';
|
||||
import { coerceString, coerceNumber, coerceBoolean, coerceColour } from '../utils/coerceType.js';
|
||||
import { logger } from '../classes/Logger.js';
|
||||
import { isKeyOfType, isOntimeEvent } from 'ontime-types/src/utils/guards.js';
|
||||
|
||||
const whitelistedPayload = {
|
||||
title: coerceString,
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { Message, TimerMessage } from 'ontime-types';
|
||||
|
||||
import { throttle } from '../../utils/throttle.js';
|
||||
|
||||
import type { PublishFn } from '../../stores/EventStore.js';
|
||||
|
||||
@@ -21,6 +21,7 @@ export type { Settings } from './definitions/core/Settings.type.js';
|
||||
|
||||
// ---> Views
|
||||
export type { ViewSettings } from './definitions/core/Views.type.js';
|
||||
export type { TimeFormat } from './definitions/core/TimeFormat.type.js';
|
||||
|
||||
// ---> Aliases
|
||||
export type { Alias } from './definitions/core/Alias.type.js';
|
||||
@@ -61,5 +62,5 @@ export type { TimerState } from './definitions/runtime/TimerState.type.js';
|
||||
// CLIENT
|
||||
|
||||
// TYPE UTILITIES
|
||||
export { isOntimeBlock, isOntimeDelay, isOntimeEvent } from './utils/guards.js';
|
||||
export { isOntimeBlock, isOntimeDelay, isOntimeEvent, isKeyOfType } from './utils/guards.js';
|
||||
export type { MaybeNumber } from './utils/utils.type.js';
|
||||
|
||||
@@ -23,7 +23,7 @@ export function formatDisplay(milliseconds: number | null, hideZero = false): st
|
||||
return [hours, minutes, s % 60].map(format).join(':');
|
||||
}
|
||||
|
||||
export const millisToSeconds = (millis: number | null): number => {
|
||||
const millisToSeconds = (millis: number | null): number => {
|
||||
if (millis === null) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user