mirror of
https://github.com/cpvalente/ontime.git
synced 2026-08-12 10:53:51 +00:00
Feat/161 (#186)
* feat(time): add optional time format * feat(time): show 12 hour time in stage timer * feat(time): override locally * feat(time): show timer in selected format * feat(time): show 12 hour time in table header * refactor: extract time formatting into utility * refactor: migrate datetime library * refactor(formatTime): centralise time formatting for viewers * feature(12hour): version bump
This commit is contained in:
+3
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "ontime-ui",
|
||||
"version": "1.3.0",
|
||||
"version": "1.6.0",
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"@chakra-ui/react": "^2.0.0-next.3",
|
||||
@@ -14,6 +14,8 @@
|
||||
"axios": "^0.25.0",
|
||||
"color": "^4.2.3",
|
||||
"framer-motion": "^6.3.3",
|
||||
"jotai": "^1.7.8",
|
||||
"luxon": "^3.0.1",
|
||||
"react": "^18.1.0",
|
||||
"react-beautiful-dnd": "^13.1.0",
|
||||
"react-dom": "^18.1.0",
|
||||
|
||||
+2
-2
@@ -11,7 +11,7 @@ import AppRouter from './AppRouter';
|
||||
|
||||
// Load Open Sans typeface
|
||||
require('typeface-open-sans');
|
||||
const queryClient = new QueryClient();
|
||||
export const ontimeQueryClient = new QueryClient();
|
||||
|
||||
function App() {
|
||||
|
||||
@@ -46,7 +46,7 @@ function App() {
|
||||
return (
|
||||
<ChakraProvider resetCSS theme={theme}>
|
||||
<SocketProvider>
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<QueryClientProvider client={ontimeQueryClient}>
|
||||
<AppContextProvider>
|
||||
<BrowserRouter>
|
||||
<div className='App'>
|
||||
|
||||
@@ -19,7 +19,12 @@ export const ontimePlaceholderInfo = {
|
||||
* @type {{pinCode: null}}
|
||||
*/
|
||||
export const ontimePlaceholderSettings = {
|
||||
app: 'ontime',
|
||||
version: 1,
|
||||
serverPort: 4001,
|
||||
lock: null,
|
||||
pinCode: null,
|
||||
timeFormat: '24',
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -223,9 +228,7 @@ export const downloadEvents = async () => {
|
||||
filename = headerLine.substring(startFileNameIndex, endFileNameIndex);
|
||||
}
|
||||
|
||||
const url = window.URL.createObjectURL(
|
||||
new Blob([response.data], { type: 'application/json' })
|
||||
);
|
||||
const url = window.URL.createObjectURL(new Blob([response.data], { type: 'application/json' }));
|
||||
const link = document.createElement('a');
|
||||
link.href = url;
|
||||
link.setAttribute('download', filename);
|
||||
@@ -252,4 +255,5 @@ export const uploadEvents = async (file) => {
|
||||
* @description HTTP request to upload events
|
||||
* @return {Promise}
|
||||
*/
|
||||
export const uploadEventsWithPath = async (filepath) => axios.post(`${ontimeURL}/dbpath`, { path: filepath });
|
||||
export const uploadEventsWithPath = async (filepath) =>
|
||||
axios.post(`${ontimeURL}/dbpath`, { path: filepath });
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
import { atomWithStorage, selectAtom } from 'jotai/utils';
|
||||
|
||||
export const eventSettingsAtom = atomWithStorage('ontime-eventSettings', {
|
||||
showQuickEntry: false,
|
||||
startTimeIsLastEnd: false,
|
||||
defaultPublic: false,
|
||||
});
|
||||
|
||||
export const showQuickEntryAtom = selectAtom(
|
||||
eventSettingsAtom,
|
||||
(settings) => settings.showQuickEntry
|
||||
);
|
||||
export const startTimeIsLastEndAtom = selectAtom(
|
||||
eventSettingsAtom,
|
||||
(settings) => settings.startTimeIsLastEnd
|
||||
);
|
||||
export const defaultPublicAtom = selectAtom(
|
||||
eventSettingsAtom,
|
||||
(settings) => settings.defaultPublic
|
||||
);
|
||||
@@ -1,7 +1,7 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import { stringFromMillis } from '../../utils/time';
|
||||
import { formatTime } from '../../utils/time';
|
||||
|
||||
import style from './Paginator.module.scss';
|
||||
|
||||
@@ -9,8 +9,8 @@ export default function TodayItem(props) {
|
||||
const { selected, timeStart, timeEnd, title, backstageEvent, colour } = props;
|
||||
|
||||
// Format timers
|
||||
const start = stringFromMillis(timeStart, false) || '';
|
||||
const end = stringFromMillis(timeEnd, false) || '';
|
||||
const start = formatTime(timeStart, { format: 'hh:mm' });
|
||||
const end = formatTime(timeEnd, { format: 'hh:mm' });
|
||||
|
||||
// user colours
|
||||
const userColour = colour !== '' ? colour : 'transparent';
|
||||
|
||||
@@ -1,32 +0,0 @@
|
||||
import React, { createContext, useState } from 'react';
|
||||
|
||||
export const LocalEventSettingsContext = createContext({
|
||||
showQuickEntry: false,
|
||||
starTimeIsLastEnd: true,
|
||||
defaultPublic: true,
|
||||
|
||||
setShowQuickEntry: () => undefined,
|
||||
setStarTimeIsLastEnd: () => undefined,
|
||||
setDefaultPublic: () => undefined,
|
||||
});
|
||||
|
||||
export const LocalEventSettingsProvider = ({ children }) => {
|
||||
const [showQuickEntry, setShowQuickEntry] = useState(false);
|
||||
const [starTimeIsLastEnd, setStarTimeIsLastEnd] = useState(true);
|
||||
const [defaultPublic, setDefaultPublic] = useState(false);
|
||||
|
||||
return (
|
||||
<LocalEventSettingsContext.Provider
|
||||
value={{
|
||||
showQuickEntry,
|
||||
setShowQuickEntry,
|
||||
starTimeIsLastEnd,
|
||||
setStarTimeIsLastEnd,
|
||||
defaultPublic,
|
||||
setDefaultPublic,
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</LocalEventSettingsContext.Provider>
|
||||
);
|
||||
};
|
||||
@@ -373,10 +373,9 @@ describe('test formatEvents function', () => {
|
||||
isNext: false,
|
||||
colour: "",
|
||||
},
|
||||
|
||||
]
|
||||
|
||||
const parsed = formatEventList(testEvent, selectedId, nextId, true);
|
||||
const parsed = formatEventList(testEvent, selectedId, nextId, { showEnd: true });
|
||||
expect(parsed).toStrictEqual(expected);
|
||||
});
|
||||
|
||||
@@ -403,7 +402,7 @@ describe('test formatEvents function', () => {
|
||||
|
||||
]
|
||||
|
||||
const parsed = formatEventList(testEvent, selectedId, nextId, true);
|
||||
const parsed = formatEventList(testEvent, selectedId, nextId, { showEnd: true });
|
||||
expect(parsed).toStrictEqual(expected);
|
||||
});
|
||||
|
||||
@@ -431,7 +430,7 @@ describe('test formatEvents function', () => {
|
||||
|
||||
]
|
||||
|
||||
const parsed = formatEventList(testEvent, selectedId, nextId, true);
|
||||
const parsed = formatEventList(testEvent, selectedId, nextId, { showEnd: true });
|
||||
expect(parsed).toStrictEqual(expected);
|
||||
});
|
||||
})
|
||||
});
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
import { formatTime } from '../time';
|
||||
|
||||
describe('formatTime()', () => {
|
||||
test('parses 24h strings', () => {
|
||||
const ms = 13 * 60 * 60 * 1000;
|
||||
const options = {
|
||||
showSeconds: true,
|
||||
format: 'irrelevant',
|
||||
};
|
||||
const time = formatTime(ms, options, () => '24');
|
||||
expect(time).toStrictEqual('13:00:00');
|
||||
});
|
||||
|
||||
test('parses same string in 12h strings', () => {
|
||||
const ms = 13 * 60 * 60 * 1000;
|
||||
const options = {
|
||||
showSeconds: true,
|
||||
format: 'hh:mm:ss a',
|
||||
};
|
||||
const time = formatTime(ms, options, () => '12');
|
||||
expect(time).toStrictEqual('01:00:00 PM');
|
||||
});
|
||||
|
||||
test('handles null times', () => {
|
||||
const ms = null;
|
||||
const time = formatTime(ms);
|
||||
expect(time).toStrictEqual('...');
|
||||
});
|
||||
});
|
||||
@@ -1,4 +1,4 @@
|
||||
import { stringFromMillis } from './time';
|
||||
import { formatTime } from './time';
|
||||
|
||||
/**
|
||||
* @description From a list of events, returns only events of type event with calculated delays
|
||||
@@ -58,27 +58,29 @@ export const trimEventlist = (events, selectedId, limit) => {
|
||||
* @param {Object[]} events - given events
|
||||
* @param {string} selectedId - id of currently selected event
|
||||
* @param {string} nextId - id of next event
|
||||
* @param {boolean} [showEnd] - whether to show the end time
|
||||
* @param {object} [options]
|
||||
* @param {boolean} [options.showEnd] - whether to show the end time
|
||||
* @returns {Object[]} Formatted list of events [{time: -, title: -, isNow, isNext}]
|
||||
*/
|
||||
export const formatEventList = (events, selectedId, nextId, showEnd = false) => {
|
||||
export const formatEventList = (events, selectedId, nextId, options) => {
|
||||
if (events == null) return [];
|
||||
const { showEnd = false } = options;
|
||||
|
||||
const givenEvents = [...events];
|
||||
|
||||
// format list
|
||||
const formattedEvents = [];
|
||||
for (const g of givenEvents) {
|
||||
const start = stringFromMillis(g.timeStart, false);
|
||||
const end = stringFromMillis(g.timeEnd, false);
|
||||
for (const event of givenEvents) {
|
||||
const start = formatTime(event.timeStart)
|
||||
const end = formatTime(event.timeEnd);
|
||||
|
||||
formattedEvents.push({
|
||||
id: g.id,
|
||||
id: event.id,
|
||||
time: showEnd ? `${start} - ${end}` : start,
|
||||
title: g.title,
|
||||
isNow: g.id === selectedId,
|
||||
isNext: g.id === nextId,
|
||||
colour: g.colour,
|
||||
title: event.title,
|
||||
isNow: event.id === selectedId,
|
||||
isNext: event.id === nextId,
|
||||
colour: event.colour,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -1,3 +1,8 @@
|
||||
import { DateTime } from 'luxon';
|
||||
|
||||
import { ontimeQueryClient } from '../../App';
|
||||
import { APP_SETTINGS } from '../api/apiConstants';
|
||||
|
||||
const mts = 1000; // millis to seconds
|
||||
const mtm = 1000 * 60; // millis to minutes
|
||||
const mth = 1000 * 60 * 60; // millis to hours
|
||||
@@ -26,12 +31,7 @@ export const nowInMillis = () => {
|
||||
* @param {string} ifNull - what to return if value is null
|
||||
* @returns {string} String representing time 00:12:02
|
||||
*/
|
||||
export const stringFromMillis = (
|
||||
ms,
|
||||
showSeconds = true,
|
||||
delim = ':',
|
||||
ifNull = '...'
|
||||
) => {
|
||||
export const stringFromMillis = (ms, showSeconds = true, delim = ':', ifNull = '...') => {
|
||||
if (ms == null || isNaN(ms)) return ifNull;
|
||||
const isNegative = ms < 0 ? '-' : '';
|
||||
const millis = Math.abs(ms);
|
||||
@@ -54,18 +54,34 @@ export const stringFromMillis = (
|
||||
};
|
||||
|
||||
/**
|
||||
* @description Converts an excel date to milliseconds
|
||||
* @argument {string} excelDate - excel string date
|
||||
* @returns {number} - time in milliseconds
|
||||
* @description Resolves format from url and store
|
||||
* @return {string|undefined}
|
||||
*/
|
||||
export const excelDateStringToMillis = (excelDate) => {
|
||||
const date = new Date(excelDate);
|
||||
if (date instanceof Date && !isNaN(date)) {
|
||||
const h = date.getHours();
|
||||
const m = date.getMinutes();
|
||||
const s = date.getSeconds();
|
||||
export const resolveTimeFormat = () => {
|
||||
const params = new URL(document.location).searchParams;
|
||||
const urlOptions = params.get('format');
|
||||
const settings = ontimeQueryClient.getQueryData(APP_SETTINGS);
|
||||
|
||||
return h * mth + m * mtm + s * mts;
|
||||
}
|
||||
return 0;
|
||||
return urlOptions || settings?.timeFormat;
|
||||
};
|
||||
|
||||
/**
|
||||
/**
|
||||
* @description utility function to format a date in 12 or 24 hour format
|
||||
* @param {number} milliseconds
|
||||
* @param {object} [options]
|
||||
* @param {boolean} [options.showSeconds]
|
||||
* @param {string} [options.format]
|
||||
* @param {function} resolver
|
||||
* @return {string}
|
||||
*/
|
||||
export const formatTime = (milliseconds, options, resolver = resolveTimeFormat) => {
|
||||
if (milliseconds === null) {
|
||||
return '...';
|
||||
}
|
||||
const timeFormat = resolver();
|
||||
const { showSeconds = false, format: formatString = 'hh:mm a' } = options || {};
|
||||
return timeFormat === '12'
|
||||
? DateTime.fromMillis(milliseconds).toUTC().toFormat(formatString)
|
||||
: stringFromMillis(milliseconds, showSeconds);
|
||||
};
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
import React, { lazy, useEffect } from 'react';
|
||||
import React, { lazy } from 'react';
|
||||
import { useDisclosure } from '@chakra-ui/hooks';
|
||||
import { Box } from '@chakra-ui/layout';
|
||||
import ErrorBoundary from 'common/components/errorBoundary/ErrorBoundary';
|
||||
import ModalManager from 'features/modals/ModalManager';
|
||||
|
||||
import { LocalEventSettingsProvider } from '../../common/context/LocalEventSettingsContext';
|
||||
import { LoggingProvider } from '../../common/context/LoggingContext';
|
||||
import MenuBar from '../menu/MenuBar';
|
||||
|
||||
@@ -19,28 +18,24 @@ export default function Editor() {
|
||||
const { isOpen, onOpen, onClose } = useDisclosure();
|
||||
|
||||
// Set window title
|
||||
useEffect(() => {
|
||||
document.title = 'ontime - Editor';
|
||||
}, []);
|
||||
document.title = 'ontime - Editor';
|
||||
|
||||
return (
|
||||
<LoggingProvider>
|
||||
<LocalEventSettingsProvider>
|
||||
<ErrorBoundary>
|
||||
<ModalManager isOpen={isOpen} onClose={onClose} />
|
||||
</ErrorBoundary>
|
||||
<div className={styles.mainContainer}>
|
||||
<Box id='settings' className={styles.settings}>
|
||||
<ErrorBoundary>
|
||||
<MenuBar onOpen={onOpen} isOpen={isOpen} onClose={onClose} />
|
||||
</ErrorBoundary>
|
||||
</Box>
|
||||
<EventList onOpen={onOpen} isOpen={isOpen} onClose={onClose} />
|
||||
<MessageControl />
|
||||
<TimerControl />
|
||||
<Info />
|
||||
</div>
|
||||
</LocalEventSettingsProvider>
|
||||
<ErrorBoundary>
|
||||
<ModalManager isOpen={isOpen} onClose={onClose} />
|
||||
</ErrorBoundary>
|
||||
<div className={styles.mainContainer}>
|
||||
<Box id='settings' className={styles.settings}>
|
||||
<ErrorBoundary>
|
||||
<MenuBar onOpen={onOpen} isOpen={isOpen} onClose={onClose} />
|
||||
</ErrorBoundary>
|
||||
</Box>
|
||||
<EventList onOpen={onOpen} isOpen={isOpen} onClose={onClose} />
|
||||
<MessageControl />
|
||||
<TimerControl />
|
||||
<Info />
|
||||
</div>
|
||||
</LoggingProvider>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,9 +1,13 @@
|
||||
import React, { useContext, useEffect, useState } from 'react';
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { Checkbox } from '@chakra-ui/react';
|
||||
import { Tooltip } from '@chakra-ui/tooltip';
|
||||
import { useAtomValue } from 'jotai';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import { LocalEventSettingsContext } from '../../../common/context/LocalEventSettingsContext';
|
||||
import {
|
||||
defaultPublicAtom,
|
||||
startTimeIsLastEndAtom,
|
||||
} from '../../../common/atoms/LocalEventSettings';
|
||||
|
||||
import style from './EntryBlock.module.scss';
|
||||
|
||||
@@ -16,13 +20,14 @@ export default function EntryBlock(props) {
|
||||
disableAddDelay = true,
|
||||
disableAddBlock,
|
||||
} = props;
|
||||
const { starTimeIsLastEnd, defaultPublic } = useContext(LocalEventSettingsContext);
|
||||
const [doStartTime, setStartTime] = useState(starTimeIsLastEnd);
|
||||
const startTimeIsLastEnd = useAtomValue(startTimeIsLastEndAtom);
|
||||
const defaultPublic = useAtomValue(defaultPublicAtom);
|
||||
const [doStartTime, setStartTime] = useState(startTimeIsLastEnd);
|
||||
const [doPublic, setPublic] = useState(defaultPublic);
|
||||
|
||||
useEffect(() => {
|
||||
setStartTime(starTimeIsLastEnd);
|
||||
}, [starTimeIsLastEnd]);
|
||||
setStartTime(startTimeIsLastEnd);
|
||||
}, [startTimeIsLastEnd]);
|
||||
|
||||
useEffect(() => {
|
||||
setPublic(defaultPublic);
|
||||
@@ -36,7 +41,7 @@ export default function EntryBlock(props) {
|
||||
onClick={() =>
|
||||
eventsHandler(
|
||||
'add',
|
||||
{ type: 'event', after: previousId, isPublic: doPublic },
|
||||
{ type: 'event', after: previousId, isPublic: doPublic },
|
||||
{ startIsLastEnd: doStartTime ? previousId : undefined }
|
||||
)
|
||||
}
|
||||
@@ -68,7 +73,9 @@ export default function EntryBlock(props) {
|
||||
size='sm'
|
||||
colorScheme='blue'
|
||||
isChecked={doStartTime}
|
||||
onChange={(e) => setStartTime(e.target.checked)}
|
||||
onChange={(e) => {
|
||||
setStartTime(e.target.checked);
|
||||
}}
|
||||
>
|
||||
Start time is last end
|
||||
</Checkbox>
|
||||
@@ -93,4 +100,3 @@ EntryBlock.propTypes = {
|
||||
disableAddDelay: PropTypes.bool,
|
||||
disableAddBlock: PropTypes.bool,
|
||||
};
|
||||
|
||||
|
||||
@@ -2,9 +2,10 @@ import React, { createRef, useCallback, useContext, useEffect, useState } from '
|
||||
import { DragDropContext, Droppable } from 'react-beautiful-dnd';
|
||||
import Empty from 'common/components/state/Empty';
|
||||
import { useSocket } from 'common/context/socketContext';
|
||||
import { useAtomValue } from 'jotai';
|
||||
|
||||
import { showQuickEntryAtom } from '../../../common/atoms/LocalEventSettings';
|
||||
import { CursorContext } from '../../../common/context/CursorContext';
|
||||
import { LocalEventSettingsContext } from '../../../common/context/LocalEventSettingsContext';
|
||||
import EntryBlock from '../EntryBlock/EntryBlock';
|
||||
|
||||
import EventListItem from './EventListItem';
|
||||
@@ -19,21 +20,24 @@ export default function EventList(props) {
|
||||
const [selectedId, setSelectedId] = useState(null);
|
||||
const [nextId, setNextId] = useState(null);
|
||||
const cursorRef = createRef();
|
||||
const { showQuickEntry } = useContext(LocalEventSettingsContext);
|
||||
const showQuickEntry = useAtomValue(showQuickEntryAtom);
|
||||
|
||||
const insertAtCursor = useCallback((type, cursor) => {
|
||||
if (cursor === -1) {
|
||||
eventsHandler('add', { type: type });
|
||||
} else {
|
||||
const previousEvent = events[cursor];
|
||||
const nextEvent = events[cursor + 1];
|
||||
if (type === 'event') {
|
||||
eventsHandler('add', { type: type, after: previousEvent.id });
|
||||
} else if (previousEvent?.type !== type && nextEvent?.type !== type) {
|
||||
eventsHandler('add', { type: type, after: previousEvent.id });
|
||||
const insertAtCursor = useCallback(
|
||||
(type, cursor) => {
|
||||
if (cursor === -1) {
|
||||
eventsHandler('add', { type: type });
|
||||
} else {
|
||||
const previousEvent = events[cursor];
|
||||
const nextEvent = events[cursor + 1];
|
||||
if (type === 'event') {
|
||||
eventsHandler('add', { type: type, after: previousEvent.id });
|
||||
} else if (previousEvent?.type !== type && nextEvent?.type !== type) {
|
||||
eventsHandler('add', { type: type, after: previousEvent.id });
|
||||
}
|
||||
}
|
||||
}
|
||||
},[events, eventsHandler])
|
||||
},
|
||||
[events, eventsHandler]
|
||||
);
|
||||
|
||||
// Handle keyboard shortcuts
|
||||
const handleKeyPress = useCallback(
|
||||
@@ -54,19 +58,19 @@ export default function EventList(props) {
|
||||
if (e.key === 'e' || e.key === 'E') {
|
||||
e.preventDefault();
|
||||
if (cursor == null) return;
|
||||
insertAtCursor('event', cursor)
|
||||
insertAtCursor('event', cursor);
|
||||
}
|
||||
// D
|
||||
if (e.key === 'd' || e.key === 'D') {
|
||||
e.preventDefault();
|
||||
if (cursor == null) return;
|
||||
insertAtCursor('delay', cursor)
|
||||
insertAtCursor('delay', cursor);
|
||||
}
|
||||
// B
|
||||
if (e.key === 'b' || e.key === 'B') {
|
||||
e.preventDefault();
|
||||
if (cursor == null) return;
|
||||
insertAtCursor('block', cursor)
|
||||
insertAtCursor('block', cursor);
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@@ -1,7 +1,11 @@
|
||||
import React, { memo, useCallback, useContext } from 'react';
|
||||
import { useAtomValue } from 'jotai';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import { LocalEventSettingsContext } from '../../../common/context/LocalEventSettingsContext';
|
||||
import {
|
||||
defaultPublicAtom,
|
||||
startTimeIsLastEndAtom,
|
||||
} from '../../../common/atoms/LocalEventSettings';
|
||||
import { LoggingContext } from '../../../common/context/LoggingContext';
|
||||
import BlockBlock from '../BlockBlock/BlockBlock';
|
||||
import DelayBlock from '../DelayBlock/DelayBlock';
|
||||
@@ -19,19 +23,11 @@ const areEqual = (prevProps, nextProps) => {
|
||||
};
|
||||
|
||||
const EventListItem = (props) => {
|
||||
const {
|
||||
type,
|
||||
index,
|
||||
eventIndex,
|
||||
data,
|
||||
selected,
|
||||
next,
|
||||
eventsHandler,
|
||||
delay,
|
||||
previousEnd,
|
||||
} = props;
|
||||
const { type, index, eventIndex, data, selected, next, eventsHandler, delay, previousEnd } =
|
||||
props;
|
||||
const { emitError } = useContext(LoggingContext);
|
||||
const { starTimeIsLastEnd, defaultPublic } = useContext(LocalEventSettingsContext);
|
||||
const startTimeIsLastEnd = useAtomValue(startTimeIsLastEndAtom);
|
||||
const defaultPublic = useAtomValue(defaultPublicAtom);
|
||||
|
||||
/**
|
||||
* @description calculates duration from given options
|
||||
@@ -56,7 +52,7 @@ const EventListItem = (props) => {
|
||||
after: data.id,
|
||||
isPublic: defaultPublic,
|
||||
},
|
||||
{ startIsLastEnd: starTimeIsLastEnd ? data.id : undefined }
|
||||
{ startIsLastEnd: startTimeIsLastEnd ? data.id : undefined }
|
||||
);
|
||||
break;
|
||||
case 'delay':
|
||||
@@ -103,7 +99,7 @@ const EventListItem = (props) => {
|
||||
break;
|
||||
}
|
||||
},
|
||||
[calculateDuration, data, defaultPublic, emitError, eventsHandler, starTimeIsLastEnd]
|
||||
[calculateDuration, data, defaultPublic, emitError, eventsHandler, startTimeIsLastEnd]
|
||||
);
|
||||
|
||||
switch (type) {
|
||||
@@ -147,5 +143,5 @@ EventListItem.propTypes = {
|
||||
next: PropTypes.bool,
|
||||
eventsHandler: PropTypes.func,
|
||||
delay: PropTypes.number,
|
||||
previousEnd: PropTypes.number
|
||||
}
|
||||
previousEnd: PropTypes.number,
|
||||
};
|
||||
|
||||
@@ -1,22 +1,32 @@
|
||||
import React, { useCallback, useContext, useEffect, useState } from 'react';
|
||||
import React, { useContext, useEffect, useState } from 'react';
|
||||
import { IconButton } from '@chakra-ui/button';
|
||||
import { ModalBody } from '@chakra-ui/modal';
|
||||
import { Checkbox, FormControl, FormLabel, Input, PinInput, PinInputField } from '@chakra-ui/react';
|
||||
import {
|
||||
Checkbox,
|
||||
FormControl,
|
||||
FormLabel,
|
||||
Input,
|
||||
PinInput,
|
||||
PinInputField,
|
||||
Select,
|
||||
} from '@chakra-ui/react';
|
||||
import { FiEye } from '@react-icons/all-files/fi/FiEye';
|
||||
import { FiX } from '@react-icons/all-files/fi/FiX';
|
||||
import { APP_SETTINGS } from 'common/api/apiConstants';
|
||||
import { getSettings, ontimePlaceholderSettings, postSettings } from 'common/api/ontimeApi';
|
||||
import { useFetch } from 'common/hooks/useFetch';
|
||||
import { useAtom } from 'jotai';
|
||||
|
||||
import { eventSettingsAtom } from '../../common/atoms/LocalEventSettings';
|
||||
import TooltipActionBtn from '../../common/components/buttons/TooltipActionBtn';
|
||||
import { LocalEventSettingsContext } from '../../common/context/LocalEventSettingsContext';
|
||||
import { LoggingContext } from '../../common/context/LoggingContext';
|
||||
|
||||
import { inputProps } from './modalHelper';
|
||||
import SubmitContainer from './SubmitContainer';
|
||||
|
||||
import style from './Modals.module.scss';
|
||||
const version = require('../../../package.json').version
|
||||
|
||||
const version = require('../../../package.json').version;
|
||||
|
||||
export default function AppSettingsModal() {
|
||||
const { data, status, refetch } = useFetch(APP_SETTINGS, getSettings);
|
||||
@@ -26,18 +36,8 @@ export default function AppSettingsModal() {
|
||||
const [submitting, setSubmitting] = useState(false);
|
||||
const [hidePin, setHidePin] = useState(true);
|
||||
|
||||
const {
|
||||
showQuickEntry,
|
||||
setShowQuickEntry,
|
||||
starTimeIsLastEnd,
|
||||
setStarTimeIsLastEnd,
|
||||
defaultPublic,
|
||||
setDefaultPublic,
|
||||
} = useContext(LocalEventSettingsContext);
|
||||
|
||||
const [doShowQuickEntry, setDoShowQuickEntry] = useState(showQuickEntry);
|
||||
const [doStarTimeIsLastEnd, setDoStarTimeIsLastEnd] = useState(starTimeIsLastEnd);
|
||||
const [doDefaultPublic, setDoDefaultPublic] = useState(defaultPublic);
|
||||
const [eventSettings, saveEventSettings] = useAtom(eventSettingsAtom);
|
||||
const [formSettings, setFormSettings] = useState(eventSettings);
|
||||
|
||||
/**
|
||||
* Set formdata from server state
|
||||
@@ -47,117 +47,72 @@ export default function AppSettingsModal() {
|
||||
if (changed) return;
|
||||
setFormData({
|
||||
pinCode: data.pinCode,
|
||||
timeFormat: data.timeFormat,
|
||||
});
|
||||
}, [changed, data]);
|
||||
|
||||
/**
|
||||
* Set formdata from context
|
||||
*/
|
||||
useEffect(() => {
|
||||
if (showQuickEntry == null) return;
|
||||
setDoShowQuickEntry(showQuickEntry);
|
||||
}, [showQuickEntry]);
|
||||
|
||||
useEffect(() => {
|
||||
if (starTimeIsLastEnd == null) return;
|
||||
setDoStarTimeIsLastEnd(starTimeIsLastEnd);
|
||||
}, [starTimeIsLastEnd]);
|
||||
|
||||
useEffect(() => {
|
||||
if (defaultPublic == null) return;
|
||||
setDoDefaultPublic(defaultPublic);
|
||||
}, [defaultPublic]);
|
||||
|
||||
/**
|
||||
* Validate and submit data
|
||||
*/
|
||||
const submitHandler = useCallback(
|
||||
async (event) => {
|
||||
event.preventDefault();
|
||||
setSubmitting(true);
|
||||
const submitHandler = async (event) => {
|
||||
event.preventDefault();
|
||||
setSubmitting(true);
|
||||
|
||||
// set context
|
||||
setShowQuickEntry(doShowQuickEntry);
|
||||
setStarTimeIsLastEnd(doStarTimeIsLastEnd);
|
||||
setDefaultPublic(doDefaultPublic);
|
||||
// set context
|
||||
saveEventSettings(formSettings);
|
||||
const validation = { isValid: false };
|
||||
|
||||
const f = formData;
|
||||
|
||||
// we might not have changed this
|
||||
if (f.pinCode !== data.pinCode) {
|
||||
const e = { status: false, message: '' };
|
||||
|
||||
// Validate fields
|
||||
if (f.pinCode === '' || f.pinCode == null) {
|
||||
e.status = true;
|
||||
e.message += 'App pin code removed';
|
||||
} else {
|
||||
e.status = true;
|
||||
e.message += 'App pin code added';
|
||||
}
|
||||
|
||||
// set fields with error
|
||||
if (!e.status) {
|
||||
emitError(`Invalid Input: ${e.message}`);
|
||||
} else {
|
||||
await postSettings(formData);
|
||||
await refetch();
|
||||
emitWarning(e.message);
|
||||
setChanged(false);
|
||||
}
|
||||
// we might not have changed this
|
||||
if (formData.pinCode !== data.pinCode) {
|
||||
// Validate fields
|
||||
if (formData.pinCode === '' || formData.pinCode == null) {
|
||||
validation.isValid = true;
|
||||
validation.message += 'App pin code removed';
|
||||
} else {
|
||||
validation.isValid = true;
|
||||
validation.message += 'App pin code added';
|
||||
}
|
||||
setSubmitting(false);
|
||||
setChanged(false);
|
||||
},
|
||||
[
|
||||
data.pinCode,
|
||||
doDefaultPublic,
|
||||
doShowQuickEntry,
|
||||
doStarTimeIsLastEnd,
|
||||
emitError,
|
||||
emitWarning,
|
||||
formData,
|
||||
refetch,
|
||||
setDefaultPublic,
|
||||
setShowQuickEntry,
|
||||
setStarTimeIsLastEnd,
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
if (formData.timeFormat !== data.timeFormat) {
|
||||
if (formData.timeFormat === '12' || formData.timeFormat === '24') {
|
||||
validation.isValid = true;
|
||||
}
|
||||
}
|
||||
|
||||
// set fields with error
|
||||
if (!validation.isValid) {
|
||||
emitError(`Invalid Input: ${validation.message}`);
|
||||
} else {
|
||||
await postSettings(formData);
|
||||
await refetch();
|
||||
validation?.message && emitWarning(validation.message);
|
||||
}
|
||||
setSubmitting(false);
|
||||
setChanged(false);
|
||||
};
|
||||
|
||||
/**
|
||||
* Reverts local state equals to server state
|
||||
*/
|
||||
const revert = useCallback(async () => {
|
||||
const revert = async () => {
|
||||
setChanged(false);
|
||||
await refetch();
|
||||
|
||||
// set from context
|
||||
setDoShowQuickEntry(showQuickEntry);
|
||||
setDoStarTimeIsLastEnd(starTimeIsLastEnd);
|
||||
setDoDefaultPublic(defaultPublic);
|
||||
}, [defaultPublic, refetch, showQuickEntry, starTimeIsLastEnd]);
|
||||
setFormSettings(eventSettings);
|
||||
await refetch();
|
||||
};
|
||||
|
||||
/**
|
||||
* Handles change of input field in local state
|
||||
* @param {string} field - object parameter to update
|
||||
* @param {string} value - new object parameter value
|
||||
*/
|
||||
const handleChange = useCallback(
|
||||
(field, value) => {
|
||||
const temp = { ...formData };
|
||||
temp[field] = value;
|
||||
setFormData(temp);
|
||||
setChanged(true);
|
||||
},
|
||||
[formData]
|
||||
);
|
||||
|
||||
/**
|
||||
* Sets changed flag to true
|
||||
*/
|
||||
const handleContextChange = useCallback(() => {
|
||||
const handleChange = (field, value) => {
|
||||
const temp = { ...formData };
|
||||
temp[field] = value;
|
||||
setFormData(temp);
|
||||
setChanged(true);
|
||||
}, []);
|
||||
};
|
||||
|
||||
const disableModal = status !== 'success';
|
||||
|
||||
@@ -201,6 +156,7 @@ export default function AppSettingsModal() {
|
||||
<PinInput
|
||||
{...inputProps}
|
||||
type='alphanumeric'
|
||||
name='pinCode'
|
||||
defaultValue=''
|
||||
value={formData.pinCode}
|
||||
mask={hidePin}
|
||||
@@ -235,31 +191,52 @@ export default function AppSettingsModal() {
|
||||
</div>
|
||||
</FormControl>
|
||||
</div>
|
||||
<div className={style.modalColumn}>
|
||||
<FormControl id='timeFormat'>
|
||||
<FormLabel htmlFor='timeFormat'>
|
||||
Time format
|
||||
<span className={style.labelNote}>
|
||||
<br />
|
||||
12 / 24 hour format (viewers only for now)
|
||||
</span>
|
||||
</FormLabel>
|
||||
<Select
|
||||
size='sm'
|
||||
name='timeFormat'
|
||||
value={formData.timeFormat}
|
||||
isDisabled={disableModal}
|
||||
onChange={(event) => handleChange('timeFormat', event.target.value)}
|
||||
>
|
||||
<option value='12'>12 hours eg. 11:00:10 PM</option>
|
||||
<option value='24'>24 hours eg. 23:00:10</option>
|
||||
</Select>
|
||||
</FormControl>
|
||||
</div>
|
||||
<div className={style.hSeparator}>Create Event Default Settings</div>
|
||||
<div className={style.modalColumn}>
|
||||
<Checkbox
|
||||
isChecked={doShowQuickEntry}
|
||||
isChecked={formSettings.showQuickEntry}
|
||||
onChange={(e) => {
|
||||
setDoShowQuickEntry(e.target.checked);
|
||||
handleContextChange();
|
||||
setFormSettings((prev) => ({ ...prev, showQuickEntry: e.target.checked }));
|
||||
setChanged(true);
|
||||
}}
|
||||
>
|
||||
Show quick entry on hover
|
||||
</Checkbox>
|
||||
<Checkbox
|
||||
isChecked={doStarTimeIsLastEnd}
|
||||
isChecked={formSettings.startTimeIsLastEnd}
|
||||
onChange={(e) => {
|
||||
setDoStarTimeIsLastEnd(e.target.checked);
|
||||
handleContextChange();
|
||||
setFormSettings((prev) => ({ ...prev, startTimeIsLastEnd: e.target.checked }));
|
||||
setChanged(true);
|
||||
}}
|
||||
>
|
||||
Start time is last end
|
||||
</Checkbox>
|
||||
<Checkbox
|
||||
isChecked={doDefaultPublic}
|
||||
isChecked={formSettings.defaultPublic}
|
||||
onChange={(e) => {
|
||||
setDoDefaultPublic(e.target.checked);
|
||||
handleContextChange();
|
||||
setFormSettings((prev) => ({ ...prev, defaultPublic: e.target.checked }));
|
||||
setChanged(true);
|
||||
}}
|
||||
>
|
||||
Event default public
|
||||
|
||||
@@ -32,7 +32,7 @@
|
||||
grid-template-areas:
|
||||
'name playback running time actions'
|
||||
'now playback running time actions';
|
||||
grid-template-columns: 1fr auto 10em 10em auto;
|
||||
grid-template-columns: 1fr auto 10em 12.5em auto;
|
||||
align-items: center;
|
||||
padding: 0.25em 1em;
|
||||
|
||||
|
||||
@@ -10,7 +10,7 @@ import { useSocket } from '../../common/context/socketContext';
|
||||
import { TableSettingsContext } from '../../common/context/TableSettingsContext';
|
||||
import { useFetch } from '../../common/hooks/useFetch';
|
||||
import { formatDisplay } from '../../common/utils/dateConfig';
|
||||
import { stringFromMillis } from '../../common/utils/time';
|
||||
import { formatTime } from '../../common/utils/time';
|
||||
|
||||
import PlaybackIcon from './tableElements/PlaybackIcon';
|
||||
|
||||
@@ -97,7 +97,10 @@ export default function TableHeader() {
|
||||
|
||||
// prepare presentation variables
|
||||
const timerNow = `${timer.running < 0 ? '-' : ''}${formatDisplay(timer.running)}`;
|
||||
|
||||
const timeNow = formatTime(timer.clock, {
|
||||
showSeconds: true,
|
||||
format: 'hh:mm:ss a',
|
||||
});
|
||||
return (
|
||||
<div className={style.header}>
|
||||
<div className={style.headerName}>{data?.title || ''}</div>
|
||||
@@ -115,7 +118,7 @@ export default function TableHeader() {
|
||||
<div className={style.headerClock}>
|
||||
<span className={style.label}>Time Now</span>
|
||||
<br />
|
||||
<span className={style.timer}>{stringFromMillis(timer.clock)}</span>
|
||||
<span className={style.timer}>{timeNow}</span>
|
||||
</div>
|
||||
<div className={style.headerActions}>
|
||||
<Tooltip openDelay={300} label='Follow selected'>
|
||||
|
||||
@@ -1,12 +1,11 @@
|
||||
/* eslint-disable react/display-name */
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { EVENT_TABLE, EVENTS_TABLE } from 'common/api/apiConstants';
|
||||
import { fetchEvent } from 'common/api/eventApi';
|
||||
import { fetchAllEvents } from 'common/api/eventsApi';
|
||||
import { useSocket } from 'common/context/socketContext';
|
||||
import { useFetch } from 'common/hooks/useFetch';
|
||||
|
||||
import { stringFromMillis } from '../../common/utils/time';
|
||||
import { EVENT_TABLE, EVENTS_TABLE } from '../../common/api/apiConstants';
|
||||
import { fetchEvent } from '../../common/api/eventApi';
|
||||
import { fetchAllEvents } from '../../common/api/eventsApi';
|
||||
import { useSocket } from '../../common/context/socketContext';
|
||||
import { useFetch } from '../../common/hooks/useFetch';
|
||||
|
||||
const withSocket = (Component) => {
|
||||
return (props) => {
|
||||
@@ -30,9 +29,9 @@ const withSocket = (Component) => {
|
||||
visible: false,
|
||||
});
|
||||
const [timer, setTimer] = useState({
|
||||
clock: null,
|
||||
running: null,
|
||||
isNegative: null,
|
||||
clock: 0,
|
||||
running: 0,
|
||||
isNegative: false,
|
||||
startedAt: null,
|
||||
expectedFinish: null,
|
||||
});
|
||||
@@ -67,7 +66,9 @@ const withSocket = (Component) => {
|
||||
|
||||
// Ask for update on load
|
||||
useEffect(() => {
|
||||
if (socket == null) return;
|
||||
if (!socket) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Handle timer messages
|
||||
socket.on('messages-timer', (data) => {
|
||||
@@ -154,12 +155,12 @@ const withSocket = (Component) => {
|
||||
|
||||
// Filter events only to pass down
|
||||
useEffect(() => {
|
||||
if (eventsData == null) return;
|
||||
if (!eventsData) {
|
||||
return;
|
||||
}
|
||||
// filter just events with title
|
||||
if (Array.isArray(eventsData)) {
|
||||
const pe = eventsData.filter(
|
||||
(d) => d.type === 'event' && d.title !== '' && d.isPublic
|
||||
);
|
||||
const pe = eventsData.filter((d) => d.type === 'event' && d.title !== '' && d.isPublic);
|
||||
setPublicEvents(pe);
|
||||
|
||||
// everything goes backstage
|
||||
@@ -169,10 +170,13 @@ const withSocket = (Component) => {
|
||||
|
||||
// Set general data
|
||||
useEffect(() => {
|
||||
if (genData == null) return;
|
||||
if (!genData) {
|
||||
return;
|
||||
}
|
||||
setGeneral(genData);
|
||||
}, [genData]);
|
||||
|
||||
|
||||
/********************************************/
|
||||
/*** + titleManager ***/
|
||||
/*** WRAP INFORMATION RELATED TO TITLES ***/
|
||||
@@ -221,9 +225,6 @@ const withSocket = (Component) => {
|
||||
const timeManager = {
|
||||
...timer,
|
||||
finished: playback === 'start' && timer.isNegative && timer.startedAt,
|
||||
clock: stringFromMillis(timer.clock),
|
||||
clockMs: timer.clock,
|
||||
clockNoSeconds: stringFromMillis(timer.clock, false),
|
||||
playstate: playback,
|
||||
};
|
||||
|
||||
|
||||
@@ -8,10 +8,16 @@ import { AnimatePresence, motion } from 'framer-motion';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import { getEventsWithDelay } from '../../../common/utils/eventsManager';
|
||||
import { formatTime } from '../../../common/utils/time';
|
||||
import { titleVariants } from '../common/animation';
|
||||
|
||||
import style from './StageManager.module.scss';
|
||||
|
||||
const formatOptions = {
|
||||
showSeconds: true,
|
||||
format: 'hh:mm:ss a',
|
||||
};
|
||||
|
||||
export default function StageManager(props) {
|
||||
const { publ, title, time, backstageEvents, selectedId, general } = props;
|
||||
const [filteredEvents, setFilteredEvents] = useState(null);
|
||||
@@ -41,6 +47,8 @@ export default function StageManager(props) {
|
||||
if (time.isNegative) stageTimer = `-${stageTimer}`;
|
||||
}
|
||||
|
||||
const clock = formatTime(time.clock, formatOptions);
|
||||
|
||||
return (
|
||||
<div className={style.container__gray}>
|
||||
<NavLogo />
|
||||
@@ -118,7 +126,7 @@ export default function StageManager(props) {
|
||||
|
||||
<div className={style.clockContainer}>
|
||||
<div className={style.label}>Time Now</div>
|
||||
<div className={style.clock}>{time.clock}</div>
|
||||
<div className={style.clock}>{clock}</div>
|
||||
</div>
|
||||
|
||||
<div className={style.countdownContainer}>
|
||||
|
||||
@@ -6,19 +6,24 @@ import NavLogo from '../../../common/components/nav/NavLogo';
|
||||
import Empty from '../../../common/components/state/Empty';
|
||||
import { formatDisplay, millisToSeconds } from '../../../common/utils/dateConfig';
|
||||
import getDelayTo from '../../../common/utils/getDelayTo';
|
||||
import { stringFromMillis } from '../../../common/utils/time';
|
||||
import { formatTime } from '../../../common/utils/time';
|
||||
|
||||
import { fetchTimerData, sanitiseTitle, timerMessages } from './countdown.helpers';
|
||||
|
||||
import style from './Countdown.module.scss';
|
||||
|
||||
const formatOptions = {
|
||||
showSeconds: true,
|
||||
format: 'hh:mm:ss a',
|
||||
};
|
||||
|
||||
export default function Countdown(props) {
|
||||
const [searchParams] = useSearchParams();
|
||||
const { backstageEvents, time, selectedId } = props;
|
||||
const [follow, setFollow] = useState(null);
|
||||
const [runningTimer, setRunningTimer] = useState(0);
|
||||
const [runningMessage, setRunningMessage] = useState('');
|
||||
const [delay, setDelay] = useState(0);
|
||||
const [searchParams] = useSearchParams();
|
||||
|
||||
// Set window title
|
||||
useEffect(() => {
|
||||
@@ -46,8 +51,8 @@ export default function Countdown(props) {
|
||||
if (typeof followThis !== 'undefined') {
|
||||
setFollow(followThis);
|
||||
const idx = backstageEvents.findIndex((event) => event.id === followThis.id);
|
||||
const delay = getDelayTo(backstageEvents, idx);
|
||||
setDelay(delay);
|
||||
const delayToEvent = getDelayTo(backstageEvents, idx);
|
||||
setDelay(delayToEvent);
|
||||
}
|
||||
}, [backstageEvents, searchParams]);
|
||||
|
||||
@@ -73,6 +78,16 @@ export default function Countdown(props) {
|
||||
|
||||
const isSelected = useMemo(() => runningMessage === timerMessages.running, [runningMessage]);
|
||||
|
||||
const clock = formatTime(time.clock, formatOptions);
|
||||
const startTime =
|
||||
follow === null
|
||||
? '...'
|
||||
: formatTime(follow.timeStart + delay, formatOptions);
|
||||
const endTime =
|
||||
follow === null
|
||||
? '...'
|
||||
: formatTime(follow.timeEnd + delay, formatOptions);
|
||||
|
||||
return (
|
||||
<div className={style.container}>
|
||||
<NavLogo />
|
||||
@@ -100,19 +115,17 @@ export default function Countdown(props) {
|
||||
<div className={style.timers}>
|
||||
<div className={style.timer}>
|
||||
<div className={style.label}>Time Now</div>
|
||||
<span className={style.value}>{time.clock}</span>
|
||||
<span className={style.value}>{clock}</span>
|
||||
</div>
|
||||
<div className={style.timer}>
|
||||
<div className={style.label}>Start Time</div>
|
||||
<span className={`${style.value} ${delay > 0 ? style.delayed : ''}`}>
|
||||
{stringFromMillis(follow.timeStart + delay)}
|
||||
{startTime}
|
||||
</span>
|
||||
</div>
|
||||
<div className={style.timer}>
|
||||
<div className={style.label}>End Time</div>
|
||||
<span className={`${style.value} ${delay > 0 ? style.delayed : ''}`}>
|
||||
{stringFromMillis(follow.timeEnd + delay)}
|
||||
</span>
|
||||
<span className={`${style.value} ${delay > 0 ? style.delayed : ''}`}>{endTime}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className={style.status}>{runningMessage}</div>
|
||||
@@ -126,7 +139,7 @@ export default function Countdown(props) {
|
||||
isSelected || time.waiting
|
||||
)}
|
||||
</span>
|
||||
<div className={style.title}>{follow.title || 'Untitled Event'}</div>
|
||||
<div className={style.title}>{follow?.title || 'Untitled Event'}</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
@@ -137,4 +150,5 @@ Countdown.propTypes = {
|
||||
backstageEvents: PropTypes.array,
|
||||
time: PropTypes.object,
|
||||
selectedId: PropTypes.string,
|
||||
settings: PropTypes.object,
|
||||
};
|
||||
|
||||
@@ -35,7 +35,7 @@ describe('fetchTimerData() function', () => {
|
||||
const startMockValue = 10000;
|
||||
const timeNow = 1000;
|
||||
const follow = { id: 'anotherevent', timeStart: startMockValue };
|
||||
const time = { clockMs: timeNow };
|
||||
const time = { clock: timeNow };
|
||||
|
||||
const { message, timer } = fetchTimerData(time, follow, 'notthesameevent');
|
||||
expect(message).toBe(timerMessages.toStart);
|
||||
@@ -48,7 +48,7 @@ describe('fetchTimerData() function', () => {
|
||||
const timeNow = 15000;
|
||||
const followId = 'testId';
|
||||
const follow = { id: followId, timeStart: startMockValue, timeEnd: endMockValue };
|
||||
const time = { clockMs: timeNow, running: endMockValue - startMockValue };
|
||||
const time = { clock: timeNow, running: endMockValue - startMockValue };
|
||||
|
||||
const { message, timer } = fetchTimerData(time, follow, 'notthesameevent');
|
||||
expect(message).toBe(timerMessages.waiting);
|
||||
@@ -61,7 +61,7 @@ describe('fetchTimerData() function', () => {
|
||||
const timeNow = 30000;
|
||||
const followId = 'testId';
|
||||
const follow = { id: followId, timeStart: startMockValue, timeEnd: endMockValue };
|
||||
const time = { clockMs: timeNow, running: endMockValue - startMockValue };
|
||||
const time = { clock: timeNow, running: endMockValue - startMockValue };
|
||||
|
||||
const { message, timer } = fetchTimerData(time, follow, 'notthesameevent');
|
||||
expect(message).toBe(timerMessages.ended);
|
||||
@@ -74,7 +74,7 @@ describe('fetchTimerData() function', () => {
|
||||
const timeNow = 15000;
|
||||
const followId = 'testId';
|
||||
const follow = { id: followId, timeStart: startMockValue, timeEnd: endMockValue };
|
||||
const time = { clockMs: timeNow, running: DAY_TO_MS + endMockValue - startMockValue };
|
||||
const time = { clock: timeNow, running: DAY_TO_MS + endMockValue - startMockValue };
|
||||
|
||||
const { message, timer } = fetchTimerData(time, follow, 'notthesameevent');
|
||||
expect(message).toBe(timerMessages.waiting);
|
||||
@@ -87,7 +87,7 @@ describe('fetchTimerData() function', () => {
|
||||
const timeNow = 15000;
|
||||
const followId = 'testId';
|
||||
const follow = { id: followId, timeStart: startMockValue, timeEnd: endMockValue };
|
||||
const time = { clockMs: timeNow, running: DAY_TO_MS + endMockValue - startMockValue };
|
||||
const time = { clock: timeNow, running: DAY_TO_MS + endMockValue - startMockValue };
|
||||
|
||||
const { message, timer } = fetchTimerData(time, follow, followId);
|
||||
expect(message).toBe(timerMessages.running);
|
||||
@@ -100,7 +100,7 @@ describe('fetchTimerData() function', () => {
|
||||
const timeNow = 2000;
|
||||
const followId = 'testId';
|
||||
const follow = { id: followId, timeStart: startMockValue, timeEnd: endMockValue };
|
||||
const time = { clockMs: timeNow, running: DAY_TO_MS + endMockValue - startMockValue };
|
||||
const time = { clock: timeNow, running: DAY_TO_MS + endMockValue - startMockValue };
|
||||
|
||||
const { message, timer } = fetchTimerData(time, follow, 'notthesameevent');
|
||||
expect(message).toBe(timerMessages.toStart);
|
||||
|
||||
@@ -34,22 +34,22 @@ export const fetchTimerData = (time, follow, selectedId) => {
|
||||
// check that is not running
|
||||
message = time.playstate === 'pause' ? timerMessages.waiting : timerMessages.running;
|
||||
timer = time.running;
|
||||
} else if (time.clockMs < follow.timeStart) {
|
||||
} else if (time.clock < follow.timeStart) {
|
||||
// if it hasnt started, we count to start
|
||||
message = timerMessages.toStart;
|
||||
timer = millisToSeconds(follow.timeStart - time.clockMs);
|
||||
} else if (follow.timeStart <= time.clockMs && time.clockMs <= follow.timeEnd) {
|
||||
timer = millisToSeconds(follow.timeStart - time.clock);
|
||||
} else if (follow.timeStart <= time.clock && time.clock <= follow.timeEnd) {
|
||||
// if it has started, we show running timer
|
||||
message = timerMessages.waiting;
|
||||
timer = time.running;
|
||||
} else {
|
||||
if (follow.timeStart > follow.timeEnd) {
|
||||
// ends day after
|
||||
if (follow.timeStart > time.clockMs ) {
|
||||
if (follow.timeStart > time.clock ) {
|
||||
// if it hasnt started, we count to start
|
||||
message = timerMessages.toStart;
|
||||
timer = millisToSeconds(follow.timeStart - time.clockMs);
|
||||
} else if (follow.timeStart <= time.clockMs) {
|
||||
timer = millisToSeconds(follow.timeStart - time.clock);
|
||||
} else if (follow.timeStart <= time.clock) {
|
||||
// if it has started, we show running timer
|
||||
message = timerMessages.waiting;
|
||||
timer = time.running;
|
||||
|
||||
@@ -6,10 +6,16 @@ import TitleSide from 'common/components/views/TitleSide';
|
||||
import { AnimatePresence, motion } from 'framer-motion';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import { formatTime } from '../../../common/utils/time';
|
||||
import { titleVariants } from '../common/animation';
|
||||
|
||||
import style from './Public.module.scss';
|
||||
|
||||
const formatOptions = {
|
||||
showSeconds: true,
|
||||
format: 'hh:mm:ss a',
|
||||
};
|
||||
|
||||
export default function Public(props) {
|
||||
const { publ, publicTitle, time, events, publicSelectedId, general } = props;
|
||||
const [pageNumber, setPageNumber] = useState(0);
|
||||
@@ -23,7 +29,7 @@ export default function Public(props) {
|
||||
// Format messages
|
||||
const showPubl = publ.text !== '' && publ.visible;
|
||||
|
||||
// motion
|
||||
const clock = formatTime(time.clock, formatOptions);
|
||||
|
||||
return (
|
||||
<div className={style.container__gray}>
|
||||
@@ -78,12 +84,12 @@ export default function Public(props) {
|
||||
<div className={style.label}>Today</div>
|
||||
<div className={style.nav}>
|
||||
{pageNumber > 1 &&
|
||||
[...Array(pageNumber).keys()].map((i) => (
|
||||
<div
|
||||
key={i}
|
||||
className={i === currentPage ? style.navItemSelected : style.navItem}
|
||||
/>
|
||||
))}
|
||||
[...Array(pageNumber).keys()].map((i) => (
|
||||
<div
|
||||
key={i}
|
||||
className={i === currentPage ? style.navItemSelected : style.navItem}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<Paginator
|
||||
@@ -95,18 +101,14 @@ export default function Public(props) {
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div
|
||||
className={
|
||||
showPubl ? style.publicContainer : style.publicContainerHidden
|
||||
}
|
||||
>
|
||||
<div className={showPubl ? style.publicContainer : style.publicContainerHidden}>
|
||||
<div className={style.label}>Public message</div>
|
||||
<div className={style.message}>{publ.text}</div>
|
||||
</div>
|
||||
|
||||
<div className={style.clockContainer}>
|
||||
<div className={style.label}>Time Now</div>
|
||||
<div className={style.clock}>{time.clock}</div>
|
||||
<div className={style.clock}>{clock}</div>
|
||||
</div>
|
||||
|
||||
<div className={style.infoContainer}>
|
||||
@@ -116,11 +118,7 @@ export default function Public(props) {
|
||||
</div>
|
||||
<div className={style.qr}>
|
||||
{general.url != null && general.url !== '' && (
|
||||
<QRCode
|
||||
value={general.url}
|
||||
size={window.innerWidth / 12}
|
||||
level='L'
|
||||
/>
|
||||
<QRCode value={general.url} size={window.innerWidth / 12} level='L' />
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -7,20 +7,27 @@ import { formatDisplay } from 'common/utils/dateConfig';
|
||||
import { AnimatePresence, motion } from 'framer-motion';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import { formatTime } from '../../../common/utils/time';
|
||||
|
||||
import style from './Pip.module.scss';
|
||||
|
||||
const formatOptions = {
|
||||
showSeconds: true,
|
||||
format: 'hh:mm:ss a',
|
||||
};
|
||||
|
||||
export default function Pip(props) {
|
||||
const { time, backstageEvents, selectedId, general } = props;
|
||||
const [size, setSize] = useState('');
|
||||
const ref = useRef(null);
|
||||
const pipAreaRef = useRef(null);
|
||||
const [filteredEvents, setFilteredEvents] = useState(null);
|
||||
const [pageNumber, setPageNumber] = useState(0);
|
||||
const [currentPage, setCurrentPage] = useState(0);
|
||||
|
||||
// calculcate pip size
|
||||
// calculate pip size
|
||||
useLayoutEffect(() => {
|
||||
const h = ref.current.clientHeight;
|
||||
const w = ref.current.clientWidth;
|
||||
const h = pipAreaRef.current.clientHeight;
|
||||
const w = pipAreaRef.current.clientWidth;
|
||||
setSize(`${w} x ${h}`);
|
||||
}, []);
|
||||
|
||||
@@ -51,11 +58,12 @@ export default function Pip(props) {
|
||||
}, [backstageEvents]);
|
||||
|
||||
// Format messages
|
||||
const showInfo =
|
||||
general.backstageInfo !== '' && general.backstageInfo != null;
|
||||
const showInfo = general.backstageInfo !== '' && general.backstageInfo != null;
|
||||
let stageTimer = formatDisplay(Math.abs(time.running), true);
|
||||
if (time.isNegative) stageTimer = `-${stageTimer}`;
|
||||
|
||||
const clock = formatTime(time.clock, formatOptions);
|
||||
|
||||
return (
|
||||
<div className={style.container__gray}>
|
||||
<NavLogo />
|
||||
@@ -67,12 +75,12 @@ export default function Pip(props) {
|
||||
<div className={style.label}>Today</div>
|
||||
<div className={style.nav}>
|
||||
{pageNumber > 1 &&
|
||||
[...Array(pageNumber).keys()].map((i) => (
|
||||
<div
|
||||
key={i}
|
||||
className={i === currentPage ? style.navItemSelected : style.navItem}
|
||||
/>
|
||||
))}
|
||||
[...Array(pageNumber).keys()].map((i) => (
|
||||
<div
|
||||
key={i}
|
||||
className={i === currentPage ? style.navItemSelected : style.navItem}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<Paginator
|
||||
@@ -86,7 +94,7 @@ export default function Pip(props) {
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className={style.pip} ref={ref}>
|
||||
<div className={style.pip} ref={pipAreaRef}>
|
||||
<Emptyimage className={style.empty} />
|
||||
<span className={style.piptext}>{size}</span>
|
||||
</div>
|
||||
@@ -100,11 +108,7 @@ export default function Pip(props) {
|
||||
</div>
|
||||
<div className={style.qr}>
|
||||
{general.url != null && general.url !== '' && (
|
||||
<QRCode
|
||||
value={general.url}
|
||||
size={window.innerWidth / 12}
|
||||
level='L'
|
||||
/>
|
||||
<QRCode value={general.url} size={window.innerWidth / 12} level='L' />
|
||||
)}
|
||||
</div>
|
||||
</motion.div>
|
||||
@@ -113,7 +117,7 @@ export default function Pip(props) {
|
||||
|
||||
<div className={style.clockContainer}>
|
||||
<div className={style.label}>Time Now</div>
|
||||
<div className={style.clock}>{time.clock}</div>
|
||||
<div className={style.clock}>{clock}</div>
|
||||
</div>
|
||||
|
||||
<div className={style.countdownContainer}>
|
||||
|
||||
@@ -9,13 +9,18 @@ import {
|
||||
getEventsWithDelay,
|
||||
trimEventlist,
|
||||
} from '../../../common/utils/eventsManager';
|
||||
import { formatTime, stringFromMillis } from '../../../common/utils/time';
|
||||
|
||||
import style from './StudioClock.module.scss';
|
||||
|
||||
const formatOptions = {
|
||||
showSeconds: false,
|
||||
format: 'hh:mm',
|
||||
};
|
||||
|
||||
export default function StudioClock(props) {
|
||||
const { title, time, backstageEvents, selectedId, nextId, onAir } = props;
|
||||
const { fontSize, ref } = useFitText({ maxFontSize: 500 });
|
||||
const [, , secondsNow] = time.clock.split(':');
|
||||
const { fontSize: titleFontSize, ref: titleRef } = useFitText({ maxFontSize: 500 });
|
||||
const [schedule, setSchedule] = useState([]);
|
||||
|
||||
const activeIndicators = [...Array(12).keys()];
|
||||
@@ -28,26 +33,32 @@ export default function StudioClock(props) {
|
||||
}, []);
|
||||
|
||||
// Prepare event list
|
||||
// Todo: useMemo()
|
||||
useEffect(() => {
|
||||
if (backstageEvents == null) return;
|
||||
|
||||
|
||||
const delayed = getEventsWithDelay(backstageEvents);
|
||||
const events = delayed.filter((e) => e.type === 'event');
|
||||
const trimmed = trimEventlist(events, selectedId, MAX_TITLES);
|
||||
const formatted = formatEventList(trimmed, selectedId, nextId);
|
||||
const formatted = formatEventList(trimmed, selectedId, nextId, {
|
||||
showEnd: false,
|
||||
});
|
||||
setSchedule(formatted);
|
||||
}, [backstageEvents, selectedId, nextId]);
|
||||
}, [backstageEvents, nextId, selectedId] );
|
||||
|
||||
const clock = formatTime(time.clock, formatOptions);
|
||||
|
||||
const [, , secondsNow] = stringFromMillis(time.clock).split(':');
|
||||
|
||||
return (
|
||||
<div className={style.container}>
|
||||
<NavLogo />
|
||||
<div className={style.clockContainer}>
|
||||
<div className={style.time}>{time.clockNoSeconds}</div>
|
||||
<div className={style.time}>{clock}</div>
|
||||
<div
|
||||
ref={ref}
|
||||
ref={titleRef}
|
||||
className={style.nextTitle}
|
||||
style={{ fontSize, height: '10vh', width: '100%', maxWidth: '82%' }}
|
||||
style={{ fontSize: titleFontSize, height: '10vh', width: '100%', maxWidth: '82%' }}
|
||||
>
|
||||
{title.titleNext}
|
||||
</div>
|
||||
|
||||
@@ -89,6 +89,13 @@ $cyan-idle: #0aa;
|
||||
line-height: 0.8em;
|
||||
}
|
||||
|
||||
.timeAA {
|
||||
color: $red-active;
|
||||
font-size: calc(#{$clock-size} / 4.5);
|
||||
margin-top: calc(50% - calc(#{$clock-size} / 7));
|
||||
line-height: 0.8em;
|
||||
}
|
||||
|
||||
.nextTitle:after,
|
||||
.nextCountdown:after,
|
||||
.nextCountdown__overtime:after {
|
||||
|
||||
@@ -7,8 +7,15 @@ import TitleCard from 'common/components/views/TitleCard';
|
||||
import { AnimatePresence, motion } from 'framer-motion';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import { formatTime } from '../../../common/utils/time';
|
||||
|
||||
import style from './Timer.module.scss';
|
||||
|
||||
const formatOptions = {
|
||||
showSeconds: true,
|
||||
format: 'hh:mm:ss a',
|
||||
};
|
||||
|
||||
export default function Timer(props) {
|
||||
const { general, pres, title, time } = props;
|
||||
const [elapsed, setElapsed] = useState(true);
|
||||
@@ -32,22 +39,12 @@ export default function Timer(props) {
|
||||
}
|
||||
}, [searchParams]);
|
||||
|
||||
const clock = formatTime(time.clock, formatOptions);
|
||||
|
||||
const showOverlay = pres.text !== '' && pres.visible;
|
||||
const isPlaying = time.playstate !== 'pause';
|
||||
const normalisedTime = Math.max(time.running, 0);
|
||||
|
||||
// show timer if end message is empty
|
||||
const endMessage =
|
||||
general.endMessage == null || general.endMessage === '' ? (
|
||||
<TimerDisplay
|
||||
time={time.running}
|
||||
isNegative={time.isNegative}
|
||||
hideZeroHours
|
||||
/>
|
||||
) : (
|
||||
general.endMessage
|
||||
);
|
||||
|
||||
// motion
|
||||
const titleVariants = {
|
||||
hidden: {
|
||||
@@ -65,16 +62,8 @@ export default function Timer(props) {
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
className={
|
||||
time.finished ? style.container__grayFinished : style.container__gray
|
||||
}
|
||||
>
|
||||
<div
|
||||
className={
|
||||
showOverlay ? style.messageOverlayActive : style.messageOverlay
|
||||
}
|
||||
>
|
||||
<div className={time.finished ? style.container__grayFinished : style.container__gray}>
|
||||
<div className={showOverlay ? style.messageOverlayActive : style.messageOverlay}>
|
||||
<div className={style.message}>{pres.text}</div>
|
||||
</div>
|
||||
|
||||
@@ -82,12 +71,18 @@ export default function Timer(props) {
|
||||
|
||||
<div className={style.clockContainer}>
|
||||
<div className={style.label}>Time Now</div>
|
||||
<div className={style.clock}>{time.clock}</div>
|
||||
<div className={style.clock}>{clock}</div>
|
||||
</div>
|
||||
|
||||
<div className={style.timerContainer}>
|
||||
{time.finished ? (
|
||||
<div className={style.finished}>{endMessage}</div>
|
||||
<div className={style.finished}>
|
||||
{general.endMessage == null || general.endMessage === '' ? (
|
||||
<TimerDisplay time={time.running} isNegative={time.isNegative} hideZeroHours />
|
||||
) : (
|
||||
general.endMessage
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
<div className={isPlaying ? style.countdown : style.countdownPaused}>
|
||||
<TimerDisplay time={normalisedTime} hideZeroHours />
|
||||
@@ -96,11 +91,7 @@ export default function Timer(props) {
|
||||
</div>
|
||||
|
||||
{!time.finished && (
|
||||
<div
|
||||
className={
|
||||
isPlaying ? style.progressContainer : style.progressContainerPaused
|
||||
}
|
||||
>
|
||||
<div className={isPlaying ? style.progressContainer : style.progressContainerPaused}>
|
||||
<MyProgressBar
|
||||
now={normalisedTime}
|
||||
complete={time.durationSeconds}
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
grid-template-columns: 1fr 1fr 5vw 1fr 1fr;
|
||||
grid-template-rows: auto 1fr auto minmax(25vh, auto);
|
||||
grid-template-areas:
|
||||
' clck .... .... .... ....'
|
||||
' clck clck .... .... ....'
|
||||
' timr timr timr timr timr'
|
||||
' prog prog prog prog prog'
|
||||
' now now .... next next';
|
||||
|
||||
@@ -95,7 +95,7 @@
|
||||
|
||||
.clock {
|
||||
font-family: 'Open Sans', sans-serif;
|
||||
font-size: 3vw;
|
||||
font-size: 2.25vw;
|
||||
line-height: 3vw;
|
||||
text-align: center;
|
||||
letter-spacing: 0.25vw;
|
||||
|
||||
@@ -6623,6 +6623,11 @@ jest@^27.4.3:
|
||||
import-local "^3.0.2"
|
||||
jest-cli "^27.5.1"
|
||||
|
||||
jotai@^1.7.8:
|
||||
version "1.7.8"
|
||||
resolved "https://registry.yarnpkg.com/jotai/-/jotai-1.7.8.tgz#1ac4daa2731e0f8e7fab8abb96aebf94b9dfc1a3"
|
||||
integrity sha512-rXwWz6uLqyZUCRzWIPiYTjI1hjskVxVpDN3lBoOHi66z6uQFkFmKwR8YhcQd+Ex4lODlReuKNIx6WwsF9aKy3g==
|
||||
|
||||
js-sha3@0.8.0:
|
||||
version "0.8.0"
|
||||
resolved "https://registry.yarnpkg.com/js-sha3/-/js-sha3-0.8.0.tgz#b9b7a5da73afad7dedd0f8c463954cbde6818840"
|
||||
@@ -6928,6 +6933,11 @@ lru-cache@^6.0.0:
|
||||
dependencies:
|
||||
yallist "^4.0.0"
|
||||
|
||||
luxon@^3.0.1:
|
||||
version "3.0.1"
|
||||
resolved "https://registry.yarnpkg.com/luxon/-/luxon-3.0.1.tgz#6901111d10ad06fd267ad4e4128a84bef8a77299"
|
||||
integrity sha512-hF3kv0e5gwHQZKz4wtm4c+inDtyc7elkanAsBq+fundaCdUBNJB1dHEGUZIM6SfSBUlbVFduPwEtNjFK8wLtcw==
|
||||
|
||||
lz-string@^1.4.4:
|
||||
version "1.4.4"
|
||||
resolved "https://registry.yarnpkg.com/lz-string/-/lz-string-1.4.4.tgz#c0d8eaf36059f705796e1e344811cf4c498d3a26"
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "ontime",
|
||||
"version": "1.5.0",
|
||||
"version": "1.6.0",
|
||||
"author": "Carlos Valente",
|
||||
"description": "Time keeping for live events",
|
||||
"repository": "https://github.com/cpvalente/ontime",
|
||||
|
||||
@@ -1049,7 +1049,6 @@ export class EventTimer extends Timer {
|
||||
this.runCycle();
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Deleted an event from the list by its id
|
||||
* @param {string} eventId
|
||||
|
||||
@@ -201,12 +201,14 @@ export const getSettings = async (req, res) => {
|
||||
const version = data.settings.version;
|
||||
const serverPort = data.settings.serverPort;
|
||||
const pinCode = data.settings.pinCode;
|
||||
const timeFormat = data.settings.timeFormat;
|
||||
|
||||
// send object with network information
|
||||
res.status(200).send({
|
||||
version,
|
||||
serverPort,
|
||||
pinCode,
|
||||
timeFormat,
|
||||
});
|
||||
};
|
||||
|
||||
@@ -226,9 +228,18 @@ export const postSettings = async (req, res) => {
|
||||
pin = req.body?.pinCode;
|
||||
}
|
||||
}
|
||||
|
||||
let timeFormat = data.settings.timeFormat;
|
||||
if (typeof req.body?.timeFormat === 'string') {
|
||||
if (req.body?.timeFormat === '12' || req.body?.timeFormat === '24') {
|
||||
timeFormat = req.body.timeFormat;
|
||||
}
|
||||
}
|
||||
|
||||
data.settings = {
|
||||
...data.settings,
|
||||
pinCode: pin,
|
||||
timeFormat: timeFormat,
|
||||
};
|
||||
await db.write();
|
||||
res.sendStatus(200);
|
||||
|
||||
@@ -13,6 +13,7 @@ export const dbModelv1 = {
|
||||
serverPort: 4001,
|
||||
lock: null,
|
||||
pinCode: null,
|
||||
timeFormat: '24',
|
||||
},
|
||||
aliases: [],
|
||||
userFields: {
|
||||
|
||||
@@ -51,4 +51,4 @@ describe('getPreviousPlayable()', () => {
|
||||
expect(id).toBe(null);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -184,6 +184,7 @@ describe('test json parser with valid def', () => {
|
||||
settings: {
|
||||
app: 'ontime',
|
||||
version: 1,
|
||||
timeFormat: '24',
|
||||
},
|
||||
};
|
||||
|
||||
@@ -386,6 +387,7 @@ describe('test corrupt data', () => {
|
||||
version: 1,
|
||||
serverPort: 4001,
|
||||
lock: null,
|
||||
timeFormat: '24',
|
||||
},
|
||||
};
|
||||
|
||||
@@ -408,6 +410,7 @@ describe('test corrupt data', () => {
|
||||
version: 1,
|
||||
serverPort: 4001,
|
||||
lock: null,
|
||||
timeFormat: '24',
|
||||
},
|
||||
};
|
||||
|
||||
@@ -424,6 +427,7 @@ describe('test corrupt data', () => {
|
||||
version: 1,
|
||||
serverPort: 4001,
|
||||
lock: null,
|
||||
timeFormat: '24',
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
@@ -98,6 +98,7 @@ export const parseSettings_v1 = (data, enforce) => {
|
||||
const settings = {
|
||||
lock: s.lock || null,
|
||||
pinCode: s.pinCode || null,
|
||||
timeFormat: s.timeFormat || '24',
|
||||
};
|
||||
|
||||
// write to db
|
||||
|
||||
Reference in New Issue
Block a user