mirror of
https://github.com/cpvalente/ontime.git
synced 2026-08-14 03:43:50 +00:00
V1 (#118)
* chore: upgrade minor versions * chore: upgrade cypress * chore: upgrade fe dependencies * update readme * update osx images * update readme * upgrade dependencies * upgrade test dependency * feat: add option to input autofill * feat: autofill to the left * chore: update docs * chore: cleanup ci * version bump * style: prevent zero height bar * fix: prevent loosing menu * ux: improve input, no spellcheck or autocomplete * small ux improvements in cuesheets * feat 111/increase maximum number of events * fix: optimistic delete issue with filter * refact: pincode workflow * chore: add versioning to packages
This commit is contained in:
@@ -1,11 +1,10 @@
|
||||
import React from 'react';
|
||||
import { Editable, EditableInput, EditablePreview } from '@chakra-ui/editable';
|
||||
import VisibleIconBtn from '../../../common/components/buttons/VisibleIconBtn';
|
||||
import { Tooltip } from '@chakra-ui/tooltip';
|
||||
import { IconButton } from '@chakra-ui/button';
|
||||
import { IoSunny } from '@react-icons/all-files/io5/IoSunny';
|
||||
import style from './MessageControl.module.scss';
|
||||
|
||||
const inputProps = {
|
||||
size: 'sm',
|
||||
};
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
export default function InputRow(props) {
|
||||
const { label, placeholder, text, visible, actionHandler, changeHandler } = props;
|
||||
@@ -24,12 +23,26 @@ export default function InputRow(props) {
|
||||
<EditablePreview className={style.padleft} />
|
||||
<EditableInput className={style.padleft} />
|
||||
</Editable>
|
||||
<VisibleIconBtn
|
||||
active={visible || undefined}
|
||||
actionHandler={actionHandler}
|
||||
{...inputProps}
|
||||
/>
|
||||
<Tooltip label={visible ? 'Make invisible' : 'Make visible'} openDelay={500}>
|
||||
<IconButton
|
||||
aria-label='Toggle visibility'
|
||||
size='sm'
|
||||
icon={<IoSunny size='18px' />}
|
||||
colorScheme='blue'
|
||||
variant={visible ? 'solid' : 'outline'}
|
||||
onClick={() => actionHandler('update', { field: 'isPublic', value: !visible })}
|
||||
/>
|
||||
</Tooltip>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
InputRow.propTypes = {
|
||||
label: PropTypes.string,
|
||||
placeholder: PropTypes.string,
|
||||
text: PropTypes.string,
|
||||
visible: PropTypes.bool,
|
||||
actionHandler: PropTypes.func.isRequired,
|
||||
changeHandler: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import React, { useCallback, useEffect, useState } from 'react';
|
||||
import { useSocket } from 'app/context/socketContext';
|
||||
import InputRow from './InputRow';
|
||||
import OnAirIconBtn from '../../../common/components/buttons/OnAirIconBtn';
|
||||
import { Tooltip } from '@chakra-ui/tooltip';
|
||||
import { IconButton } from '@chakra-ui/button';
|
||||
import { IoMicSharp } from '@react-icons/all-files/io5/IoMicSharp';
|
||||
import { IoMicOffOutline } from '@react-icons/all-files/io5/IoMicOffOutline';
|
||||
import style from './MessageControl.module.scss';
|
||||
|
||||
export default function MessageControl() {
|
||||
@@ -57,44 +60,47 @@ export default function MessageControl() {
|
||||
};
|
||||
}, [socket]);
|
||||
|
||||
const messageControl = async (action, payload) => {
|
||||
switch (action) {
|
||||
case 'pres-text':
|
||||
socket.emit('set-timer-text', payload);
|
||||
break;
|
||||
case 'toggle-pres-visible':
|
||||
socket.emit('set-timer-visible', !pres.visible);
|
||||
break;
|
||||
case 'publ-text':
|
||||
socket.emit('set-public-text', payload);
|
||||
break;
|
||||
case 'toggle-publ-visible':
|
||||
socket.emit('set-public-visible', !publ.visible);
|
||||
break;
|
||||
case 'lower-text':
|
||||
socket.emit('set-lower-text', payload);
|
||||
break;
|
||||
case 'toggle-lower-visible':
|
||||
socket.emit('set-lower-visible', !lower.visible);
|
||||
break;
|
||||
case 'toggle-onAir':
|
||||
socket.emit('set-onAir', !onAir);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
};
|
||||
const messageControl = useCallback(
|
||||
(action, payload) => {
|
||||
switch (action) {
|
||||
case 'pres-text':
|
||||
socket.emit('set-timer-text', payload);
|
||||
break;
|
||||
case 'toggle-pres-visible':
|
||||
socket.emit('set-timer-visible', payload);
|
||||
break;
|
||||
case 'publ-text':
|
||||
socket.emit('set-public-text', payload);
|
||||
break;
|
||||
case 'toggle-publ-visible':
|
||||
socket.emit('set-public-visible', payload);
|
||||
break;
|
||||
case 'lower-text':
|
||||
socket.emit('set-lower-text', payload);
|
||||
break;
|
||||
case 'toggle-lower-visible':
|
||||
socket.emit('set-lower-visible', payload);
|
||||
break;
|
||||
case 'toggle-onAir':
|
||||
socket.emit('set-onAir', payload);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
},
|
||||
[socket]
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className={style.messageContainer}>
|
||||
<InputRow
|
||||
label='Presenter screen message'
|
||||
label='Timer screen message'
|
||||
placeholder='only the presenter screens see this'
|
||||
text={pres.text}
|
||||
visible={pres.visible}
|
||||
changeHandler={(event) => messageControl('pres-text', event)}
|
||||
actionHandler={() => messageControl('toggle-pres-visible')}
|
||||
actionHandler={() => messageControl('toggle-pres-visible', !pres.visible)}
|
||||
/>
|
||||
<InputRow
|
||||
label='Public screen message'
|
||||
@@ -102,7 +108,7 @@ export default function MessageControl() {
|
||||
text={publ.text}
|
||||
visible={publ.visible}
|
||||
changeHandler={(event) => messageControl('publ-text', event)}
|
||||
actionHandler={() => messageControl('toggle-publ-visible')}
|
||||
actionHandler={() => messageControl('toggle-publ-visible', !publ.visible)}
|
||||
/>
|
||||
<InputRow
|
||||
label='Lower third message'
|
||||
@@ -110,20 +116,23 @@ export default function MessageControl() {
|
||||
text={lower.text}
|
||||
visible={lower.visible}
|
||||
changeHandler={(event) => messageControl('lower-text', event)}
|
||||
actionHandler={() => messageControl('toggle-lower-visible')}
|
||||
actionHandler={() => messageControl('toggle-lower-visible', !lower.visible)}
|
||||
/>
|
||||
</div>
|
||||
<div className={style.onAirToggle}>
|
||||
<OnAirIconBtn
|
||||
className={style.btn}
|
||||
active={onAir}
|
||||
size='md'
|
||||
actionHandler={() => messageControl('toggle-onAir')}
|
||||
/>
|
||||
<Tooltip label={onAir ? 'Go Off Air' : 'Go On Air'} openDelay={500}>
|
||||
<IconButton
|
||||
className={style.btn}
|
||||
size='md'
|
||||
icon={onAir ? <IoMicSharp size='24px' /> : <IoMicOffOutline size='24px' />}
|
||||
colorScheme='blue'
|
||||
variant={onAir ? 'solid' : 'outline'}
|
||||
onClick={() => messageControl('toggle-onAir', !onAir)}
|
||||
aria-label='Toggle On Air'
|
||||
/>
|
||||
</Tooltip>
|
||||
<span className={style.onAirLabel}>On Air</span>
|
||||
<span className={style.oscLabel}>
|
||||
{`/ontime/offAir << OSC >> /ontime/onAir`}
|
||||
</span>
|
||||
<span className={style.oscLabel}>{`/ontime/offAir << OSC >> /ontime/onAir`}</span>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import React, { useCallback, useEffect, useState } from 'react';
|
||||
import style from './PlaybackControl.module.scss';
|
||||
import { useSocket } from 'app/context/socketContext';
|
||||
import PlaybackButtons from './PlaybackButtons';
|
||||
@@ -63,37 +63,40 @@ export default function PlaybackControl() {
|
||||
};
|
||||
}, [socket]);
|
||||
|
||||
const playbackControl = async (action) => {
|
||||
switch (action) {
|
||||
case 'start':
|
||||
socket.emit('set-playstate', 'start');
|
||||
break;
|
||||
case 'pause':
|
||||
socket.emit('set-playstate', 'pause');
|
||||
break;
|
||||
case 'roll':
|
||||
socket.emit('set-playstate', 'roll');
|
||||
break;
|
||||
case 'previous':
|
||||
socket.emit('set-playstate', 'previous');
|
||||
resetTimer();
|
||||
break;
|
||||
case 'next':
|
||||
socket.emit('set-playstate', 'next');
|
||||
resetTimer();
|
||||
break;
|
||||
case 'unload':
|
||||
socket.emit('set-playstate', 'unload');
|
||||
resetTimer();
|
||||
break;
|
||||
case 'reload':
|
||||
socket.emit('set-playstate', 'reload');
|
||||
resetTimer();
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
};
|
||||
const playbackControl = useCallback(
|
||||
(action) => {
|
||||
switch (action) {
|
||||
case 'start':
|
||||
socket.emit('set-playstate', 'start');
|
||||
break;
|
||||
case 'pause':
|
||||
socket.emit('set-playstate', 'pause');
|
||||
break;
|
||||
case 'roll':
|
||||
socket.emit('set-playstate', 'roll');
|
||||
break;
|
||||
case 'previous':
|
||||
socket.emit('set-playstate', 'previous');
|
||||
resetTimer();
|
||||
break;
|
||||
case 'next':
|
||||
socket.emit('set-playstate', 'next');
|
||||
resetTimer();
|
||||
break;
|
||||
case 'unload':
|
||||
socket.emit('set-playstate', 'unload');
|
||||
resetTimer();
|
||||
break;
|
||||
case 'reload':
|
||||
socket.emit('set-playstate', 'reload');
|
||||
resetTimer();
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
},
|
||||
[socket]
|
||||
);
|
||||
|
||||
return (
|
||||
<div className={style.mainContainer}>
|
||||
|
||||
@@ -21,9 +21,8 @@ const areEqual = (prevProps, nextProps) => {
|
||||
const incrementProps = {
|
||||
size: 'sm',
|
||||
width: '2.9em',
|
||||
colorScheme: 'whiteAlpha',
|
||||
colorScheme: 'white',
|
||||
variant: 'outline',
|
||||
_focus: { boxShadow: 'none' },
|
||||
};
|
||||
|
||||
const PlaybackTimer = (props) => {
|
||||
@@ -68,23 +67,43 @@ const PlaybackTimer = (props) => {
|
||||
</>
|
||||
)}
|
||||
<div className={style.btn}>
|
||||
<Tooltip label='Remove 1 minute' delay={500} shouldWrapChildren={disableButtons}>
|
||||
<Button {...incrementProps} disabled={disableButtons} onClick={() => handleIncrement(-1)}>
|
||||
<Tooltip label='Remove 1 minute' openDelay={500} shouldWrapChildren={disableButtons}>
|
||||
<Button
|
||||
{...incrementProps}
|
||||
disabled={disableButtons}
|
||||
onClick={() => handleIncrement(-1)}
|
||||
_hover={!disableButtons && { bg: '#ebedf0', color: '#333' }}
|
||||
>
|
||||
-1
|
||||
</Button>
|
||||
</Tooltip>
|
||||
<Tooltip label='Add 1 minute' delay={500} shouldWrapChildren={disableButtons}>
|
||||
<Button {...incrementProps} disabled={disableButtons} onClick={() => handleIncrement(1)}>
|
||||
<Tooltip label='Add 1 minute' openDelay={500} shouldWrapChildren={disableButtons}>
|
||||
<Button
|
||||
{...incrementProps}
|
||||
disabled={disableButtons}
|
||||
onClick={() => handleIncrement(1)}
|
||||
_hover={!disableButtons && { bg: '#ebedf0', color: '#333' }}
|
||||
>
|
||||
+1
|
||||
</Button>
|
||||
</Tooltip>
|
||||
<Tooltip label='Remove 5 minutes' delay={500} shouldWrapChildren={disableButtons}>
|
||||
<Button {...incrementProps} disabled={disableButtons} onClick={() => handleIncrement(-5)}>
|
||||
<Tooltip label='Remove 5 minutes' openDelay={500} shouldWrapChildren={disableButtons}>
|
||||
<Button
|
||||
{...incrementProps}
|
||||
disabled={disableButtons}
|
||||
onClick={() => handleIncrement(-5)}
|
||||
_hover={!disableButtons && { bg: '#ebedf0', color: '#333' }}
|
||||
>
|
||||
-5
|
||||
</Button>
|
||||
</Tooltip>
|
||||
<Tooltip label='Add 5 minutes' delay={500} shouldWrapChildren={disableButtons}>
|
||||
<Button {...incrementProps} disabled={disableButtons} onClick={() => handleIncrement(5)}>
|
||||
<Tooltip label='Add 5 minutes' openDelay={500} shouldWrapChildren={disableButtons}>
|
||||
<Button
|
||||
{...incrementProps}
|
||||
disabled={disableButtons}
|
||||
onClick={() => handleIncrement(5)}
|
||||
_hover={!disableButtons && { bg: '#ebedf0', color: '#333' }}
|
||||
>
|
||||
+5
|
||||
</Button>
|
||||
</Tooltip>
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
import React from 'react';
|
||||
import PrevIconBtn from '../../../common/components/buttons/PrevIconBtn';
|
||||
import NextIconBtn from '../../../common/components/buttons/NextIconBtn';
|
||||
import ReloadIconButton from '../../../common/components/buttons/ReloadIconBtn';
|
||||
import { IoPlaySkipBack } from '@react-icons/all-files/io5/IoPlaySkipBack';
|
||||
import { IoPlaySkipForward } from '@react-icons/all-files/io5/IoPlaySkipForward';
|
||||
import { IoArrowUndo } from '@react-icons/all-files/io5/IoArrowUndo';
|
||||
import UnloadIconBtn from '../../../common/components/buttons/UnloadIconBtn';
|
||||
import PropTypes from 'prop-types';
|
||||
import style from './PlaybackControl.module.scss';
|
||||
import TransportIconBtn from '../../../common/components/buttons/TransportIconBtn';
|
||||
|
||||
export default function Transport(props) {
|
||||
const { playback, selectedId, playbackControl, noEvents } = props;
|
||||
@@ -12,25 +13,31 @@ export default function Transport(props) {
|
||||
|
||||
return (
|
||||
<div className={style.playbackContainer}>
|
||||
<PrevIconBtn
|
||||
clickhandler={() => playbackControl('previous')}
|
||||
<TransportIconBtn
|
||||
clickHandler={() => playbackControl('previous')}
|
||||
disabled={isRolling || noEvents}
|
||||
tooltip='Previous event'
|
||||
icon={<IoPlaySkipBack size='22px' />}
|
||||
/>
|
||||
<NextIconBtn
|
||||
clickhandler={() => playbackControl('next')}
|
||||
<TransportIconBtn
|
||||
clickHandler={() => playbackControl('next')}
|
||||
disabled={isRolling || noEvents}
|
||||
tooltip='Next event'
|
||||
icon={<IoPlaySkipForward size='22px' />}
|
||||
/>
|
||||
<ReloadIconButton
|
||||
clickhandler={() => playbackControl('reload')}
|
||||
<TransportIconBtn
|
||||
clickHandler={() => playbackControl('reload')}
|
||||
disabled={selectedId == null || isRolling || noEvents}
|
||||
tooltip='Reload event'
|
||||
icon={<IoArrowUndo size='22px' />}
|
||||
/>
|
||||
<UnloadIconBtn
|
||||
clickhandler={() => playbackControl('unload')}
|
||||
clickHandler={() => playbackControl('unload')}
|
||||
disabled={(selectedId == null && !isRolling) || noEvents}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
Transport.propTypes = {
|
||||
playback: PropTypes.string,
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
import React from 'react';
|
||||
import { HStack } from '@chakra-ui/react';
|
||||
import { Draggable } from 'react-beautiful-dnd';
|
||||
import { IoRemove } from '@react-icons/all-files/io5/IoRemove';
|
||||
import { FiMoreVertical } from '@react-icons/all-files/fi/FiMoreVertical';
|
||||
import DeleteIconBtn from 'common/components/buttons/DeleteIconBtn';
|
||||
import ActionButtons from '../list/ActionButtons';
|
||||
import style from './BlockBlock.module.scss';
|
||||
import ActionButtons from '../../../common/components/buttons/ActionButtons';
|
||||
import TooltipLoadingActionBtn from '../../../common/components/buttons/TooltipLoadingActionBtn';
|
||||
import PropTypes from 'prop-types';
|
||||
import style from './BlockBlock.module.scss';
|
||||
|
||||
export default function BlockBlock(props) {
|
||||
const { index, data, actionHandler } = props;
|
||||
@@ -22,7 +23,13 @@ export default function BlockBlock(props) {
|
||||
<FiMoreVertical />
|
||||
</span>
|
||||
<HStack spacing='0.5em' className={style.actionOverlay}>
|
||||
<DeleteIconBtn actionHandler={actionHandler} />
|
||||
<TooltipLoadingActionBtn
|
||||
clickHandler={() => actionHandler('delete')}
|
||||
icon={<IoRemove />}
|
||||
colorScheme='red'
|
||||
tooltip='Delete'
|
||||
_hover={{ bg: 'red.400' }}
|
||||
/>
|
||||
<ActionButtons showAdd showDelay actionHandler={actionHandler} />
|
||||
</HStack>
|
||||
</div>
|
||||
|
||||
@@ -1,21 +1,23 @@
|
||||
import React from 'react';
|
||||
import React, { useCallback } from 'react';
|
||||
import { HStack } from '@chakra-ui/react';
|
||||
import { Draggable } from 'react-beautiful-dnd';
|
||||
import { FiMoreVertical } from '@react-icons/all-files/fi/FiMoreVertical';
|
||||
import { millisToMinutes } from 'common/utils/dateConfig';
|
||||
import ActionButtons from '../list/ActionButtons';
|
||||
import DeleteIconBtn from 'common/components/buttons/DeleteIconBtn';
|
||||
import ApplyIconBtn from 'common/components/buttons/ApplyIconBtn';
|
||||
import { IoRemove } from '@react-icons/all-files/io5/IoRemove';
|
||||
import { FiCheck } from '@react-icons/all-files/fi/FiCheck';
|
||||
import { FiMoreVertical } from '@react-icons/all-files/fi/FiMoreVertical';
|
||||
import ActionButtons from '../../../common/components/buttons/ActionButtons';
|
||||
import DelayInput from 'common/input/DelayInput';
|
||||
import TooltipActionBtn from '../../../common/components/buttons/TooltipActionBtn';
|
||||
import TooltipLoadingActionBtn from '../../../common/components/buttons/TooltipLoadingActionBtn';
|
||||
import style from './DelayBlock.module.scss';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
export default function DelayBlock(props) {
|
||||
const { eventsHandler, data, index, actionHandler } = props;
|
||||
|
||||
const applyDelayHandler = () => {
|
||||
const applyDelayHandler = useCallback(() => {
|
||||
eventsHandler('applyDelay', { id: data.id, duration: data.duration });
|
||||
};
|
||||
}, [data.duration, data.id, eventsHandler]);
|
||||
|
||||
const delayValue = data.duration != null ? millisToMinutes(data.duration) : undefined;
|
||||
return (
|
||||
@@ -27,8 +29,20 @@ export default function DelayBlock(props) {
|
||||
</span>
|
||||
<DelayInput className={style.input} value={delayValue} actionHandler={actionHandler} />
|
||||
<HStack spacing='0.5em' className={style.actionOverlay}>
|
||||
<ApplyIconBtn clickhandler={applyDelayHandler} />
|
||||
<DeleteIconBtn actionHandler={actionHandler} />
|
||||
<TooltipActionBtn
|
||||
clickHandler={applyDelayHandler}
|
||||
icon={<FiCheck />}
|
||||
colorScheme='orange'
|
||||
tooltip='Apply delays'
|
||||
_hover={{ bg: 'orange.400' }}
|
||||
/>
|
||||
<TooltipLoadingActionBtn
|
||||
clickHandler={() => actionHandler('delete')}
|
||||
icon={<IoRemove />}
|
||||
colorScheme='red'
|
||||
tooltip='Delete'
|
||||
_hover={{ bg: 'red.400' }}
|
||||
/>
|
||||
<ActionButtons showAdd actionHandler={actionHandler} />
|
||||
</HStack>
|
||||
</div>
|
||||
|
||||
@@ -41,11 +41,9 @@ export default function Editor() {
|
||||
|
||||
<Box className={styles.editor}>
|
||||
<h1>Event List</h1>
|
||||
<div className={styles.content}>
|
||||
<ErrorBoundary>
|
||||
<EventListWrapper />
|
||||
</ErrorBoundary>
|
||||
</div>
|
||||
<ErrorBoundary>
|
||||
<EventListWrapper />
|
||||
</ErrorBoundary>
|
||||
</Box>
|
||||
</CollapseProvider>
|
||||
</CursorProvider>
|
||||
|
||||
@@ -1,11 +1,19 @@
|
||||
import React, { useContext, useEffect, useState } from 'react';
|
||||
import { Tooltip } from '@chakra-ui/tooltip';
|
||||
import { Checkbox } from '@chakra-ui/react';
|
||||
import style from './EntryBlock.module.scss';
|
||||
import { LocalEventSettingsContext } from '../../../app/context/LocalEventSettingsContext';
|
||||
import PropTypes from 'prop-types';
|
||||
import style from './EntryBlock.module.scss';
|
||||
|
||||
export default function EntryBlock(props) {
|
||||
const { showKbd, index, eventsHandler } = props;
|
||||
const {
|
||||
showKbd,
|
||||
previousId,
|
||||
eventsHandler,
|
||||
visible,
|
||||
disableAddDelay = true,
|
||||
disableAddBlock,
|
||||
} = props;
|
||||
const { starTimeIsLastEnd, defaultPublic } = useContext(LocalEventSettingsContext);
|
||||
const [doStartTime, setStartTime] = useState(starTimeIsLastEnd);
|
||||
const [doPublic, setPublic] = useState(defaultPublic);
|
||||
@@ -19,33 +27,36 @@ export default function EntryBlock(props) {
|
||||
}, [defaultPublic]);
|
||||
|
||||
return (
|
||||
<div className={style.create}>
|
||||
<div className={`${style.create} ${visible ? style.visible : ''}`}>
|
||||
<Tooltip label='Add Event' openDelay={300}>
|
||||
<span
|
||||
className={style.createEvent}
|
||||
onClick={() =>
|
||||
eventsHandler(
|
||||
'add',
|
||||
{ type: 'event', order: index + 1, isPublic: doPublic },
|
||||
{ startIsLastEnd: doStartTime ? index : undefined }
|
||||
{ type: 'event', after: previousId, isPublic: doPublic },
|
||||
{ startIsLastEnd: doStartTime ? previousId : undefined }
|
||||
)
|
||||
}
|
||||
role='button'
|
||||
>
|
||||
E{showKbd && <span className={style.keyboard}>Alt + E</span>}
|
||||
</span>
|
||||
</Tooltip>
|
||||
<Tooltip label='Add Delay' openDelay={300}>
|
||||
<span
|
||||
className={style.createDelay}
|
||||
onClick={() => eventsHandler('add', { type: 'delay', order: index + 1 })}
|
||||
className={`${style.createDelay} ${disableAddDelay ? style.disabled : ''}`}
|
||||
onClick={() => eventsHandler('add', { type: 'delay', after: previousId })}
|
||||
role='button'
|
||||
>
|
||||
D{showKbd && <span className={style.keyboard}>Alt + D</span>}
|
||||
</span>
|
||||
</Tooltip>
|
||||
<Tooltip label='Add Block' openDelay={300}>
|
||||
<span
|
||||
className={style.createBlock}
|
||||
onClick={() => eventsHandler('add', { type: 'block', order: index + 1 })}
|
||||
className={`${style.createBlock} ${disableAddBlock ? style.disabled : ''}`}
|
||||
onClick={() => eventsHandler('add', { type: 'block', after: previousId })}
|
||||
role='button'
|
||||
>
|
||||
B{showKbd && <span className={style.keyboard}>Alt + B</span>}
|
||||
</span>
|
||||
@@ -65,9 +76,19 @@ export default function EntryBlock(props) {
|
||||
isChecked={doPublic}
|
||||
onChange={(e) => setPublic(e.target.checked)}
|
||||
>
|
||||
Default public
|
||||
Event is public
|
||||
</Checkbox>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
EntryBlock.propTypes = {
|
||||
showKbd: PropTypes.bool,
|
||||
eventsHandler: PropTypes.func,
|
||||
visible: PropTypes.bool,
|
||||
previousId: PropTypes.string,
|
||||
disableAddDelay: PropTypes.bool,
|
||||
disableAddBlock: PropTypes.bool,
|
||||
};
|
||||
|
||||
|
||||
@@ -1,5 +1,20 @@
|
||||
@use '../../../styles/main' as *;
|
||||
|
||||
@mixin when-visible() {
|
||||
opacity: 1;
|
||||
height: calc(2.5em + 20px);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 5%;
|
||||
transition: height 0.15s ease;
|
||||
margin: 2px 0;
|
||||
|
||||
* {
|
||||
display: flex;
|
||||
}
|
||||
}
|
||||
|
||||
.create {
|
||||
padding: 0 0.5em;
|
||||
box-sizing: border-box;
|
||||
@@ -18,17 +33,7 @@
|
||||
}
|
||||
|
||||
&:hover {
|
||||
opacity: 1;
|
||||
height: calc(2.5em + 20px);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 5%;
|
||||
transition: height 0.15s ease;
|
||||
|
||||
* {
|
||||
display: flex;
|
||||
}
|
||||
@include when-visible;
|
||||
}
|
||||
|
||||
.createEvent,
|
||||
@@ -43,6 +48,7 @@
|
||||
line-height: 21px;
|
||||
border-radius: 4px;
|
||||
opacity: 0.6;
|
||||
cursor: pointer;
|
||||
|
||||
.keyboard {
|
||||
margin-left: 4px;
|
||||
@@ -85,8 +91,17 @@
|
||||
}
|
||||
|
||||
.options {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
opacity: 0.65;
|
||||
}
|
||||
|
||||
.disabled {
|
||||
border-color: $text-gray-disabled;
|
||||
color: $text-gray-disabled;
|
||||
pointer-events: none;
|
||||
}
|
||||
}
|
||||
|
||||
.create.visible {
|
||||
@include when-visible;
|
||||
}
|
||||
|
||||
@@ -1,14 +1,25 @@
|
||||
import React from 'react';
|
||||
import React, { memo } from 'react';
|
||||
import { HStack } from '@chakra-ui/react';
|
||||
import { FiMoreVertical } from '@react-icons/all-files/fi/FiMoreVertical';
|
||||
import ActionButtons from '../../../common/components/buttons/ActionButtons';
|
||||
import EventTimes from '../../../common/components/eventTimes/EventTimes';
|
||||
import EditableText from '../../../common/input/EditableText';
|
||||
import PublicIconBtn from '../../../common/components/buttons/PublicIconBtn';
|
||||
import ActionButtons from '../list/ActionButtons';
|
||||
import PropTypes from 'prop-types';
|
||||
import style from './EventBlock.module.scss';
|
||||
|
||||
export default function CollapsedBlock (props) {
|
||||
const areEqual = (prevProps, nextProps) => {
|
||||
return (
|
||||
prevProps.provided === nextProps.provided &&
|
||||
prevProps.data.revision === nextProps.data.revision &&
|
||||
prevProps.next === nextProps.next &&
|
||||
prevProps.delay === nextProps.delay &&
|
||||
prevProps.delayValue === nextProps.delayValue &&
|
||||
prevProps.previousEnd === nextProps.previousEnd
|
||||
);
|
||||
};
|
||||
|
||||
function CollapsedBlock(props) {
|
||||
const { provided, data, next, delay, delayValue, previousEnd, actionHandler } = props;
|
||||
|
||||
return (
|
||||
@@ -43,7 +54,7 @@ export default function CollapsedBlock (props) {
|
||||
</HStack>
|
||||
</>
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
CollapsedBlock.propTypes = {
|
||||
provided: PropTypes.any.isRequired,
|
||||
@@ -54,3 +65,5 @@ CollapsedBlock.propTypes = {
|
||||
previousEnd: PropTypes.number.isRequired,
|
||||
actionHandler: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
export default memo(CollapsedBlock, areEqual);
|
||||
|
||||
@@ -1,15 +1,28 @@
|
||||
import React from 'react';
|
||||
import React, { memo } from 'react';
|
||||
import { VStack } from '@chakra-ui/react';
|
||||
import { IoRemove } from '@react-icons/all-files/io5/IoRemove';
|
||||
import { FiMoreVertical } from '@react-icons/all-files/fi/FiMoreVertical';
|
||||
import ActionButtons from '../../../common/components/buttons/ActionButtons';
|
||||
import EventTimesVertical from '../../../common/components/eventTimes/EventTimesVertical';
|
||||
import EditableText from '../../../common/input/EditableText';
|
||||
import PublicIconBtn from '../../../common/components/buttons/PublicIconBtn';
|
||||
import ActionButtons from '../list/ActionButtons';
|
||||
import DeleteIconBtn from '../../../common/components/buttons/DeleteIconBtn';
|
||||
import TooltipLoadingActionBtn from '../../../common/components/buttons/TooltipLoadingActionBtn';
|
||||
import PropTypes from 'prop-types';
|
||||
import style from './EventBlock.module.scss';
|
||||
|
||||
export default function ExpandedBlock(props) {
|
||||
const areEqual = (prevProps, nextProps) => {
|
||||
return (
|
||||
prevProps.provided === nextProps.provided &&
|
||||
prevProps.data.revision === nextProps.data.revision &&
|
||||
prevProps.next === nextProps.next &&
|
||||
prevProps.delay === nextProps.delay &&
|
||||
prevProps.delayValue === nextProps.delayValue &&
|
||||
prevProps.previousEnd === nextProps.previousEnd
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
function ExpandedBlock(props) {
|
||||
const { provided, data, eventIndex, next, delay, delayValue, previousEnd, actionHandler } = props;
|
||||
|
||||
const oscid = data?.id || '...';
|
||||
@@ -70,11 +83,17 @@ export default function ExpandedBlock(props) {
|
||||
<VStack spacing='0.5em' className={style.actionOverlay}>
|
||||
<PublicIconBtn actionHandler={actionHandler} active={data.isPublic} />
|
||||
<ActionButtons showAdd showDelay showBlock actionHandler={actionHandler} />
|
||||
<DeleteIconBtn actionHandler={actionHandler} />
|
||||
<TooltipLoadingActionBtn
|
||||
clickHandler={() => actionHandler('delete')}
|
||||
icon={<IoRemove />}
|
||||
colorScheme='red'
|
||||
tooltip='Delete'
|
||||
_hover={{ bg: 'red.400' }}
|
||||
/>
|
||||
</VStack>
|
||||
</>
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
ExpandedBlock.propTypes = {
|
||||
provided: PropTypes.any.isRequired,
|
||||
@@ -86,3 +105,5 @@ ExpandedBlock.propTypes = {
|
||||
previousEnd: PropTypes.number,
|
||||
actionHandler: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
export default memo(ExpandedBlock, areEqual);
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
import React from 'react';
|
||||
import ProtectRoute from '../../common/components/protectRoute/ProtectRoute';
|
||||
import Editor from './Editor';
|
||||
|
||||
export default function ProtectedEditor() {
|
||||
return (
|
||||
<ProtectRoute>
|
||||
<Editor />
|
||||
</ProtectRoute>
|
||||
);
|
||||
}
|
||||
@@ -1,49 +0,0 @@
|
||||
import React from 'react';
|
||||
import { Menu, MenuButton, MenuItem, MenuList } from '@chakra-ui/menu';
|
||||
import { IconButton } from '@chakra-ui/button';
|
||||
import { FiClock } from '@react-icons/all-files/fi/FiClock';
|
||||
import { FiMinusCircle } from '@react-icons/all-files/fi/FiMinusCircle';
|
||||
import { FiPlus } from '@react-icons/all-files/fi/FiPlus';
|
||||
import { Tooltip } from '@chakra-ui/tooltip';
|
||||
|
||||
export default function ActionButtons(props) {
|
||||
const { showAdd, showDelay, showBlock, actionHandler } = props;
|
||||
|
||||
const menuStyle = {
|
||||
color: '#000000',
|
||||
backgroundColor: 'rgba(255,255,255,1)',
|
||||
};
|
||||
|
||||
return (
|
||||
<Menu isLazy lazyBehavior='unmount'>
|
||||
<Tooltip label='Add ...' delay={500}>
|
||||
<MenuButton
|
||||
as={IconButton}
|
||||
aria-label='Options'
|
||||
size='xs'
|
||||
icon={<FiPlus />}
|
||||
_expanded={{ bg: 'orange.300', color: 'white' }}
|
||||
_focus={{ boxShadow: 'none' }}
|
||||
backgroundColor='orange.200'
|
||||
color='orange.500'
|
||||
/>
|
||||
</Tooltip>
|
||||
<MenuList style={menuStyle}>
|
||||
<MenuItem icon={<FiPlus />} onClick={() => actionHandler('event')} isDisabled={!showAdd}>
|
||||
Add Event after
|
||||
</MenuItem>
|
||||
|
||||
<MenuItem icon={<FiClock />} onClick={() => actionHandler('delay')} isDisabled={!showDelay}>
|
||||
Add Delay after
|
||||
</MenuItem>
|
||||
<MenuItem
|
||||
icon={<FiMinusCircle />}
|
||||
onClick={() => actionHandler('block')}
|
||||
isDisabled={!showBlock}
|
||||
>
|
||||
Add Block after
|
||||
</MenuItem>
|
||||
</MenuList>
|
||||
</Menu>
|
||||
);
|
||||
}
|
||||
@@ -18,6 +18,20 @@ export default function EventList(props) {
|
||||
const cursorRef = createRef();
|
||||
const { showQuickEntry } = useContext(LocalEventSettingsContext);
|
||||
|
||||
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])
|
||||
|
||||
// Handle keyboard shortcuts
|
||||
const handleKeyPress = useCallback(
|
||||
(e) => {
|
||||
@@ -37,23 +51,23 @@ export default function EventList(props) {
|
||||
if (e.key === 'e' || e.key === 'E') {
|
||||
e.preventDefault();
|
||||
if (cursor == null) return;
|
||||
eventsHandler('add', { type: 'event', order: cursor + 1 });
|
||||
insertAtCursor('event', cursor)
|
||||
}
|
||||
// D
|
||||
if (e.key === 'd' || e.key === 'D') {
|
||||
e.preventDefault();
|
||||
if (cursor == null) return;
|
||||
eventsHandler('add', { type: 'delay', order: cursor + 1 });
|
||||
insertAtCursor('delay', cursor)
|
||||
}
|
||||
// B
|
||||
if (e.key === 'b' || e.key === 'B') {
|
||||
e.preventDefault();
|
||||
if (cursor == null) return;
|
||||
eventsHandler('add', { type: 'block', order: cursor + 1 });
|
||||
insertAtCursor('block', cursor)
|
||||
}
|
||||
}
|
||||
},
|
||||
[cursor, events.length, eventsHandler, moveCursorDown, moveCursorUp]
|
||||
[cursor, events.length, insertAtCursor, moveCursorDown, moveCursorUp]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -61,6 +75,7 @@ export default function EventList(props) {
|
||||
document.addEventListener('keydown', handleKeyPress);
|
||||
|
||||
if (cursor > events.length - 1) setCursor(events.length - 1);
|
||||
if (events.length > 0 && cursor === -1) setCursor(0);
|
||||
|
||||
// remove the event listener
|
||||
return () => {
|
||||
@@ -126,26 +141,28 @@ export default function EventList(props) {
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [selectedId, isCursorLocked]);
|
||||
|
||||
if (events.length < 1) {
|
||||
return <Empty text='No Events' style={{marginTop: "10vh"}} />;
|
||||
}
|
||||
|
||||
// DND
|
||||
const handleOnDragEnd = (result) => {
|
||||
// drop outside of area
|
||||
if (!result.destination) return;
|
||||
const handleOnDragEnd = useCallback(
|
||||
(result) => {
|
||||
// drop outside of area
|
||||
if (!result.destination) return;
|
||||
|
||||
// no change
|
||||
if (result.destination === result.source.index) return;
|
||||
// no change
|
||||
if (result.destination === result.source.index) return;
|
||||
|
||||
// Call API
|
||||
eventsHandler('reorder', {
|
||||
index: result.draggableId,
|
||||
from: result.source.index,
|
||||
to: result.destination.index,
|
||||
});
|
||||
};
|
||||
// Call API
|
||||
eventsHandler('reorder', {
|
||||
index: result.draggableId,
|
||||
from: result.source.index,
|
||||
to: result.destination.index,
|
||||
});
|
||||
},
|
||||
[eventsHandler]
|
||||
);
|
||||
|
||||
if (events.length < 1) {
|
||||
return <Empty text='No Events' style={{ marginTop: '10vh' }} />;
|
||||
}
|
||||
let cumulativeDelay = 0;
|
||||
let eventIndex = -1;
|
||||
let previousEnd = 0;
|
||||
@@ -171,10 +188,11 @@ export default function EventList(props) {
|
||||
previousEnd = thisEnd;
|
||||
thisEnd = e.timeEnd;
|
||||
}
|
||||
const isLast = index === events.length - 1;
|
||||
return (
|
||||
<div key={e.id}>
|
||||
{index === 0 && showQuickEntry && (
|
||||
<EntryBlock index={-1} eventsHandler={eventsHandler} />
|
||||
<EntryBlock index={e.id} eventsHandler={eventsHandler} />
|
||||
)}
|
||||
<div
|
||||
ref={cursor === index ? cursorRef : undefined}
|
||||
@@ -192,11 +210,14 @@ export default function EventList(props) {
|
||||
previousEnd={previousEnd}
|
||||
/>
|
||||
</div>
|
||||
{showQuickEntry && (
|
||||
{(showQuickEntry || isLast) && (
|
||||
<EntryBlock
|
||||
showKbd={index === cursor}
|
||||
index={index}
|
||||
previousId={e.id}
|
||||
eventsHandler={eventsHandler}
|
||||
visible={isLast}
|
||||
disableAddDelay={e.type === 'delay'}
|
||||
disableAddBlock={e.type === 'block'}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -4,6 +4,7 @@ import BlockBlock from '../BlockBlock/BlockBlock';
|
||||
import EventBlock from '../EventBlock/EventBlock';
|
||||
import { LoggingContext } from '../../../app/context/LoggingContext';
|
||||
import { LocalEventSettingsContext } from '../../../app/context/LocalEventSettingsContext';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
const areEqual = (prevProps, nextProps) => {
|
||||
return (
|
||||
@@ -51,17 +52,17 @@ const EventListItem = (props) => {
|
||||
'add',
|
||||
{
|
||||
type: 'event',
|
||||
order: index + 1,
|
||||
after: data.id,
|
||||
isPublic: defaultPublic,
|
||||
},
|
||||
{ startIsLastEnd: starTimeIsLastEnd ? index : undefined }
|
||||
{ startIsLastEnd: starTimeIsLastEnd ? data.id : undefined }
|
||||
);
|
||||
break;
|
||||
case 'delay':
|
||||
eventsHandler('add', { type: 'delay', order: index + 1 });
|
||||
eventsHandler('add', { type: 'delay', after: data.id });
|
||||
break;
|
||||
case 'block':
|
||||
eventsHandler('add', { type: 'block', order: index + 1 });
|
||||
eventsHandler('add', { type: 'block', after: data.id });
|
||||
break;
|
||||
case 'delete':
|
||||
eventsHandler('delete', data.id);
|
||||
@@ -101,7 +102,7 @@ const EventListItem = (props) => {
|
||||
break;
|
||||
}
|
||||
},
|
||||
[calculateDuration, data, defaultPublic, emitError, eventsHandler, index, starTimeIsLastEnd]
|
||||
[calculateDuration, data, defaultPublic, emitError, eventsHandler, starTimeIsLastEnd]
|
||||
);
|
||||
|
||||
switch (type) {
|
||||
@@ -135,3 +136,15 @@ const EventListItem = (props) => {
|
||||
};
|
||||
|
||||
export default memo(EventListItem, areEqual);
|
||||
|
||||
EventListItem.propTypes = {
|
||||
type: PropTypes.oneOf(['event', 'delay', 'block']),
|
||||
index: PropTypes.number,
|
||||
eventIndex: PropTypes.number,
|
||||
data: PropTypes.object,
|
||||
selected: PropTypes.bool,
|
||||
next: PropTypes.bool,
|
||||
eventsHandler: PropTypes.func,
|
||||
delay: PropTypes.number,
|
||||
previousEnd: PropTypes.number
|
||||
}
|
||||
@@ -17,6 +17,7 @@ import EventList from './EventList';
|
||||
import EventListMenu from 'features/menu/EventListMenu.jsx';
|
||||
import Empty from 'common/state/Empty';
|
||||
import { CollapseContext } from '../../../app/context/CollapseContext';
|
||||
import styles from '../Editor.module.scss';
|
||||
|
||||
export default function EventListWrapper() {
|
||||
const { expandAll, collapseMultiple } = useContext(CollapseContext);
|
||||
@@ -40,7 +41,17 @@ export default function EventListWrapper() {
|
||||
|
||||
// optimistically update object, temp ID until refetch
|
||||
const optimistic = [...previousEvents];
|
||||
optimistic.splice(newEvent.order, 0, {
|
||||
let insertAfterIndex = 0;
|
||||
if (newEvent.after) {
|
||||
const index = optimistic.findIndex((event) => event.id === newEvent?.after);
|
||||
if (index > -1) {
|
||||
insertAfterIndex = index + 1;
|
||||
}
|
||||
} else if (newEvent.order) {
|
||||
insertAfterIndex = newEvent.order;
|
||||
}
|
||||
|
||||
optimistic.splice(insertAfterIndex, 0, {
|
||||
...newEvent,
|
||||
id: new Date().toISOString(),
|
||||
});
|
||||
@@ -50,7 +61,7 @@ export default function EventListWrapper() {
|
||||
return { previousEvents };
|
||||
},
|
||||
|
||||
// Mutation fails, rollback undos optimist update
|
||||
// Mutation fails, rollback undoes optimist update
|
||||
onError: (error, newEvent, context) => {
|
||||
queryClient.setQueryData(EVENTS_TABLE, context.previousEvents);
|
||||
},
|
||||
@@ -128,7 +139,7 @@ export default function EventListWrapper() {
|
||||
// Snapshot the previous value
|
||||
const previousEvents = queryClient.getQueryData(EVENTS_TABLE);
|
||||
|
||||
const filtered = [...previousEvents].filter((e) => e.id === 'eventId')
|
||||
const filtered = [...previousEvents].filter((e) => e.id !== eventId);
|
||||
|
||||
// optimistically update object
|
||||
queryClient.setQueryData(EVENTS_TABLE, filtered);
|
||||
@@ -231,12 +242,16 @@ export default function EventListWrapper() {
|
||||
const newEvent = { ...payload };
|
||||
// there is an option to pass an index of an array to use as start time
|
||||
if (typeof options?.startIsLastEnd !== 'undefined') {
|
||||
newEvent.timeStart = data[options.startIsLastEnd].timeEnd || 0;
|
||||
const previousEvent = data.find((event) => event.id === options.startIsLastEnd);
|
||||
newEvent.timeStart = previousEvent.timeEnd || 0;
|
||||
}
|
||||
// hard coding duration value to be as expected for now
|
||||
// this until timeOptions gets implemented
|
||||
// Todo: implement duration options
|
||||
newEvent.duration = Math.max(0, newEvent.timeEnd - newEvent.timeStart) || 0;
|
||||
if (newEvent.type === 'event') {
|
||||
newEvent.duration = Math.max(0, newEvent.timeEnd - newEvent.timeStart) || 0;
|
||||
}
|
||||
|
||||
await addEvent.mutateAsync(newEvent);
|
||||
} catch (error) {
|
||||
emitError(`Error fetching data: ${error.message}`);
|
||||
@@ -336,11 +351,13 @@ export default function EventListWrapper() {
|
||||
return (
|
||||
<>
|
||||
<EventListMenu eventsHandler={eventsHandler} />
|
||||
{status === 'success' && events != null ? (
|
||||
<EventList events={events} eventsHandler={eventsHandler} />
|
||||
) : (
|
||||
<Empty text='Connecting to server' />
|
||||
)}
|
||||
<div className={styles.content}>
|
||||
{status === 'success' && events != null ? (
|
||||
<EventList events={events} eventsHandler={eventsHandler} />
|
||||
) : (
|
||||
<Empty text='Connecting to server' />
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, { useContext, useEffect, useState } from 'react';
|
||||
import React, { useCallback, useContext, useEffect, useState } from 'react';
|
||||
import { HStack } from '@chakra-ui/react';
|
||||
import CollapseBar from '../../common/components/collapseBar/CollapseBar';
|
||||
import { LoggingContext } from '../../app/context/LoggingContext';
|
||||
@@ -46,14 +46,14 @@ export default function InfoLogger() {
|
||||
setData(d);
|
||||
}, [logData, showUser, showClient, showServer, showPlayback, showRx, showTx]);
|
||||
|
||||
const disableOthers = (toEnable) => {
|
||||
const disableOthers = useCallback((toEnable) => {
|
||||
toEnable === 'USER' ? setShowUser(true) : setShowUser(false);
|
||||
toEnable === 'CLIENT' ? setShowClient(true) : setShowClient(false);
|
||||
toEnable === 'SERVER' ? setShowServer(true) : setShowServer(false);
|
||||
toEnable === 'RX' ? setShowRx(true) : setShowRx(false);
|
||||
toEnable === 'TX' ? setShowTx(true) : setShowTx(false);
|
||||
toEnable === 'PLAYBACK' ? setShowPlayback(true) : setShowPlayback(false);
|
||||
};
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className={collapsed ? style.container : style.container__expanded}>
|
||||
@@ -64,46 +64,58 @@ export default function InfoLogger() {
|
||||
<div
|
||||
onClick={() => setShowUser((s) => !s)}
|
||||
onAuxClick={() => disableOthers('USER')}
|
||||
onContextMenu={(e) => e.preventDefault()}
|
||||
className={showUser ? style.active : null}
|
||||
role='button'
|
||||
>
|
||||
USER
|
||||
</div>
|
||||
<div
|
||||
onClick={() => setShowClient((s) => !s)}
|
||||
onAuxClick={() => disableOthers('CLIENT')}
|
||||
onContextMenu={(e) => e.preventDefault()}
|
||||
className={showClient ? style.active : null}
|
||||
role='button'
|
||||
>
|
||||
CLIENT
|
||||
</div>
|
||||
<div
|
||||
onClick={() => setShowServer((s) => !s)}
|
||||
onAuxClick={() => disableOthers('SERVER')}
|
||||
onContextMenu={(e) => e.preventDefault()}
|
||||
className={showServer ? style.active : null}
|
||||
role='button'
|
||||
>
|
||||
SERVER
|
||||
</div>
|
||||
<div
|
||||
onClick={() => setShowPlayback((s) => !s)}
|
||||
onAuxClick={() => disableOthers('PLAYBACK')}
|
||||
onContextMenu={(e) => e.preventDefault()}
|
||||
className={showPlayback ? style.active : null}
|
||||
role='button'
|
||||
>
|
||||
Playback
|
||||
</div>
|
||||
<div
|
||||
onClick={() => setShowRx((s) => !s)}
|
||||
onAuxClick={() => disableOthers('RX')}
|
||||
onContextMenu={(e) => e.preventDefault()}
|
||||
className={showRx ? style.active : null}
|
||||
role='button'
|
||||
>
|
||||
RX
|
||||
</div>
|
||||
<div
|
||||
onClick={() => setShowTx((s) => !s)}
|
||||
onAuxClick={() => disableOthers('TX')}
|
||||
onContextMenu={(e) => e.preventDefault()}
|
||||
className={showTx ? style.active : null}
|
||||
role='button'
|
||||
>
|
||||
TX
|
||||
</div>
|
||||
<div onClick={clearLog} className={style.clear}>
|
||||
<div onClick={clearLog} className={style.clear} role='button'>
|
||||
Clear
|
||||
</div>
|
||||
</HStack>
|
||||
@@ -115,10 +127,10 @@ export default function InfoLogger() {
|
||||
d.level === 'INFO'
|
||||
? style.info
|
||||
: d.level === 'WARN'
|
||||
? style.warn
|
||||
: d.level === 'ERROR'
|
||||
? style.error
|
||||
: ''
|
||||
? style.warn
|
||||
: d.level === 'ERROR'
|
||||
? style.error
|
||||
: ''
|
||||
}
|
||||
>
|
||||
<div className={style.time}>{d.time}</div>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import React, { useState } from 'react';
|
||||
import style from './Info.module.scss';
|
||||
import PropTypes from 'prop-types';
|
||||
import CollapseBar from '../../common/components/collapseBar/CollapseBar';
|
||||
import style from './Info.module.scss';
|
||||
|
||||
export default function InfoTitle(props) {
|
||||
const [collapsed, setCollapsed] = useState(false);
|
||||
@@ -43,3 +44,9 @@ export default function InfoTitle(props) {
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
InfoTitle.propTypes = {
|
||||
title: PropTypes.string,
|
||||
data: PropTypes.object,
|
||||
roll: PropTypes.bool,
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import React from 'react';
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import SocketProvider from 'app/context/socketContext';
|
||||
import { QueryClient, QueryClientProvider } from 'react-query';
|
||||
|
||||
@@ -1,67 +1,114 @@
|
||||
import React, { memo, useContext } from 'react';
|
||||
import { ButtonGroup, Divider, HStack } from '@chakra-ui/react';
|
||||
import React, { memo, useCallback, useContext } from 'react';
|
||||
import { ButtonGroup, HStack } from '@chakra-ui/react';
|
||||
import { CursorContext } from '../../app/context/CursorContext';
|
||||
import { FiChevronsUp } from '@react-icons/all-files/fi/FiChevronsUp';
|
||||
import { FiChevronsDown } from '@react-icons/all-files/fi/FiChevronsDown';
|
||||
import { FiTarget } from '@react-icons/all-files/fi/FiTarget';
|
||||
import { IoCaretUp } from '@react-icons/all-files/io5/IoCaretUp';
|
||||
import { IoCaretDown } from '@react-icons/all-files/io5/IoCaretDown';
|
||||
import TooltipActionBtn from '../../common/components/buttons/TooltipActionBtn';
|
||||
import MenuActionButtons from './MenuActionButtons';
|
||||
import CollapseBtn from 'common/components/buttons/CollapseBtn';
|
||||
import CursorUpBtn from '../../common/components/buttons/CursorUpBtn';
|
||||
import CursorDownBtn from '../../common/components/buttons/CursorDownBtn';
|
||||
import CursorLockedBtn from 'common/components/buttons/CursorLockedBtn';
|
||||
import PropTypes from 'prop-types';
|
||||
import style from './EventListMenu.module.css';
|
||||
import ExpandBtn from '../../common/components/buttons/ExpandBtn';
|
||||
|
||||
const EventListMenu = ({ eventsHandler }) => {
|
||||
const { isCursorLocked, toggleCursorLocked, moveCursorUp, moveCursorDown } =
|
||||
useContext(CursorContext);
|
||||
|
||||
const actionHandler = (action) => {
|
||||
switch (action) {
|
||||
case 'event':
|
||||
eventsHandler('add', { type: action, order: 0 });
|
||||
break;
|
||||
case 'delay':
|
||||
eventsHandler('add', { type: action, order: 0 });
|
||||
break;
|
||||
case 'block':
|
||||
eventsHandler('add', { type: action, order: 0 });
|
||||
break;
|
||||
case 'cursorUp':
|
||||
moveCursorUp();
|
||||
break;
|
||||
case 'cursorDown':
|
||||
moveCursorDown();
|
||||
break;
|
||||
case 'togglelock':
|
||||
toggleCursorLocked();
|
||||
break;
|
||||
case 'deleteall':
|
||||
eventsHandler('deleteall');
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
const actionHandler = useCallback(
|
||||
(action) => {
|
||||
switch (action) {
|
||||
case 'event':
|
||||
eventsHandler('add', { type: action });
|
||||
break;
|
||||
case 'delay':
|
||||
eventsHandler('add', { type: action });
|
||||
break;
|
||||
case 'block':
|
||||
eventsHandler('add', { type: action });
|
||||
break;
|
||||
case 'cursorUp':
|
||||
moveCursorUp();
|
||||
break;
|
||||
case 'cursorDown':
|
||||
moveCursorDown();
|
||||
break;
|
||||
case 'togglelock':
|
||||
toggleCursorLocked();
|
||||
break;
|
||||
case 'deleteall':
|
||||
eventsHandler('deleteall');
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
},
|
||||
[eventsHandler, moveCursorDown, moveCursorUp, toggleCursorLocked]
|
||||
);
|
||||
|
||||
const collapsingBtnProps = {
|
||||
variant: 'outline',
|
||||
size: 'sm',
|
||||
};
|
||||
|
||||
const cursorBtnProps = {
|
||||
size: 'sm',
|
||||
color: 'pink.300',
|
||||
borderColor: 'pink.300',
|
||||
variant: 'outline',
|
||||
};
|
||||
|
||||
return (
|
||||
<HStack className={style.headerButtons}>
|
||||
<ButtonGroup isAttached>
|
||||
<ExpandBtn size='sm' clickhandler={() => eventsHandler('expandall')} />
|
||||
<CollapseBtn size='sm' clickhandler={() => eventsHandler('collapseall')} />
|
||||
</ButtonGroup>
|
||||
<Divider orientation='vertical' />
|
||||
<ButtonGroup isAttached>
|
||||
<CursorUpBtn size='sm' clickhandler={() => actionHandler('cursorUp')} />
|
||||
<CursorDownBtn size='sm' clickhandler={() => actionHandler('cursorDown')} />
|
||||
<CursorLockedBtn
|
||||
size='sm'
|
||||
clickhandler={() => actionHandler('togglelock')}
|
||||
active={isCursorLocked}
|
||||
width='3em'
|
||||
<TooltipActionBtn
|
||||
clickHandler={() => eventsHandler('expandall')}
|
||||
icon={<FiChevronsDown />}
|
||||
tooltip='Expand All'
|
||||
_hover={{ bg: '#ebedf0', color: '#333' }}
|
||||
{...collapsingBtnProps}
|
||||
/>
|
||||
<TooltipActionBtn
|
||||
clickHandler={() => eventsHandler('collapseall')}
|
||||
icon={<FiChevronsUp />}
|
||||
tooltip='Collapse All'
|
||||
_hover={{ bg: '#ebedf0', color: '#333' }}
|
||||
{...collapsingBtnProps}
|
||||
/>
|
||||
</ButtonGroup>
|
||||
<ButtonGroup isAttached>
|
||||
<TooltipActionBtn
|
||||
{...cursorBtnProps}
|
||||
clickHandler={() => actionHandler('cursorUp')}
|
||||
icon={<IoCaretUp />}
|
||||
tooltip='Move cursor up Alt + ↑'
|
||||
_hover={{ bg: 'pink.400' }}
|
||||
/>
|
||||
<TooltipActionBtn
|
||||
{...cursorBtnProps}
|
||||
clickHandler={() => actionHandler('cursorDown')}
|
||||
icon={<IoCaretDown />}
|
||||
tooltip='Move cursor down Alt + ↓'
|
||||
_hover={{ bg: 'pink.400' }}
|
||||
/>
|
||||
<TooltipActionBtn
|
||||
{...cursorBtnProps}
|
||||
clickHandler={() => actionHandler('togglelock')}
|
||||
icon={<FiTarget />}
|
||||
tooltip='Lock cursor to current'
|
||||
width='3em'
|
||||
backgroundColor={isCursorLocked && 'pink.400'}
|
||||
_hover={{ bg: 'pink.300' }}
|
||||
variant={isCursorLocked ? 'solid' : 'outline'}
|
||||
/>
|
||||
</ButtonGroup>
|
||||
<Divider orientation='vertical' />
|
||||
<MenuActionButtons actionHandler={actionHandler} size='sm' />
|
||||
</HStack>
|
||||
);
|
||||
};
|
||||
|
||||
export default memo(EventListMenu);
|
||||
|
||||
EventListMenu.propTypes = {
|
||||
eventsHandler: PropTypes.func,
|
||||
};
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
.headerButtons {
|
||||
align-content: center;
|
||||
justify-content: flex-end;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
@@ -7,9 +7,10 @@ import { FiPlus } from '@react-icons/all-files/fi/FiPlus';
|
||||
import { FiTrash2 } from '@react-icons/all-files/fi/FiTrash2';
|
||||
import { Divider } from '@chakra-ui/layout';
|
||||
import { Tooltip } from '@chakra-ui/tooltip';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
export default function MenuActionButtons(props) {
|
||||
const { actionHandler, size } = props;
|
||||
const { actionHandler, size = 'xs' } = props;
|
||||
const menuStyle = {
|
||||
color: '#000000',
|
||||
backgroundColor: 'rgba(255,255,255,1)',
|
||||
@@ -21,7 +22,7 @@ export default function MenuActionButtons(props) {
|
||||
<MenuButton
|
||||
as={IconButton}
|
||||
aria-label='Create Menu'
|
||||
size={size || 'xs'}
|
||||
size={size}
|
||||
icon={<FiPlus />}
|
||||
_expanded={{ bg: 'orange.300', color: 'white' }}
|
||||
_focus={{ boxShadow: 'none' }}
|
||||
@@ -47,3 +48,8 @@ export default function MenuActionButtons(props) {
|
||||
</Menu>
|
||||
);
|
||||
}
|
||||
|
||||
MenuActionButtons.propTypes = {
|
||||
actionHandler: PropTypes.func,
|
||||
size: PropTypes.oneOf(['xs', 'sm', 'md', 'lg']),
|
||||
};
|
||||
|
||||
@@ -1,18 +1,19 @@
|
||||
import React, { useCallback, useContext, useEffect, useRef } from 'react';
|
||||
import { useMutation, useQueryClient } from 'react-query';
|
||||
import { downloadEvents, uploadEvents } from 'app/api/ontimeApi';
|
||||
import { EVENTS_TABLE } from 'app/api/apiConstants';
|
||||
import DownloadIconBtn from './buttons/DownloadIconBtn';
|
||||
import SettingsIconBtn from './buttons/SettingsIconBtn';
|
||||
import MaxIconBtn from './buttons/MaxIconBtn';
|
||||
import MinIconBtn from './buttons/MinIconBtn';
|
||||
import QuitIconBtn from './buttons/QuitIconBtn';
|
||||
import style from './MenuBar.module.scss';
|
||||
import HelpIconBtn from './buttons/HelpIconBtn';
|
||||
import UploadIconBtn from './buttons/UploadIconBtn';
|
||||
import { LoggingContext } from '../../app/context/LoggingContext';
|
||||
import PropTypes from 'prop-types';
|
||||
import { VStack } from '@chakra-ui/react';
|
||||
import { downloadEvents, uploadEvents } from 'app/api/ontimeApi';
|
||||
import { FiMaximize } from '@react-icons/all-files/fi/FiMaximize';
|
||||
import { FiMinimize } from '@react-icons/all-files/fi/FiMinimize';
|
||||
import { FiHelpCircle } from '@react-icons/all-files/fi/FiHelpCircle';
|
||||
import { FiSettings } from '@react-icons/all-files/fi/FiSettings';
|
||||
import { FiUpload } from '@react-icons/all-files/fi/FiUpload';
|
||||
import { FiDownload } from '@react-icons/all-files/fi/FiDownload';
|
||||
import { EVENTS_TABLE } from 'app/api/apiConstants';
|
||||
import { LoggingContext } from '../../app/context/LoggingContext';
|
||||
import TooltipActionBtn from '../../common/components/buttons/TooltipActionBtn';
|
||||
import QuitIconBtn from '../../common/components/buttons/QuitIconBtn';
|
||||
import PropTypes from 'prop-types';
|
||||
import style from './MenuBar.module.scss';
|
||||
|
||||
export default function MenuBar(props) {
|
||||
const { isOpen, onOpen, onClose } = props;
|
||||
@@ -25,46 +26,47 @@ export default function MenuBar(props) {
|
||||
},
|
||||
});
|
||||
|
||||
const handleDownload = () => {
|
||||
downloadEvents();
|
||||
};
|
||||
|
||||
const handleClick = () => {
|
||||
const handleClick = useCallback(() => {
|
||||
if (hiddenFileInput && hiddenFileInput.current) {
|
||||
hiddenFileInput.current.click();
|
||||
}
|
||||
};
|
||||
}, [hiddenFileInput]);
|
||||
|
||||
const buttonStyle = {
|
||||
fontSize: '1.5em',
|
||||
size: 'lg',
|
||||
colorScheme: 'white',
|
||||
};
|
||||
|
||||
const handleUpload = (event) => {
|
||||
const fileUploaded = event.target.files[0];
|
||||
if (fileUploaded == null) return;
|
||||
const handleUpload = useCallback(
|
||||
(event) => {
|
||||
const fileUploaded = event.target.files[0];
|
||||
if (fileUploaded == null) return;
|
||||
|
||||
// Limit file size to 1MB
|
||||
if (fileUploaded.size > 1000000) {
|
||||
emitError('Error: File size limit (1MB) exceeded');
|
||||
return;
|
||||
}
|
||||
|
||||
// Check file extension
|
||||
if (fileUploaded.name.endsWith('.xlsx') || fileUploaded.name.endsWith('.json')) {
|
||||
try {
|
||||
uploaddb.mutate(fileUploaded);
|
||||
} catch (error) {
|
||||
emitError(`Failed uploading file: ${error}`);
|
||||
// Limit file size to 1MB
|
||||
if (fileUploaded.size > 1000000) {
|
||||
emitError('Error: File size limit (1MB) exceeded');
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
emitError('Error: File type unknown');
|
||||
}
|
||||
|
||||
// reset input value
|
||||
hiddenFileInput.current.value = '';
|
||||
};
|
||||
// Check file extension
|
||||
if (fileUploaded.name.endsWith('.xlsx') || fileUploaded.name.endsWith('.json')) {
|
||||
try {
|
||||
uploaddb.mutate(fileUploaded);
|
||||
} catch (error) {
|
||||
emitError(`Failed uploading file: ${error}`);
|
||||
}
|
||||
} else {
|
||||
emitError('Error: File type unknown');
|
||||
}
|
||||
|
||||
const handleIPC = (action) => {
|
||||
// reset input value
|
||||
hiddenFileInput.current.value = '';
|
||||
},
|
||||
[emitError, uploaddb]
|
||||
);
|
||||
|
||||
const handleIPC = useCallback((action) => {
|
||||
// Stop crashes when testing locally
|
||||
if (typeof window.process?.type === 'undefined') {
|
||||
if (action === 'help') {
|
||||
@@ -91,7 +93,7 @@ export default function MenuBar(props) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
};
|
||||
}, []);
|
||||
|
||||
// Handle keyboard shortcuts
|
||||
const handleKeyPress = useCallback(
|
||||
@@ -125,16 +127,32 @@ export default function MenuBar(props) {
|
||||
|
||||
return (
|
||||
<VStack>
|
||||
<QuitIconBtn size='lg' clickhandler={() => handleIPC('shutdown')} />
|
||||
<MaxIconBtn style={{ ...buttonStyle }} size='lg' clickhandler={() => handleIPC('max')} />
|
||||
<MinIconBtn style={{ ...buttonStyle }} size='lg' clickhandler={() => handleIPC('min')} />
|
||||
<QuitIconBtn clickHandler={() => handleIPC('shutdown')} />
|
||||
<TooltipActionBtn
|
||||
{...buttonStyle}
|
||||
icon={<FiMaximize />}
|
||||
clickHandler={() => handleIPC('max')}
|
||||
tooltip='Show full window'
|
||||
/>
|
||||
<TooltipActionBtn
|
||||
{...buttonStyle}
|
||||
icon={<FiMinimize />}
|
||||
clickHandler={() => handleIPC('min')}
|
||||
tooltip='Close to tray'
|
||||
/>
|
||||
<div className={style.gap} />
|
||||
<HelpIconBtn style={{ ...buttonStyle }} size='lg' clickhandler={() => handleIPC('help')} />
|
||||
<SettingsIconBtn
|
||||
style={{ ...buttonStyle }}
|
||||
size='lg'
|
||||
<TooltipActionBtn
|
||||
{...buttonStyle}
|
||||
icon={<FiHelpCircle />}
|
||||
clickHandler={() => handleIPC('help')}
|
||||
tooltip='Help'
|
||||
/>
|
||||
<TooltipActionBtn
|
||||
{...buttonStyle}
|
||||
icon={<FiSettings />}
|
||||
className={isOpen ? style.open : ''}
|
||||
clickhandler={onOpen}
|
||||
clickHandler={onOpen}
|
||||
tooltip='Settings'
|
||||
isRound
|
||||
/>
|
||||
<div className={style.gap} />
|
||||
@@ -145,8 +163,18 @@ export default function MenuBar(props) {
|
||||
onChange={handleUpload}
|
||||
accept='.json, .xlsx'
|
||||
/>
|
||||
<UploadIconBtn style={{ ...buttonStyle }} size='lg' clickhandler={handleClick} />
|
||||
<DownloadIconBtn style={{ ...buttonStyle }} size='lg' clickhandler={handleDownload} />
|
||||
<TooltipActionBtn
|
||||
{...buttonStyle}
|
||||
icon={<FiUpload />}
|
||||
clickHandler={handleClick}
|
||||
tooltip='Import event list'
|
||||
/>
|
||||
<TooltipActionBtn
|
||||
{...buttonStyle}
|
||||
icon={<FiDownload />}
|
||||
clickHandler={downloadEvents}
|
||||
tooltip='Export event list'
|
||||
/>
|
||||
</VStack>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import React from 'react';
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import MenuBar from '../MenuBar';
|
||||
import { queryClientMock } from '../../../__mocks__/QueryClient.mock';
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import React from 'react';
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import MenuActionButtons from "../MenuActionButtons";
|
||||
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import React from 'react';
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import MenuBar from '../MenuBar';
|
||||
import { queryClientMock } from '../../../__mocks__/QueryClient.mock';
|
||||
|
||||
@@ -1,20 +0,0 @@
|
||||
import React from 'react';
|
||||
import { IconButton } from '@chakra-ui/button';
|
||||
import { Tooltip } from '@chakra-ui/tooltip';
|
||||
import { FiDownload } from '@react-icons/all-files/fi/FiDownload';
|
||||
|
||||
export default function DownloadIconBtn(props) {
|
||||
const { clickhandler, size, ...rest } = props;
|
||||
return (
|
||||
<Tooltip label='Export event list'>
|
||||
<IconButton
|
||||
size={size || 'xs'}
|
||||
icon={<FiDownload />}
|
||||
colorScheme='white'
|
||||
onClick={clickhandler}
|
||||
_focus={{ boxShadow: 'none' }}
|
||||
{...rest}
|
||||
/>
|
||||
</Tooltip>
|
||||
);
|
||||
}
|
||||
@@ -1,20 +0,0 @@
|
||||
import React from 'react';
|
||||
import { IconButton } from '@chakra-ui/button';
|
||||
import { Tooltip } from '@chakra-ui/tooltip';
|
||||
import { FiHelpCircle } from '@react-icons/all-files/fi/FiHelpCircle';
|
||||
|
||||
export default function HelpIconBtn(props) {
|
||||
const { clickhandler, size, ...rest } = props;
|
||||
return (
|
||||
<Tooltip label='Help'>
|
||||
<IconButton
|
||||
size={size || 'xs'}
|
||||
icon={<FiHelpCircle />}
|
||||
colorScheme='white'
|
||||
onClick={clickhandler}
|
||||
_focus={{ boxShadow: 'none' }}
|
||||
{...rest}
|
||||
/>
|
||||
</Tooltip>
|
||||
);
|
||||
}
|
||||
@@ -1,20 +0,0 @@
|
||||
import React from 'react';
|
||||
import { IconButton } from '@chakra-ui/button';
|
||||
import { Tooltip } from '@chakra-ui/tooltip';
|
||||
import { FiMaximize } from '@react-icons/all-files/fi/FiMaximize';
|
||||
|
||||
export default function MaxIconBtn(props) {
|
||||
const { clickhandler, size, ...rest } = props;
|
||||
return (
|
||||
<Tooltip label='Show full window'>
|
||||
<IconButton
|
||||
size={size || 'xs'}
|
||||
icon={<FiMaximize />}
|
||||
colorScheme='white'
|
||||
onClick={clickhandler}
|
||||
_focus={{ boxShadow: 'none' }}
|
||||
{...rest}
|
||||
/>
|
||||
</Tooltip>
|
||||
);
|
||||
}
|
||||
@@ -1,20 +0,0 @@
|
||||
import React from 'react';
|
||||
import { IconButton } from '@chakra-ui/button';
|
||||
import { Tooltip } from '@chakra-ui/tooltip';
|
||||
import { FiMinimize } from '@react-icons/all-files/fi/FiMinimize';
|
||||
|
||||
export default function MinIconBtn(props) {
|
||||
const { clickhandler, size, ...rest } = props;
|
||||
return (
|
||||
<Tooltip label='Close to tray'>
|
||||
<IconButton
|
||||
size={size || 'xs'}
|
||||
icon={<FiMinimize />}
|
||||
colorScheme='white'
|
||||
onClick={clickhandler}
|
||||
_focus={{ boxShadow: 'none' }}
|
||||
{...rest}
|
||||
/>
|
||||
</Tooltip>
|
||||
);
|
||||
}
|
||||
@@ -1,68 +0,0 @@
|
||||
import React, { useRef, useState } from 'react';
|
||||
import { Button, IconButton } from '@chakra-ui/button';
|
||||
import {
|
||||
AlertDialog,
|
||||
AlertDialogBody,
|
||||
AlertDialogContent,
|
||||
AlertDialogFooter,
|
||||
AlertDialogHeader,
|
||||
AlertDialogOverlay,
|
||||
} from '@chakra-ui/modal';
|
||||
import { Tooltip } from '@chakra-ui/tooltip';
|
||||
import { FiPower } from '@react-icons/all-files/fi/FiPower';
|
||||
|
||||
export default function QuitIconBtn(props) {
|
||||
const { clickhandler, size, ...rest } = props;
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const onClose = () => setIsOpen(false);
|
||||
const cancelRef = useRef();
|
||||
|
||||
const handleShutdown = () => {
|
||||
onClose();
|
||||
clickhandler();
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<Tooltip label='Quit Application'>
|
||||
<IconButton
|
||||
size={size || 'xs'}
|
||||
icon={<FiPower />}
|
||||
colorScheme='red'
|
||||
variant='outline'
|
||||
isRound
|
||||
onClick={() => setIsOpen(true)}
|
||||
_focus={{ boxShadow: 'none' }}
|
||||
{...rest}
|
||||
/>
|
||||
</Tooltip>
|
||||
<AlertDialog
|
||||
isOpen={isOpen}
|
||||
leastDestructiveRef={cancelRef}
|
||||
onClose={onClose}
|
||||
>
|
||||
<AlertDialogOverlay>
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader fontSize='lg' fontWeight='bold'>
|
||||
Server Shutdown
|
||||
</AlertDialogHeader>
|
||||
|
||||
<AlertDialogBody>
|
||||
This will shutdown the program and all running servers. Are you
|
||||
sure?
|
||||
</AlertDialogBody>
|
||||
|
||||
<AlertDialogFooter>
|
||||
<Button ref={cancelRef} onClick={onClose}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button colorScheme='red' onClick={handleShutdown} ml={3}>
|
||||
Shutdown
|
||||
</Button>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
</AlertDialogOverlay>
|
||||
</AlertDialog>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -1,20 +0,0 @@
|
||||
import React from 'react';
|
||||
import { IconButton } from '@chakra-ui/button';
|
||||
import { Tooltip } from '@chakra-ui/tooltip';
|
||||
import { FiSettings } from '@react-icons/all-files/fi/FiSettings';
|
||||
|
||||
export default function SettingsIconBtn(props) {
|
||||
const { clickhandler, size, ...rest } = props;
|
||||
return (
|
||||
<Tooltip label='Settings'>
|
||||
<IconButton
|
||||
size={size || 'xs'}
|
||||
icon={<FiSettings />}
|
||||
colorScheme='white'
|
||||
onClick={clickhandler}
|
||||
_focus={{ boxShadow: 'none' }}
|
||||
{...rest}
|
||||
/>
|
||||
</Tooltip>
|
||||
);
|
||||
}
|
||||
@@ -1,20 +0,0 @@
|
||||
import React from 'react';
|
||||
import { IconButton } from '@chakra-ui/button';
|
||||
import { Tooltip } from '@chakra-ui/tooltip';
|
||||
import { FiUpload } from '@react-icons/all-files/fi/FiUpload';
|
||||
|
||||
export default function UploadIconBtn(props) {
|
||||
const { clickhandler, size, ...rest } = props;
|
||||
return (
|
||||
<Tooltip label='Import event list'>
|
||||
<IconButton
|
||||
size={size || 'xs'}
|
||||
icon={<FiUpload />}
|
||||
colorScheme='white'
|
||||
onClick={clickhandler}
|
||||
_focus={{ boxShadow: 'none' }}
|
||||
{...rest}
|
||||
/>
|
||||
</Tooltip>
|
||||
);
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
import React, { useContext, useEffect, useState } from 'react';
|
||||
/* eslint-disable jsx-a11y/anchor-has-content */
|
||||
import React, { useCallback, useContext, useEffect, useState } from 'react';
|
||||
import { Button, IconButton } from '@chakra-ui/button';
|
||||
import { IoInformationCircleOutline } from '@react-icons/all-files/io5/IoInformationCircleOutline';
|
||||
import { IoRemove } from '@react-icons/all-files/io5/IoRemove';
|
||||
@@ -13,7 +14,7 @@ import { viewerLocations } from '../../app/appConstants';
|
||||
import { LoggingContext } from '../../app/context/LoggingContext';
|
||||
import { validateAlias } from '../../app/utils/aliases';
|
||||
import SubmitContainer from './SubmitContainer';
|
||||
import { handleLinks, host, openLink } from '../../common/utils/linkUtils';
|
||||
import { handleLinks, host } from '../../common/utils/linkUtils';
|
||||
import style from './Modals.module.scss';
|
||||
|
||||
export default function AliasesModal() {
|
||||
@@ -35,45 +36,48 @@ export default function AliasesModal() {
|
||||
/**
|
||||
* Validate and submit data
|
||||
*/
|
||||
const submitHandler = async (event) => {
|
||||
event.preventDefault();
|
||||
setSubmitting(true);
|
||||
const submitHandler = useCallback(
|
||||
async (event) => {
|
||||
event.preventDefault();
|
||||
setSubmitting(true);
|
||||
|
||||
const validatedAliases = [...aliases];
|
||||
let errors = false;
|
||||
for (const alias of validatedAliases) {
|
||||
// validate url
|
||||
const isURLValid = validateAlias(alias.pathAndParams);
|
||||
if (!isURLValid.status) {
|
||||
alias.urlError = isURLValid.message;
|
||||
errors = true;
|
||||
} else {
|
||||
alias.urlError = undefined;
|
||||
const validatedAliases = [...aliases];
|
||||
let errors = false;
|
||||
for (const alias of validatedAliases) {
|
||||
// validate url
|
||||
const isURLValid = validateAlias(alias.pathAndParams);
|
||||
if (!isURLValid.status) {
|
||||
alias.urlError = isURLValid.message;
|
||||
errors = true;
|
||||
} else {
|
||||
alias.urlError = undefined;
|
||||
}
|
||||
// validate alias
|
||||
const isAliasValid = validateAlias(alias.alias);
|
||||
if (!isAliasValid.status) {
|
||||
alias.aliasError = isAliasValid.message;
|
||||
errors = true;
|
||||
} else {
|
||||
alias.aliasError = undefined;
|
||||
}
|
||||
}
|
||||
// validate alias
|
||||
const isAliasValid = validateAlias(alias.alias);
|
||||
if (!isAliasValid.status) {
|
||||
alias.aliasError = isAliasValid.message;
|
||||
errors = true;
|
||||
} else {
|
||||
alias.aliasError = undefined;
|
||||
setAliases(validatedAliases);
|
||||
|
||||
if (!errors) {
|
||||
await postAliases(aliases);
|
||||
await refetch();
|
||||
setChanged(false);
|
||||
}
|
||||
}
|
||||
setAliases(validatedAliases);
|
||||
|
||||
if (!errors) {
|
||||
await postAliases(aliases);
|
||||
await refetch();
|
||||
setChanged(false);
|
||||
}
|
||||
|
||||
setSubmitting(false);
|
||||
};
|
||||
setSubmitting(false);
|
||||
},
|
||||
[aliases, refetch]
|
||||
);
|
||||
|
||||
/**
|
||||
* Creates a new alias in state with a temporary id
|
||||
*/
|
||||
const addNew = () => {
|
||||
const addNew = useCallback(() => {
|
||||
if (aliases.length > 20) {
|
||||
emitError('Maximum amount of aliases reacted (20)');
|
||||
return;
|
||||
@@ -87,23 +91,23 @@ export default function AliasesModal() {
|
||||
};
|
||||
setAliases((prevState) => [...prevState, emptyAlias]);
|
||||
setChanged(true);
|
||||
};
|
||||
}, [aliases.length, emitError]);
|
||||
|
||||
/**
|
||||
* Deletes an alias by a given id
|
||||
* @param {string} id - id of alias to delete
|
||||
*/
|
||||
const deleteAlias = (id) => {
|
||||
const deleteAlias = useCallback((id) => {
|
||||
setAliases((prevState) => [...prevState.filter((a) => a.id !== id)]);
|
||||
setChanged(true);
|
||||
};
|
||||
}, []);
|
||||
|
||||
/**
|
||||
* Sets enabled flag to true / false
|
||||
* @param {string} id - object id
|
||||
* @param {boolean} isEnabled - whether to enable / disable flag
|
||||
*/
|
||||
const setEnabled = (id, isEnabled) => {
|
||||
const setEnabled = useCallback((id, isEnabled) => {
|
||||
const aliasesState = [...aliases];
|
||||
for (const a of aliasesState) {
|
||||
if (a.id === id) {
|
||||
@@ -125,7 +129,7 @@ export default function AliasesModal() {
|
||||
}
|
||||
setChanged(true);
|
||||
setAliases(aliasesState);
|
||||
};
|
||||
}, [aliases, emitError]);
|
||||
|
||||
/**
|
||||
* Reverts local state equals to server state
|
||||
@@ -141,12 +145,15 @@ export default function AliasesModal() {
|
||||
* @param {string} field - object parameter to update
|
||||
* @param {string} value - new object parameter value
|
||||
*/
|
||||
const handleChange = (index, field, value) => {
|
||||
const temp = [...aliases];
|
||||
temp[index][field] = value;
|
||||
setAliases(temp);
|
||||
setChanged(true);
|
||||
};
|
||||
const handleChange = useCallback(
|
||||
(index, field, value) => {
|
||||
const temp = [...aliases];
|
||||
temp[index][field] = value;
|
||||
setAliases(temp);
|
||||
setChanged(true);
|
||||
},
|
||||
[aliases]
|
||||
);
|
||||
|
||||
return (
|
||||
<ModalBody className={style.modalBody}>
|
||||
@@ -183,16 +190,16 @@ export default function AliasesModal() {
|
||||
eg. a lower third url with some custom parameters
|
||||
<table>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td className={style.labelNote} style={{ width: '30%' }}>
|
||||
Alias
|
||||
</td>
|
||||
<td className={style.labelNote}>Page URL</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>mylower</td>
|
||||
<td>lower?bg=ff2&text=f00&size=0.6&transition=5</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td className={style.labelNote} style={{ width: '30%' }}>
|
||||
Alias
|
||||
</td>
|
||||
<td className={style.labelNote}>Page URL</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>mylower</td>
|
||||
<td>lower?bg=ff2&text=f00&size=0.6&transition=5</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<br />
|
||||
@@ -201,20 +208,20 @@ export default function AliasesModal() {
|
||||
eg. an unattended screen that you would need to change route from the app
|
||||
<table>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td className={style.labelNote} style={{ width: '30%' }}>
|
||||
Alias
|
||||
</td>
|
||||
<td className={style.labelNote}>Page URL</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>thirdfloor</td>
|
||||
<td>public</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td className={style.labelNote} style={{ width: '30%' }}>
|
||||
Alias
|
||||
</td>
|
||||
<td className={style.labelNote}>Page URL</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>thirdfloor</td>
|
||||
<td>public</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div className={style.inlineAliasPlaceholder} style={{ padding: '0.5em 0' }}>
|
||||
<div className={style.inlineAliasPlaceholder}>
|
||||
<span className={style.labelNote}>Alias</span>
|
||||
<span className={style.labelNote}>Page URL</span>
|
||||
</div>
|
||||
@@ -247,11 +254,12 @@ export default function AliasesModal() {
|
||||
href='#!'
|
||||
target='_blank'
|
||||
rel='noreferrer'
|
||||
onClick={(e) => openLink(e, alias.pathAndParams)}
|
||||
onClick={(e) => handleLinks(e, alias.pathAndParams)}
|
||||
/>
|
||||
</Tooltip>
|
||||
<Tooltip label='Enable alias' openDelay={500}>
|
||||
<IconButton
|
||||
aria-label='Enable alias'
|
||||
size='xs'
|
||||
icon={<IoSunny />}
|
||||
colorScheme='blue'
|
||||
@@ -261,6 +269,7 @@ export default function AliasesModal() {
|
||||
</Tooltip>
|
||||
<Tooltip label='Delete alias' openDelay={500}>
|
||||
<IconButton
|
||||
aria-label='Delete alias'
|
||||
size='xs'
|
||||
icon={<IoRemove />}
|
||||
colorScheme='red'
|
||||
@@ -277,7 +286,7 @@ export default function AliasesModal() {
|
||||
</div>
|
||||
))}
|
||||
|
||||
<div className={style.inlineAliasPlaceholder} style={{ padding: '0.5em 0' }}>
|
||||
<div className={style.inlineAliasPlaceholder}>
|
||||
<Button size='xs' colorScheme='blue' variant='outline' onClick={() => addNew()}>
|
||||
Add new
|
||||
</Button>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, { useContext, useEffect, useState } from 'react';
|
||||
import React, { useCallback, useContext, useEffect, useState } from 'react';
|
||||
import { ModalBody } from '@chakra-ui/modal';
|
||||
import { Checkbox, FormControl, FormLabel, Input, PinInput, PinInputField } from '@chakra-ui/react';
|
||||
import { getSettings, ontimePlaceholderSettings, postSettings } from 'app/api/ontimeApi';
|
||||
@@ -8,9 +8,12 @@ import style from './Modals.module.scss';
|
||||
import { LoggingContext } from '../../app/context/LoggingContext';
|
||||
import { IconButton } from '@chakra-ui/button';
|
||||
import { FiEye } from '@react-icons/all-files/fi/FiEye';
|
||||
import { FiX } from '@react-icons/all-files/fi/FiX';
|
||||
import SubmitContainer from './SubmitContainer';
|
||||
import { inputProps } from './modalHelper';
|
||||
import { LocalEventSettingsContext } from '../../app/context/LocalEventSettingsContext';
|
||||
import TooltipActionBtn from '../../common/components/buttons/TooltipActionBtn';
|
||||
const version = require('../../../package.json').version
|
||||
|
||||
export default function AppSettingsModal() {
|
||||
const { data, status, refetch } = useFetch(APP_SETTINGS, getSettings);
|
||||
@@ -65,48 +68,63 @@ export default function AppSettingsModal() {
|
||||
/**
|
||||
* Validate and submit data
|
||||
*/
|
||||
const submitHandler = async (event) => {
|
||||
event.preventDefault();
|
||||
setSubmitting(true);
|
||||
const submitHandler = useCallback(
|
||||
async (event) => {
|
||||
event.preventDefault();
|
||||
setSubmitting(true);
|
||||
|
||||
// set context
|
||||
setShowQuickEntry(doShowQuickEntry);
|
||||
setStarTimeIsLastEnd(doStarTimeIsLastEnd);
|
||||
setDefaultPublic(doDefaultPublic);
|
||||
// set context
|
||||
setShowQuickEntry(doShowQuickEntry);
|
||||
setStarTimeIsLastEnd(doStarTimeIsLastEnd);
|
||||
setDefaultPublic(doDefaultPublic);
|
||||
|
||||
const f = formData;
|
||||
const f = formData;
|
||||
|
||||
// we might not have changed this
|
||||
if (f.pinCode !== data.pinCode) {
|
||||
const e = { status: false, message: '' };
|
||||
// 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';
|
||||
// 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);
|
||||
}
|
||||
}
|
||||
|
||||
// set fields with error
|
||||
if (!e.status) {
|
||||
emitError(`Invalid Input: ${e.message}`);
|
||||
} else {
|
||||
await postSettings(formData);
|
||||
await refetch();
|
||||
emitWarning(e.message);
|
||||
setChanged(false);
|
||||
}
|
||||
}
|
||||
setSubmitting(false);
|
||||
setChanged(false);
|
||||
};
|
||||
setSubmitting(false);
|
||||
setChanged(false);
|
||||
},
|
||||
[
|
||||
data.pinCode,
|
||||
doDefaultPublic,
|
||||
doShowQuickEntry,
|
||||
doStarTimeIsLastEnd,
|
||||
emitError,
|
||||
emitWarning,
|
||||
formData,
|
||||
refetch,
|
||||
setDefaultPublic,
|
||||
setShowQuickEntry,
|
||||
setStarTimeIsLastEnd,
|
||||
]
|
||||
);
|
||||
|
||||
/**
|
||||
* Reverts local state equals to server state
|
||||
*/
|
||||
const revert = async () => {
|
||||
const revert = useCallback(async () => {
|
||||
setChanged(false);
|
||||
await refetch();
|
||||
|
||||
@@ -114,26 +132,29 @@ export default function AppSettingsModal() {
|
||||
setDoShowQuickEntry(showQuickEntry);
|
||||
setDoStarTimeIsLastEnd(starTimeIsLastEnd);
|
||||
setDoDefaultPublic(defaultPublic);
|
||||
};
|
||||
}, [defaultPublic, refetch, showQuickEntry, starTimeIsLastEnd]);
|
||||
|
||||
/**
|
||||
* Handles change of input field in local state
|
||||
* @param {string} field - object parameter to update
|
||||
* @param {string} value - new object parameter value
|
||||
*/
|
||||
const handleChange = (field, value) => {
|
||||
const temp = { ...formData };
|
||||
temp[field] = value;
|
||||
setFormData(temp);
|
||||
setChanged(true);
|
||||
};
|
||||
const handleChange = useCallback(
|
||||
(field, value) => {
|
||||
const temp = { ...formData };
|
||||
temp[field] = value;
|
||||
setFormData(temp);
|
||||
setChanged(true);
|
||||
},
|
||||
[formData]
|
||||
);
|
||||
|
||||
/**
|
||||
* Sets changed flag to true
|
||||
*/
|
||||
const handleContextChange = () => {
|
||||
const handleContextChange = useCallback(() => {
|
||||
setChanged(true);
|
||||
};
|
||||
}, []);
|
||||
|
||||
const disableModal = status !== 'success';
|
||||
|
||||
@@ -144,6 +165,7 @@ export default function AppSettingsModal() {
|
||||
<br />
|
||||
🔥 Changes take effect on save 🔥
|
||||
</p>
|
||||
<p className={style.notes}>{`Running ontime version ${version}`}</p>
|
||||
<form onSubmit={submitHandler}>
|
||||
<div className={style.modalFields}>
|
||||
<div className={style.hSeparator}>General App Settings</div>
|
||||
@@ -197,6 +219,16 @@ export default function AppSettingsModal() {
|
||||
onMouseUp={() => setHidePin(true)}
|
||||
isDisabled={disableModal}
|
||||
/>
|
||||
<TooltipActionBtn
|
||||
tooltip='Clear pincode'
|
||||
size='sm'
|
||||
colorScheme='red'
|
||||
variant='ghost'
|
||||
icon={<FiX />}
|
||||
onMouseDown={() => handleChange('pinCode', '')}
|
||||
onMouseUp={() => handleChange('pinCode', '')}
|
||||
isDisabled={disableModal}
|
||||
/>
|
||||
</div>
|
||||
</FormControl>
|
||||
</div>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import React, { useCallback, useEffect, useState } from 'react';
|
||||
import { ModalBody } from '@chakra-ui/modal';
|
||||
import { FormLabel, Input, Textarea } from '@chakra-ui/react';
|
||||
import { fetchEvent, postEvent } from 'app/api/eventApi';
|
||||
@@ -34,36 +34,39 @@ export default function SettingsModal() {
|
||||
/**
|
||||
* Validate and submit data
|
||||
*/
|
||||
const submitHandler = async (event) => {
|
||||
event.preventDefault();
|
||||
setSubmitting(true);
|
||||
const submitHandler = useCallback(
|
||||
async (event) => {
|
||||
event.preventDefault();
|
||||
setSubmitting(true);
|
||||
|
||||
await postEvent(formData);
|
||||
await refetch();
|
||||
await postEvent(formData);
|
||||
await refetch();
|
||||
|
||||
setChanged(false);
|
||||
setSubmitting(false);
|
||||
};
|
||||
setChanged(false);
|
||||
setSubmitting(false);
|
||||
},
|
||||
[formData, refetch]
|
||||
);
|
||||
|
||||
/**
|
||||
* Reverts local state equals to server state
|
||||
*/
|
||||
const revert = async () => {
|
||||
const revert = useCallback(async () => {
|
||||
setChanged(false);
|
||||
await refetch();
|
||||
};
|
||||
}, [refetch]);
|
||||
|
||||
/**
|
||||
* Handles change of input field in local state
|
||||
* @param {string} field - object parameter to update
|
||||
* @param {string} value - new object parameter value
|
||||
*/
|
||||
const handleChange = (field, value) => {
|
||||
const handleChange = useCallback((field, value) => {
|
||||
const temp = { ...formData };
|
||||
temp[field] = value;
|
||||
setFormData(temp);
|
||||
setChanged(true);
|
||||
};
|
||||
},[formData]);
|
||||
|
||||
return (
|
||||
<ModalBody className={style.modalBody}>
|
||||
@@ -116,9 +119,7 @@ export default function SettingsModal() {
|
||||
name='pubInfo'
|
||||
placeholder='Information to be shown on public screens'
|
||||
value={formData.publicInfo}
|
||||
onChange={(event) =>
|
||||
handleChange('publicInfo', event.target.value)
|
||||
}
|
||||
onChange={(event) => handleChange('publicInfo', event.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<div className={style.spacedEntry}>
|
||||
@@ -135,9 +136,7 @@ export default function SettingsModal() {
|
||||
placeholder='Information to be shown on backstage screens'
|
||||
resize={false}
|
||||
value={formData.backstageInfo}
|
||||
onChange={(event) =>
|
||||
handleChange('backstageInfo', event.target.value)
|
||||
}
|
||||
onChange={(event) => handleChange('backstageInfo', event.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<div className={style.spacedEntry}>
|
||||
@@ -154,9 +153,7 @@ export default function SettingsModal() {
|
||||
name='endMessage'
|
||||
placeholder='Empty message shows elapsed time'
|
||||
value={formData.endMessage}
|
||||
onChange={(event) =>
|
||||
handleChange('endMessage', event.target.value)
|
||||
}
|
||||
onChange={(event) => handleChange('endMessage', event.target.value)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, { useContext, useEffect, useState } from 'react';
|
||||
import React, { useCallback, useContext, useEffect, useState } from 'react';
|
||||
import { ModalBody } from '@chakra-ui/modal';
|
||||
import { FormControl, FormLabel, Input, Switch } from '@chakra-ui/react';
|
||||
import { getInfo, httpPlaceholder, ontimeVars, postInfo } from 'app/api/ontimeApi';
|
||||
@@ -36,29 +36,32 @@ export default function IntegrationSettingsModal() {
|
||||
/**
|
||||
* Validate and submit data
|
||||
*/
|
||||
const submitHandler = async (event) => {
|
||||
event.preventDefault();
|
||||
const submitHandler = useCallback(
|
||||
async (event) => {
|
||||
event.preventDefault();
|
||||
|
||||
const f = formData;
|
||||
let e = { status: false, message: '' };
|
||||
const f = formData;
|
||||
const e = { status: false, message: '' };
|
||||
|
||||
// set fields with error
|
||||
if (e.status) {
|
||||
emitError(`Invalid Input: ${e.message}`);
|
||||
} else {
|
||||
await postInfo(f);
|
||||
setChanged(false);
|
||||
setSubmitting(false);
|
||||
}
|
||||
};
|
||||
// set fields with error
|
||||
if (e.status) {
|
||||
emitError(`Invalid Input: ${e.message}`);
|
||||
} else {
|
||||
await postInfo(f);
|
||||
setChanged(false);
|
||||
setSubmitting(false);
|
||||
}
|
||||
},
|
||||
[emitError, formData]
|
||||
);
|
||||
|
||||
/**
|
||||
* Reverts local state equals to server state
|
||||
*/
|
||||
const revert = async () => {
|
||||
const revert = useCallback(async () => {
|
||||
setChanged(false);
|
||||
await refetch();
|
||||
};
|
||||
}, [refetch]);
|
||||
|
||||
// Todo: make change handler
|
||||
// Todo: toggle between GET / POST
|
||||
|
||||
@@ -31,6 +31,7 @@
|
||||
grid-template-columns: 20% 1fr 1em 1.5em 1.5em;
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
padding: 0.5em 0;
|
||||
}
|
||||
|
||||
.error {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, { useContext, useEffect, useState } from 'react';
|
||||
import React, { useCallback, useContext, useEffect, useState } from 'react';
|
||||
import { ModalBody } from '@chakra-ui/modal';
|
||||
import { FormControl, FormLabel, Input } from '@chakra-ui/react';
|
||||
import { getOSC, oscPlaceholderSettings, postOSC } from 'app/api/ontimeApi';
|
||||
@@ -92,59 +92,65 @@ export default function OscSettingsModal() {
|
||||
/**
|
||||
* Validate and submit data
|
||||
*/
|
||||
const submitHandler = async (event) => {
|
||||
event.preventDefault();
|
||||
setSubmitting(true);
|
||||
const submitHandler = useCallback(
|
||||
async (event) => {
|
||||
event.preventDefault();
|
||||
setSubmitting(true);
|
||||
|
||||
const f = formData;
|
||||
const e = { status: false, message: '' };
|
||||
const f = formData;
|
||||
const e = { status: false, message: '' };
|
||||
|
||||
// Validate fields
|
||||
if (f.port < 1024 || f.port > 65535) {
|
||||
// Port in incorrect range
|
||||
e.status = true;
|
||||
e.message += 'OSC IN Port in incorrect range (1024 - 65535)';
|
||||
} else if (f.portOut < 1024 || f.portOut > 65535) {
|
||||
// Port in incorrect range
|
||||
e.status = true;
|
||||
e.message += 'OSC OUT Port in incorrect range (1024 - 65535)';
|
||||
} else if (f.port === f.portOut) {
|
||||
// Cant use the same port
|
||||
e.status = true;
|
||||
e.message += 'OSC IN and OUT Ports cant be the same';
|
||||
}
|
||||
// Validate fields
|
||||
if (f.port < 1024 || f.port > 65535) {
|
||||
// Port in incorrect range
|
||||
e.status = true;
|
||||
e.message += 'OSC IN Port in incorrect range (1024 - 65535)';
|
||||
} else if (f.portOut < 1024 || f.portOut > 65535) {
|
||||
// Port in incorrect range
|
||||
e.status = true;
|
||||
e.message += 'OSC OUT Port in incorrect range (1024 - 65535)';
|
||||
} else if (f.port === f.portOut) {
|
||||
// Cant use the same port
|
||||
e.status = true;
|
||||
e.message += 'OSC IN and OUT Ports cant be the same';
|
||||
}
|
||||
|
||||
// set fields with error
|
||||
if (e.status) {
|
||||
emitError(`Invalid Input: ${e.message}`);
|
||||
} else {
|
||||
// Post here
|
||||
await postOSC(formData);
|
||||
await refetch();
|
||||
setChanged(false);
|
||||
}
|
||||
setSubmitting(false);
|
||||
};
|
||||
// set fields with error
|
||||
if (e.status) {
|
||||
emitError(`Invalid Input: ${e.message}`);
|
||||
} else {
|
||||
// Post here
|
||||
await postOSC(formData);
|
||||
await refetch();
|
||||
setChanged(false);
|
||||
}
|
||||
setSubmitting(false);
|
||||
},
|
||||
[emitError, formData, refetch]
|
||||
);
|
||||
|
||||
/**
|
||||
* Reverts local state equals to server state
|
||||
*/
|
||||
const revert = async () => {
|
||||
const revert = useCallback(async () => {
|
||||
setChanged(false);
|
||||
await refetch();
|
||||
};
|
||||
}, [refetch]);
|
||||
|
||||
/**
|
||||
* Handles change of input field in local state
|
||||
* @param {string} field - object parameter to update
|
||||
* @param {(string | number | boolean)} value - new object parameter value
|
||||
*/
|
||||
const handleChange = (field, value) => {
|
||||
const temp = { ...formData };
|
||||
temp[field] = value;
|
||||
setFormData(temp);
|
||||
setChanged(true);
|
||||
};
|
||||
const handleChange = useCallback(
|
||||
(field, value) => {
|
||||
const temp = { ...formData };
|
||||
temp[field] = value;
|
||||
setFormData(temp);
|
||||
setChanged(true);
|
||||
},
|
||||
[formData]
|
||||
);
|
||||
|
||||
return (
|
||||
<ModalBody className={style.modalBody}>
|
||||
@@ -184,7 +190,7 @@ export default function OscSettingsModal() {
|
||||
name='port'
|
||||
placeholder='8888'
|
||||
value={formData.port}
|
||||
onChange={(event) => handleChange('port', parseInt(event.target.value))}
|
||||
onChange={(event) => handleChange('port', parseInt(event.target.value, 10))}
|
||||
style={{ width: '6em', textAlign: 'center' }}
|
||||
/>
|
||||
</FormControl>
|
||||
@@ -224,7 +230,7 @@ export default function OscSettingsModal() {
|
||||
name='portOut'
|
||||
placeholder='9999'
|
||||
value={formData.portOut}
|
||||
onChange={(event) => handleChange('portOut', parseInt(event.target.value))}
|
||||
onChange={(event) => handleChange('portOut', parseInt(event.target.value, 10))}
|
||||
style={{ width: '6em', textAlign: 'left' }}
|
||||
/>
|
||||
</FormControl>
|
||||
|
||||
@@ -11,7 +11,7 @@ export default function SubmitContainer(props) {
|
||||
<Button
|
||||
isDisabled={submitting || !changed}
|
||||
variant='ghosted'
|
||||
onClick={() => revert()}
|
||||
onClick={revert}
|
||||
>
|
||||
Revert
|
||||
</Button>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import React, { useCallback, useEffect, useState } from 'react';
|
||||
import { ModalBody } from '@chakra-ui/modal';
|
||||
import { Input } from '@chakra-ui/react';
|
||||
import { getUserFields, postUserFields, userFieldsPlaceholder } from '../../app/api/ontimeApi';
|
||||
@@ -28,7 +28,7 @@ export default function TableOptionsModal() {
|
||||
/**
|
||||
* Validate and submit data
|
||||
*/
|
||||
const submitHandler = async (event) => {
|
||||
const submitHandler = useCallback(async (event) => {
|
||||
event.preventDefault();
|
||||
setSubmitting(true);
|
||||
|
||||
@@ -46,29 +46,29 @@ export default function TableOptionsModal() {
|
||||
}
|
||||
|
||||
setSubmitting(false);
|
||||
};
|
||||
},[refetch, userFields]);
|
||||
|
||||
/**
|
||||
* Reverts local state equals to server state
|
||||
*/
|
||||
const revert = async () => {
|
||||
const revert = useCallback(async () => {
|
||||
setChanged(false);
|
||||
await refetch();
|
||||
};
|
||||
},[refetch]);
|
||||
|
||||
/**
|
||||
* Handles change of input field in local state
|
||||
* @param {string} field - object parameter to update
|
||||
* @param {string} value - new object parameter value
|
||||
*/
|
||||
const handleChange = (field, value) => {
|
||||
const handleChange = useCallback((field, value) => {
|
||||
if (value.length < 30) {
|
||||
const temp = { ...userFields };
|
||||
temp[field] = value;
|
||||
setUserFields(temp);
|
||||
setChanged(true);
|
||||
}
|
||||
};
|
||||
},[userFields]);
|
||||
|
||||
return (
|
||||
<ModalBody className={style.modalBody}>
|
||||
|
||||
@@ -96,7 +96,7 @@ export default function OntimeTable({ tableData, userFields, handleUpdate, selec
|
||||
saveHiddenColumns([]);
|
||||
}, [saveHiddenColumns, toggleHideAllColumns]);
|
||||
|
||||
const handleOnDragEnd = (event) => {
|
||||
const handleOnDragEnd = useCallback((event) => {
|
||||
const { delta, active, over } = event;
|
||||
|
||||
// cancel if delta y is greater than 200
|
||||
@@ -121,7 +121,7 @@ export default function OntimeTable({ tableData, userFields, handleUpdate, selec
|
||||
|
||||
saveColumnOrder(cols);
|
||||
setColumnOrder(cols);
|
||||
};
|
||||
}, [columnOrder, hiddenColumns, saveColumnOrder, setColumnOrder]);
|
||||
|
||||
// save hidden columns object to local storage
|
||||
useEffect(() => {
|
||||
@@ -170,66 +170,66 @@ export default function OntimeTable({ tableData, userFields, handleUpdate, selec
|
||||
)}
|
||||
<table {...getTableProps()} className={style.ontimeTable}>
|
||||
<thead className={style.tableHeader}>
|
||||
{headerGroups.map((headerGroup) => {
|
||||
const { key, ...restHeaderGroupProps } = headerGroup.getHeaderGroupProps();
|
||||
return (
|
||||
<DndContext
|
||||
key={key}
|
||||
sensors={sensors}
|
||||
collisionDetection={closestCenter}
|
||||
onDragEnd={handleOnDragEnd}
|
||||
>
|
||||
<tr {...restHeaderGroupProps}>
|
||||
<th className={style.indexColumn}>
|
||||
<Tooltip label='Event Order' openDelay={300}>
|
||||
#
|
||||
</Tooltip>
|
||||
</th>
|
||||
<SortableContext
|
||||
key={key}
|
||||
items={headerGroup.headers}
|
||||
strategy={horizontalListSortingStrategy}
|
||||
>
|
||||
{headerGroup.headers.map((column) => {
|
||||
const { key } = column.getHeaderProps();
|
||||
return <SortableCell key={key} column={column} />;
|
||||
})}
|
||||
</SortableContext>
|
||||
</tr>
|
||||
</DndContext>
|
||||
);
|
||||
})}
|
||||
{headerGroups.map((headerGroup) => {
|
||||
const { key, ...restHeaderGroupProps } = headerGroup.getHeaderGroupProps();
|
||||
return (
|
||||
<DndContext
|
||||
key={key}
|
||||
sensors={sensors}
|
||||
collisionDetection={closestCenter}
|
||||
onDragEnd={handleOnDragEnd}
|
||||
>
|
||||
<tr {...restHeaderGroupProps}>
|
||||
<th className={style.indexColumn}>
|
||||
<Tooltip label='Event Order' openDelay={300}>
|
||||
#
|
||||
</Tooltip>
|
||||
</th>
|
||||
<SortableContext
|
||||
key={key}
|
||||
items={headerGroup.headers}
|
||||
strategy={horizontalListSortingStrategy}
|
||||
>
|
||||
{headerGroup.headers.map((column) => {
|
||||
const { key } = column.getHeaderProps();
|
||||
return <SortableCell key={key} column={column} />;
|
||||
})}
|
||||
</SortableContext>
|
||||
</tr>
|
||||
</DndContext>
|
||||
);
|
||||
})}
|
||||
</thead>
|
||||
<tbody {...getTableBodyProps} className={style.tableBody}>
|
||||
{/*This is saving in place of a default component*/}
|
||||
{/* eslint-disable-next-line array-callback-return */}
|
||||
{rows.map((row) => {
|
||||
prepareRow(row);
|
||||
const { key } = row.getRowProps();
|
||||
const type = row.original.type;
|
||||
if (type === 'event') {
|
||||
eventIndex++;
|
||||
return (
|
||||
<EventRow
|
||||
key={key}
|
||||
row={row}
|
||||
index={eventIndex}
|
||||
selectedId={selectedId}
|
||||
delay={cumulativeDelay}
|
||||
/>
|
||||
);
|
||||
}
|
||||
if (type === 'delay') {
|
||||
if (row.original.duration != null) {
|
||||
cumulativeDelay += row.original.duration;
|
||||
{/*This is saving in place of a default component*/}
|
||||
{/* eslint-disable-next-line array-callback-return */}
|
||||
{rows.map((row) => {
|
||||
prepareRow(row);
|
||||
const { key } = row.getRowProps();
|
||||
const type = row.original.type;
|
||||
if (type === 'event') {
|
||||
eventIndex++;
|
||||
return (
|
||||
<EventRow
|
||||
key={key}
|
||||
row={row}
|
||||
index={eventIndex}
|
||||
selectedId={selectedId}
|
||||
delay={cumulativeDelay}
|
||||
/>
|
||||
);
|
||||
}
|
||||
return <DelayRow key={key} row={row} />;
|
||||
}
|
||||
if (type === 'block') {
|
||||
cumulativeDelay = 0;
|
||||
return <BlockRow key={key} row={row} />;
|
||||
}
|
||||
})}
|
||||
if (type === 'delay') {
|
||||
if (row.original.duration != null) {
|
||||
cumulativeDelay += row.original.duration;
|
||||
}
|
||||
return <DelayRow key={key} row={row} />;
|
||||
}
|
||||
if (type === 'block') {
|
||||
cumulativeDelay = 0;
|
||||
return <BlockRow key={key} row={row} />;
|
||||
}
|
||||
})}
|
||||
</tbody>
|
||||
</table>
|
||||
</>
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
import React from 'react';
|
||||
import ProtectRoute from '../../common/components/protectRoute/ProtectRoute';
|
||||
import TableWrapper from './TableWrapper';
|
||||
import { TableSettingsProvider } from '../../app/context/TableSettingsContext';
|
||||
|
||||
export default function ProtectedTable() {
|
||||
return (
|
||||
<ProtectRoute>
|
||||
<TableSettingsProvider>
|
||||
<TableWrapper />
|
||||
</TableSettingsProvider>
|
||||
</ProtectRoute>
|
||||
);
|
||||
}
|
||||
@@ -163,14 +163,14 @@
|
||||
}
|
||||
|
||||
.selected > td {
|
||||
background-color: #4bffab99;
|
||||
background-color: rgba($ontime-accent, 0.8);
|
||||
}
|
||||
}
|
||||
|
||||
.indexColumn {
|
||||
font-weight: 200;
|
||||
text-align: right;
|
||||
width: 2em;
|
||||
width: 2.5em;
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
@@ -271,6 +271,12 @@ svg {
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
.check {
|
||||
font-size: 1.5em;
|
||||
background-color: transparent !important;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
@keyframes rotation {
|
||||
from {
|
||||
transform: rotate(0deg);
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, { useContext, useEffect, useState } from 'react';
|
||||
import React, { useCallback, useContext, useEffect, useState } from 'react';
|
||||
import { useFetch } from '../../app/hooks/useFetch';
|
||||
import { useSocket } from '../../app/context/socketContext';
|
||||
import { TableSettingsContext } from '../../app/context/TableSettingsContext';
|
||||
@@ -18,6 +18,11 @@ export default function TableWrapper() {
|
||||
const [selectedId, setSelectedId] = useState(null);
|
||||
const { theme } = useContext(TableSettingsContext);
|
||||
|
||||
// Set window title
|
||||
useEffect(() => {
|
||||
document.title = 'ontime - Cuesheet';
|
||||
}, []);
|
||||
|
||||
/**
|
||||
* Handle incoming data from socket
|
||||
*/
|
||||
@@ -38,7 +43,7 @@ export default function TableWrapper() {
|
||||
};
|
||||
}, [socket]);
|
||||
|
||||
const handleUpdate = async (rowIndex, accessor, payload) => {
|
||||
const handleUpdate = useCallback(async (rowIndex, accessor, payload) => {
|
||||
if (rowIndex == null || accessor == null || payload == null) {
|
||||
return;
|
||||
}
|
||||
@@ -71,7 +76,7 @@ export default function TableWrapper() {
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
};
|
||||
}, [mutation, tableData]);
|
||||
|
||||
if (typeof tableData === 'undefined' || typeof userFields === 'undefined') {
|
||||
return <span>loading...</span>;
|
||||
|
||||
@@ -2,6 +2,7 @@ import React from 'react';
|
||||
import { FiCheck } from '@react-icons/all-files/fi/FiCheck';
|
||||
import EditableCell from './tableElements/EditableCell';
|
||||
import { stringFromMillis } from '../../common/utils/time.js';
|
||||
import style from './Table.module.scss';
|
||||
|
||||
/**
|
||||
* React - Table column object
|
||||
@@ -13,7 +14,7 @@ export const makeColumns = (sizes, userFields) => {
|
||||
{
|
||||
Header: 'Public',
|
||||
accessor: 'isPublic',
|
||||
Cell: ({ cell: { value } }) => (value ? <FiCheck /> :""),
|
||||
Cell: ({ cell: { value } }) => (value ? <FiCheck className={style.check} /> : ''),
|
||||
width: sizes?.isPublic || 50,
|
||||
},
|
||||
{
|
||||
|
||||
+21
-22
@@ -1,4 +1,4 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import React, { useCallback, useEffect, useState } from 'react';
|
||||
import { AutoTextArea } from '../../../common/input/AutoTextArea';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
@@ -21,31 +21,30 @@ export default function EditableCell(props) {
|
||||
// We need to keep and update the state of the cell normally
|
||||
const [value, setValue] = useState(initialValue);
|
||||
|
||||
const onChange = (e) => {
|
||||
setValue(e.target.value);
|
||||
};
|
||||
const onChange = useCallback((e) => setValue(e.target.value), []);
|
||||
|
||||
// We'll only update the external data when the input is blurred
|
||||
const onBlur = () => {
|
||||
handleUpdate(index, id, value);
|
||||
};
|
||||
const onBlur = useCallback(() => handleUpdate(index, id, value), [handleUpdate, id, index, value]);
|
||||
|
||||
// If the initialValue is changed external, sync it up with our state
|
||||
useEffect(() => {
|
||||
setValue(initialValue);
|
||||
}, [initialValue]);
|
||||
|
||||
return (
|
||||
<AutoTextArea
|
||||
size='sm'
|
||||
borderColor='#0001'
|
||||
defaultValue={value}
|
||||
onChange={onChange}
|
||||
onBlur={onBlur}
|
||||
rows={3}
|
||||
transition='none'
|
||||
/>
|
||||
);
|
||||
// If the initialValue is changed external, sync it up with our state
|
||||
useEffect(() => {
|
||||
setValue(initialValue);
|
||||
}, [initialValue]);
|
||||
|
||||
return (
|
||||
<AutoTextArea
|
||||
size='sm'
|
||||
borderColor='#0001'
|
||||
defaultValue={value}
|
||||
onChange={onChange}
|
||||
onBlur={onBlur}
|
||||
rows={3}
|
||||
transition='none'
|
||||
spellCheck={false}
|
||||
autoComplete={false}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
EditableCell.propTypes = {
|
||||
@@ -6,7 +6,7 @@ import PropTypes from 'prop-types';
|
||||
import styles from '../Table.module.scss';
|
||||
|
||||
export default function SortableCell({ column }) {
|
||||
const { key, style, ...restColumn } = column.getHeaderProps();
|
||||
const { style, ...restColumn } = column.getHeaderProps();
|
||||
const { attributes, listeners, setNodeRef, transform, transition, isDragging } = useSortable({
|
||||
id: column.id,
|
||||
});
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import QRCode from 'react-qr-code';
|
||||
import { AnimatePresence, motion } from 'framer-motion';
|
||||
import { formatDisplay } from 'common/utils/dateConfig';
|
||||
import Paginator from 'common/components/views/Paginator';
|
||||
import NavLogo from 'common/components/nav/NavLogo';
|
||||
import { AnimatePresence, motion } from 'framer-motion';
|
||||
import TitleSide from 'common/components/views/TitleSide';
|
||||
import { getEventsWithDelay } from '../../../common/utils/eventsManager';
|
||||
import { titleVariants } from '../common/animation';
|
||||
import PropTypes from 'prop-types';
|
||||
import style from './StageManager.module.scss';
|
||||
|
||||
export default function StageManager(props) {
|
||||
@@ -137,3 +138,12 @@ export default function StageManager(props) {
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
StageManager.propTypes = {
|
||||
publ: PropTypes.object,
|
||||
title: PropTypes.object,
|
||||
time: PropTypes.object,
|
||||
backstageEvents: PropTypes.object,
|
||||
selectedId: PropTypes.string,
|
||||
general: PropTypes.object,
|
||||
};
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import QRCode from 'react-qr-code';
|
||||
import { AnimatePresence, motion } from 'framer-motion';
|
||||
import Paginator from 'common/components/views/Paginator';
|
||||
import NavLogo from 'common/components/nav/NavLogo';
|
||||
import { AnimatePresence, motion } from 'framer-motion';
|
||||
import TitleSide from 'common/components/views/TitleSide';
|
||||
import { titleVariants } from '../common/animation';
|
||||
import PropTypes from 'prop-types';
|
||||
import style from './Public.module.scss';
|
||||
|
||||
export default function Public(props) {
|
||||
@@ -124,3 +125,12 @@ export default function Public(props) {
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Public.propTypes = {
|
||||
publ: PropTypes.object,
|
||||
publicTitle: PropTypes.object,
|
||||
time: PropTypes.object,
|
||||
events: PropTypes.object,
|
||||
publicSelectedId: PropTypes.string,
|
||||
general: PropTypes.object,
|
||||
};
|
||||
|
||||
@@ -1,30 +0,0 @@
|
||||
import { AspectRatio } from '@chakra-ui/layout';
|
||||
import { CircularProgress } from '@chakra-ui/progress';
|
||||
import { useState } from 'react';
|
||||
import style from './IFrameLoader.module.css';
|
||||
|
||||
export default function IFrameLoader(props) {
|
||||
const [loading, setLoading] = useState(true);
|
||||
const { title, src } = props;
|
||||
|
||||
return (
|
||||
<AspectRatio maxW='300' ratio={16 / 9} className={style.iframeContainer}>
|
||||
<>
|
||||
{loading && (
|
||||
<CircularProgress
|
||||
className={style.loader}
|
||||
isIndeterminate
|
||||
color='orange.300'
|
||||
trackColor='#FFF0'
|
||||
/>
|
||||
)}
|
||||
<iframe
|
||||
className={style.iframe}
|
||||
title={title}
|
||||
src={src}
|
||||
onLoad={() => setLoading(false)}
|
||||
/>
|
||||
</>
|
||||
</AspectRatio>
|
||||
);
|
||||
}
|
||||
@@ -1,13 +0,0 @@
|
||||
.iframeContainer {
|
||||
margin: auto;
|
||||
background-color: #0001;
|
||||
border: 1px solid #0001;
|
||||
}
|
||||
|
||||
.loader {
|
||||
z-index: 100;
|
||||
}
|
||||
|
||||
.iframe {
|
||||
width: inherit;
|
||||
}
|
||||
@@ -1,11 +1,12 @@
|
||||
import React, { useEffect, useLayoutEffect, useRef, useState } from 'react';
|
||||
import QRCode from 'react-qr-code';
|
||||
import style from './Pip.module.scss';
|
||||
import { AnimatePresence, motion } from 'framer-motion';
|
||||
import Paginator from 'common/components/views/Paginator';
|
||||
import NavLogo from 'common/components/nav/NavLogo';
|
||||
import { AnimatePresence, motion } from 'framer-motion';
|
||||
import { formatDisplay } from 'common/utils/dateConfig';
|
||||
import { ReactComponent as Emptyimage } from 'assets/images/empty.svg';
|
||||
import PropTypes from 'prop-types';
|
||||
import style from './Pip.module.scss';
|
||||
|
||||
export default function Pip(props) {
|
||||
const { time, backstageEvents, selectedId, general } = props;
|
||||
@@ -121,3 +122,10 @@ export default function Pip(props) {
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Pip.propTypes = {
|
||||
time: PropTypes.object,
|
||||
backstageEvents: PropTypes.object,
|
||||
selectedId: PropTypes.string,
|
||||
general: PropTypes.object,
|
||||
};
|
||||
|
||||
@@ -20,7 +20,7 @@ export default function LowerClean(props) {
|
||||
|
||||
// Calculate time
|
||||
const fadeOutTime =
|
||||
(parseInt(options.fadeOut) +
|
||||
(parseInt(options.fadeOut, 10) +
|
||||
(options.transitionIn || defaults.transitionIn)) *
|
||||
1000;
|
||||
if (isNaN(fadeOutTime)) return;
|
||||
@@ -32,9 +32,6 @@ export default function LowerClean(props) {
|
||||
return () => clearTimeout(timeout);
|
||||
}, [options.fadeOut, options.transitionIn, defaults.transitionIn]);
|
||||
|
||||
// calculate transition times
|
||||
useEffect(() => {});
|
||||
|
||||
// Format messages
|
||||
const showLowerMessage = lower.text !== '' && lower.visible;
|
||||
|
||||
|
||||
@@ -19,7 +19,7 @@ export default function LowerLines(props) {
|
||||
|
||||
// Calculate time
|
||||
const fadeOutTime =
|
||||
(parseInt(options.fadeOut) +
|
||||
(parseInt(options.fadeOut, 10) +
|
||||
(options.transitionIn || defaults.transitionIn)) *
|
||||
1000;
|
||||
if (isNaN(fadeOutTime)) return;
|
||||
|
||||
@@ -69,7 +69,7 @@ const Lower = (props) => {
|
||||
|
||||
// preset: selector
|
||||
// Should be a number 1-n
|
||||
const p = parseInt(searchParams.get('preset'));
|
||||
const p = parseInt(searchParams.get('preset'), 10);
|
||||
if (!isNaN(p)) setPreset(p);
|
||||
|
||||
// size: multiplier
|
||||
@@ -79,7 +79,7 @@ const Lower = (props) => {
|
||||
|
||||
// transitionIn: seconds
|
||||
// Should be a number 0-n
|
||||
const t = parseInt(searchParams.get('transition'));
|
||||
const t = parseInt(searchParams.get('transition'), 10);
|
||||
if (!isNaN(t)) options.transitionIn = t;
|
||||
|
||||
// textColour: string
|
||||
@@ -99,17 +99,17 @@ const Lower = (props) => {
|
||||
|
||||
// fadeOut: seconds
|
||||
// Should be a number 0-n
|
||||
const f = parseInt(searchParams.get('fadeout'));
|
||||
const f = parseInt(searchParams.get('fadeout'), 10);
|
||||
if (!isNaN(f)) options.fadeOut = f;
|
||||
|
||||
// x: pixels
|
||||
// Should be a number 0-n
|
||||
const x = parseInt(searchParams.get('x'));
|
||||
const x = parseInt(searchParams.get('x'), 10);
|
||||
if (!isNaN(x)) options.posX = x;
|
||||
|
||||
// y: pixels
|
||||
// Should be a number 0-n
|
||||
const y = parseInt(searchParams.get('y'));
|
||||
const y = parseInt(searchParams.get('y'), 10);
|
||||
if (!isNaN(y)) options.posY = y;
|
||||
|
||||
setLowerOptions({
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import React, { useEffect } from 'react';
|
||||
import { formatDisplay } from '../../../common/utils/dateConfig';
|
||||
import NavLogo from '../../../common/components/nav/NavLogo';
|
||||
import PropTypes from 'prop-types';
|
||||
import style from './MinimalTimer.module.scss';
|
||||
|
||||
export default function MinimalTimer(props) {
|
||||
@@ -31,3 +32,8 @@ export default function MinimalTimer(props) {
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
MinimalTimer.propTypes = {
|
||||
pres: PropTypes.object,
|
||||
time: PropTypes.object,
|
||||
};
|
||||
|
||||
@@ -5,6 +5,7 @@ import Countdown from 'common/components/countdown/Countdown';
|
||||
import MyProgressBar from 'common/components/myProgressBar/MyProgressBar';
|
||||
import NavLogo from 'common/components/nav/NavLogo';
|
||||
import TitleCard from 'common/components/views/TitleCard';
|
||||
import PropTypes from 'prop-types';
|
||||
import style from './Timer.module.scss';
|
||||
|
||||
export default function Timer(props) {
|
||||
@@ -149,3 +150,10 @@ export default function Timer(props) {
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Timer.propTypes = {
|
||||
general: PropTypes.object,
|
||||
pres: PropTypes.object,
|
||||
title: PropTypes.object,
|
||||
time: PropTypes.object,
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user