* style: element relationship
* feat: utility to add first element
* refactor: organise dir
* ux: deleting element closes drawer
* fix: issue with escaped characters
* fix: issue with overflow in events list
* fix: prevent event timer from receiving non event
* style: prevent small shift on class change
* style: visually detach event editor
* fix: entry block knows last event
* fix: skip event timer updates on non events
* fix: prevent electron OPENGL error
* style: cleanup text styles
* style: unify input styles
* refactor: simplify style and composition
* refactor: tweaks on style + ts
* refactor: add validation to events post
* fix: handle non events in event timer
* refactor: cleanup unused
This commit is contained in:
Carlos Valente
2022-10-23 19:21:49 +02:00
committed by GitHub
parent d8ee0d4f82
commit 1b1aced296
30 changed files with 233 additions and 252 deletions
@@ -6,19 +6,25 @@ import {
AlertDialogFooter, AlertDialogFooter,
AlertDialogHeader, AlertDialogHeader,
AlertDialogOverlay, AlertDialogOverlay,
Button, IconButton, Tooltip, Button,
IconButton,
Tooltip,
} from '@chakra-ui/react'; } from '@chakra-ui/react';
import { FiPower } from '@react-icons/all-files/fi/FiPower'; import { FiPower } from '@react-icons/all-files/fi/FiPower';
import PropTypes from 'prop-types';
import { LoggingContext } from '../../context/LoggingContext'; import { LoggingContext } from '../../context/LoggingContext';
import { Size } from '../../models/UtilTypes';
export default function QuitIconBtn(props) { interface QuitIconBtnProps {
clickHandler: () => void;
size?: Size;
}
export default function QuitIconBtn(props: QuitIconBtnProps) {
const { clickHandler, size = 'lg', ...rest } = props; const { clickHandler, size = 'lg', ...rest } = props;
const [isOpen, setIsOpen] = useState(false); const [isOpen, setIsOpen] = useState(false);
const { emitInfo } = useContext(LoggingContext); const { emitInfo } = useContext(LoggingContext);
const onClose = () => setIsOpen(false); const onClose = () => setIsOpen(false);
const cancelRef = useRef(); const cancelRef = useRef<HTMLButtonElement | null>(null);
useEffect(() => { useEffect(() => {
if (window.process?.type === 'renderer') { if (window.process?.type === 'renderer') {
@@ -38,6 +44,7 @@ export default function QuitIconBtn(props) {
<> <>
<Tooltip label='Quit Application'> <Tooltip label='Quit Application'>
<IconButton <IconButton
aria-label='Quit Application'
size={size} size={size}
icon={<FiPower />} icon={<FiPower />}
colorScheme='red' colorScheme='red'
@@ -58,7 +65,7 @@ export default function QuitIconBtn(props) {
This will shutdown the program and all running servers. Are you sure? This will shutdown the program and all running servers. Are you sure?
</AlertDialogBody> </AlertDialogBody>
<AlertDialogFooter> <AlertDialogFooter>
<Button ref={cancelRef} onClick={onClose}> <Button ref={cancelRef} onClick={onClose} variant='ghost'>
Cancel Cancel
</Button> </Button>
<Button colorScheme='red' onClick={handleShutdown} ml={3}> <Button colorScheme='red' onClick={handleShutdown} ml={3}>
@@ -71,8 +78,3 @@ export default function QuitIconBtn(props) {
</> </>
); );
} }
QuitIconBtn.propTypes = {
clickHandler: PropTypes.func,
size: PropTypes.oneOf(['xs', 'sm', 'md', 'lg']),
};
@@ -133,9 +133,10 @@ export default function TimeInput(props) {
size='sm' size='sm'
icon={<IoLink style={{ transform: 'rotate(-45deg)' }} />} icon={<IoLink style={{ transform: 'rotate(-45deg)' }} />}
aria-label='automate' aria-label='automate'
colorScheme='whiteAlpha' colorScheme='blue'
style={{ borderRadius: '2px', width: 'min-content' }} style={{ borderRadius: '2px', width: 'min-content' }}
tabIndex={-1} tabIndex={-1}
variant='ghost'
/> />
</InputLeftElement> </InputLeftElement>
<Input <Input
@@ -1,6 +1,6 @@
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
import { useSocket } from './socketContext'; import { useSocket } from '../context/socketContext';
export default function useSubscription<T>(topic: string, initialState: T, requestString?: string) { export default function useSubscription<T>(topic: string, initialState: T, requestString?: string) {
const socket = useSocket(); const socket = useSocket();
+3
View File
@@ -3,10 +3,13 @@ declare module '*.scss' {
export default content; export default content;
} }
type ListenerType = (event: 'string', args: unknown[]) => void;
declare global { declare global {
interface Window { interface Window {
ipcRenderer: { ipcRenderer: {
send: (channel: string, args?: string | object) => void; send: (channel: string, args?: string | object) => void;
on: (channel: string, listener: ListenerType) => void;
}; };
process: { process: {
type: string; type: string;
@@ -1,7 +1,7 @@
import { useEffect, useState } from 'react'; import { Input } from '@chakra-ui/react';
import { Editable, EditableInput, EditablePreview, IconButton, Tooltip } from '@chakra-ui/react';
import { IoSunny } from '@react-icons/all-files/io5/IoSunny'; import { IoSunny } from '@react-icons/all-files/io5/IoSunny';
import TooltipActionBtn from '../../../common/components/buttons/TooltipActionBtn';
import { tooltipDelayMid } from '../../../ontimeConfig'; import { tooltipDelayMid } from '../../../ontimeConfig';
import style from './MessageControl.module.scss'; import style from './MessageControl.module.scss';
@@ -17,44 +17,32 @@ interface InputRowProps {
export default function InputRow(props: InputRowProps) { export default function InputRow(props: InputRowProps) {
const { label, placeholder, text, visible, actionHandler, changeHandler } = props; const { label, placeholder, text, visible, actionHandler, changeHandler } = props;
const [inputText, setInputText] = useState<string>(text || '');
const handleInputChange = (newValue: string) => { const handleInputChange = (newValue: string) => {
setInputText(newValue);
changeHandler(newValue); changeHandler(newValue);
}; };
useEffect(() => {
if (text) {
setInputText(text);
}
}, [text]);
return ( return (
<div className={`${visible ? style.inputRowActive : ''}`}> <div className={`${visible ? style.inputRowActive : ''}`}>
<span className={style.label}>{label}</span> <span className={style.label}>{label}</span>
<div className={style.inputItems}> <div className={style.inputItems}>
<Editable <Input
onChange={(newValue) => handleInputChange(newValue)} size='sm'
value={inputText} variant='filled'
value={text}
onChange={(event) => handleInputChange(event.target.value)}
placeholder={placeholder} placeholder={placeholder}
className={style.inline} />
color={text === '' ? '#505050' : 'inherit'} <TooltipActionBtn
> clickHandler={() => actionHandler('update', { field: 'isPublic', value: !visible })}
<EditablePreview className={`${style.padleft} ${style.fullWidth}`} /> tooltip={visible ? 'Make invisible' : 'Make visible'}
<EditableInput className={style.padleft} /> aria-label='Toggle tooltip visibility'
</Editable> openDelay={tooltipDelayMid}
<Tooltip label={visible ? 'Make invisible' : 'Make visible'} openDelay={tooltipDelayMid}> icon={<IoSunny size='18px' />}
<IconButton colorScheme='blue'
aria-label='Toggle visibility' variant={visible ? 'solid' : 'outline'}
size='sm' size='sm'
icon={<IoSunny size='18px' />} />
colorScheme='blue'
variant={visible ? 'solid' : 'outline'}
onClick={() => actionHandler('update', { field: 'isPublic', value: !visible })}
/>
</Tooltip>
</div> </div>
</div> </div>
); );
@@ -24,30 +24,10 @@
color: $label-gray; color: $label-gray;
} }
.inline {
border-radius: 4px;
background-color: $input-bg;
border: $input-border;
overflow-x: hidden;
}
.padleft {
padding-left: 0.5em;
border-radius: 2px;
}
.fullWidth {
width: 100%;
}
.inputRowActive { .inputRowActive {
.label { .label {
color: $action-blue; color: $action-blue;
} }
.inline {
border-color: $action-blue;
}
} }
} }
@@ -132,7 +132,7 @@
.eventEditor { .eventEditor {
border-radius: 3px 3px 0 0; border-radius: 3px 3px 0 0;
background-color: $bg-container-l1; background-color: $bg-container-l1;
border-top: $border-l1; border-top: 1px solid $bg-gray-900;
position: absolute; position: absolute;
bottom: 0; bottom: 0;
width: 100vw; width: 100vw;
@@ -160,10 +160,10 @@
.editor { .editor {
grid-area: even; grid-area: even;
height: calc(100% - 3em); height: calc(100% - 24px);
.content { .content {
height: calc(100% - 3em); height: calc(100% - 24px);
overflow: hidden; overflow: hidden;
} }
} }
@@ -175,7 +175,7 @@
.content { .content {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
height: calc(100% - 3em); height: calc(100% - 24px);
overflow: hidden; overflow: hidden;
} }
} }
@@ -5,7 +5,6 @@ $clearance: 8px;
$block-border-radius: 3px; $block-border-radius: 3px;
@mixin block-spacing() { @mixin block-spacing() {
margin: 4px 1px;
padding: 4px 10px 4px 2px; padding: 4px 10px 4px 2px;
gap: 2px; gap: 2px;
} }
@@ -1,24 +1,14 @@
@use '../../../theme/main' as *; @use '../../../theme/main' as *;
@mixin when-visible() { .create {
opacity: 1; background-color: $bg-container-l2;
height: calc(2.5em + 20px);
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
gap: 5%; gap: 5%;
margin: 2px 0; margin: 4px 2px 2px;
}
.create {
box-sizing: border-box;
width: 100%;
font-size: 12px; font-size: 12px;
position: relative;
display: none;
.createEvent, .createEvent,
.createDelay, .createDelay,
.createBlock { .createBlock {
@@ -32,69 +22,63 @@
border-radius: 2px; border-radius: 2px;
opacity: 0.6; opacity: 0.6;
cursor: pointer; cursor: pointer;
.keyboard {
margin-left: 4px;
padding: 0 4px;
color: #ccc;
border-radius: 2px;
font-family: Monospaced, sans-serif;
background-color: rgba(0, 0, 0, 0.24);
}
} }
.createEvent { .createEvent {
border: 1px solid #2b6cb0; border: 1px solid $light-bg;
color: lighten(#2b6cb0, 30%); color: lighten($light-bg, 30%);
&:hover { &:hover {
background-color: #2b6cb0; background-color: $light-bg;
opacity: 1; opacity: 1;
} }
} }
.createDelay { .createDelay {
border: 1px solid #ecc94b;; border: 1px solid $block-delay-color;
color: lighten(#ecc94b, 30%); color: lighten($block-delay-color, 30%);
&:hover { &:hover {
background-color: #ecc94b; background-color: $block-delay-color;
opacity: 1; opacity: 1;
} }
} }
.createBlock { .createBlock {
border: 1px solid #805ad5; border: 1px solid $block-block-color;
color: lighten(#805ad5, 30%); color: lighten($block-block-color, 30%);
&:hover { &:hover {
background-color: #805ad5; background-color: $block-block-color;
opacity: 1; opacity: 1;
} }
} }
}
.options { .keyboard {
display: flex; margin-left: 4px;
flex-direction: column; padding: 0 4px;
color: $label-gray;
border-radius: 2px;
background-color: rgba(0, 0, 0, 0.24);
}
label { .options {
opacity: 0.65; display: flex;
transition: opacity 0.15s; flex-direction: column;
&:hover { label {
opacity: 1; opacity: 0.65;
} transition: opacity 0.15s;
&:hover {
opacity: 1;
} }
}
.disabled {
border-color: $text-gray-disabled;
color: $text-gray-disabled;
pointer-events: none;
} }
} }
.create.visible { .disabled {
@include when-visible; border-color: $text-gray-disabled;
} color: $text-gray-disabled;
pointer-events: none;
}
@@ -1,4 +1,4 @@
import { useCallback, useContext, useEffect, useState } from 'react'; import { useCallback, useContext, useRef } from 'react';
import { Checkbox, Tooltip } from '@chakra-ui/react'; import { Checkbox, Tooltip } from '@chakra-ui/react';
import { defaultPublicAtom, startTimeIsLastEndAtom } from 'common/atoms/LocalEventSettings'; import { defaultPublicAtom, startTimeIsLastEndAtom } from 'common/atoms/LocalEventSettings';
import { LoggingContext } from 'common/context/LoggingContext'; import { LoggingContext } from 'common/context/LoggingContext';
@@ -13,7 +13,7 @@ import style from './EntryBlock.module.scss';
interface EntryBlockProps { interface EntryBlockProps {
showKbd: boolean; showKbd: boolean;
previousId?: string; previousId?: string;
visible?: boolean; previousEventId: string | null;
disableAddDelay?: boolean; disableAddDelay?: boolean;
disableAddBlock: boolean; disableAddBlock: boolean;
} }
@@ -22,7 +22,7 @@ export default function EntryBlock(props: EntryBlockProps) {
const { const {
showKbd, showKbd,
previousId, previousId,
visible = true, previousEventId,
disableAddDelay = true, disableAddDelay = true,
disableAddBlock, disableAddBlock,
} = props; } = props;
@@ -30,14 +30,17 @@ export default function EntryBlock(props: EntryBlockProps) {
const { emitError } = useContext(LoggingContext); const { emitError } = useContext(LoggingContext);
const startTimeIsLastEnd = useAtomValue(startTimeIsLastEndAtom); const startTimeIsLastEnd = useAtomValue(startTimeIsLastEndAtom);
const defaultPublic = useAtomValue(defaultPublicAtom); const defaultPublic = useAtomValue(defaultPublicAtom);
const [doStartTime, setStartTime] = useState(startTimeIsLastEnd); const doStartTime = useRef<HTMLInputElement | null>(null);
const [doPublic, setPublic] = useState(defaultPublic); const doPublic = useRef<HTMLInputElement | null>(null)
const handleCreateEvent = useCallback((eventType: EventTypes) => { const handleCreateEvent = useCallback((eventType: EventTypes) => {
switch (eventType) { switch (eventType) {
case 'event': { case 'event': {
const newEvent = { type: 'event', after: previousId, isPublic: doPublic }; const isPublicOption = doPublic?.current?.checked || defaultPublic;
const options = { startIsLastEnd: doStartTime ? previousId : undefined }; const startTimeIsLastEndOption = doStartTime?.current?.checked || doStartTime;
const newEvent = { type: 'event', after: previousId, isPublic: isPublicOption };
const options = { startIsLastEnd: startTimeIsLastEndOption ? previousEventId : undefined };
addEvent(newEvent, options); addEvent(newEvent, options);
break; break;
} }
@@ -55,18 +58,10 @@ export default function EntryBlock(props: EntryBlockProps) {
} }
} }
}, [addEvent, doPublic, doStartTime, emitError, previousId]); }, [addEvent, doPublic, doStartTime, emitError, previousId, previousEventId]);
useEffect(() => {
setStartTime(startTimeIsLastEnd);
}, [startTimeIsLastEnd]);
useEffect(() => {
setPublic(defaultPublic);
}, [defaultPublic]);
return ( return (
<div className={`${style.create} ${visible ? style.visible : ''}`}> <div className={style.create}>
<Tooltip label='Add Event' openDelay={tooltipDelayMid}> <Tooltip label='Add Event' openDelay={tooltipDelayMid}>
<span <span
className={style.createEvent} className={style.createEvent}
@@ -96,18 +91,18 @@ export default function EntryBlock(props: EntryBlockProps) {
</Tooltip> </Tooltip>
<div className={style.options}> <div className={style.options}>
<Checkbox <Checkbox
ref={doStartTime}
size='sm' size='sm'
colorScheme='blue' colorScheme='blue'
isChecked={doStartTime} defaultChecked={startTimeIsLastEnd}
onChange={(e) => setStartTime(e.target.checked)}
> >
Start time is last end Start time is last end
</Checkbox> </Checkbox>
<Checkbox <Checkbox
ref={doPublic}
size='sm' size='sm'
colorScheme='blue' colorScheme='blue'
isChecked={doPublic} defaultChecked={defaultPublic}
onChange={(e) => setPublic(e.target.checked)}
> >
Event is public Event is public
</Checkbox> </Checkbox>
@@ -12,7 +12,7 @@
grid-template-columns: $binder-width auto 1fr auto; grid-template-columns: $binder-width auto 1fr auto;
grid-template-rows: 36px 36px 36px $element-spacing; grid-template-rows: 36px 36px 36px $element-spacing;
align-items: center; align-items: center;
margin: 2px 1px; margin: 2px 2px;
padding-right: $clearance; padding-right: $clearance;
// style - general // style - general
@@ -34,7 +34,6 @@ export default function EventBlockTimers(props) {
const handleSubmit = useCallback( const handleSubmit = useCallback(
(field, value) => { (field, value) => {
console.log('called update', field, value)
actionHandler('update', { field, value }); actionHandler('update', { field, value });
}, },
[actionHandler] [actionHandler]
+11 -2
View File
@@ -14,7 +14,7 @@ import { duplicateEvent } from 'common/utils/eventsManager';
import { useAtomValue } from 'jotai'; import { useAtomValue } from 'jotai';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import useSubscription from '../../../common/context/useSubscription'; import useSubscription from '../../../common/hooks/useSubscription';
import EntryBlock from '../entry-block/EntryBlock'; import EntryBlock from '../entry-block/EntryBlock';
import EventListItem from './EventListItem'; import EventListItem from './EventListItem';
@@ -170,7 +170,13 @@ export default function EventList(props) {
return ( return (
<div className={style.alignCenter}> <div className={style.alignCenter}>
<Empty text='No Events' style={{ marginTop: '7vh' }} /> <Empty text='No Events' style={{ marginTop: '7vh' }} />
<Button variant='solid' colorScheme='blue'>Create Event</Button> <Button
onClick={() => insertAtCursor('event', cursor)}
variant='solid'
colorScheme='blue'
>
Create Event
</Button>
</div> </div>
); );
} }
@@ -178,6 +184,7 @@ export default function EventList(props) {
let eventIndex = -1; let eventIndex = -1;
let previousEnd = 0; let previousEnd = 0;
let thisEnd = 0; let thisEnd = 0;
let previousEventId = null;
return ( return (
<div className={style.eventContainer}> <div className={style.eventContainer}>
@@ -198,6 +205,7 @@ export default function EventList(props) {
eventIndex++; eventIndex++;
previousEnd = thisEnd; previousEnd = thisEnd;
thisEnd = e.timeEnd; thisEnd = e.timeEnd;
previousEventId = e.id;
} }
const isLast = index === events.length - 1; const isLast = index === events.length - 1;
return ( return (
@@ -226,6 +234,7 @@ export default function EventList(props) {
<EntryBlock <EntryBlock
showKbd={index === cursor} showKbd={index === cursor}
previousId={e.id} previousId={e.id}
previousEventId={previousEventId}
disableAddDelay={e.type === 'delay'} disableAddDelay={e.type === 'delay'}
disableAddBlock={e.type === 'block'} disableAddBlock={e.type === 'block'}
/> />
@@ -1,12 +1,16 @@
import { useCallback, useContext } from 'react'; import { useCallback, useContext } from 'react';
import { defaultPublicAtom, startTimeIsLastEndAtom } from 'common/atoms/LocalEventSettings'; import {
defaultPublicAtom,
editorEventId,
startTimeIsLastEndAtom,
} from 'common/atoms/LocalEventSettings';
import { LoggingContext } from 'common/context/LoggingContext'; import { LoggingContext } from 'common/context/LoggingContext';
import { useEventAction } from 'common/hooks/useEventAction'; import { useEventAction } from 'common/hooks/useEventAction';
import { OntimeEvent, OntimeEventEntry } from 'common/models/EventTypes'; import { OntimeEvent, OntimeEventEntry } from 'common/models/EventTypes';
import { Playstate } from 'common/models/OntimeTypes'; import { Playstate } from 'common/models/OntimeTypes';
import { duplicateEvent } from 'common/utils/eventsManager'; import { duplicateEvent } from 'common/utils/eventsManager';
import { calculateDuration } from 'common/utils/timesManager'; import { calculateDuration } from 'common/utils/timesManager';
import { useAtomValue } from 'jotai'; import { useAtom, useAtomValue } from 'jotai';
import { CursorContext } from '../../../common/context/CursorContext'; import { CursorContext } from '../../../common/context/CursorContext';
import BlockBlock from '../block-block/BlockBlock'; import BlockBlock from '../block-block/BlockBlock';
@@ -40,6 +44,7 @@ export default function EventListItem(props: EventListItemProps) {
const defaultPublic = useAtomValue(defaultPublicAtom); const defaultPublic = useAtomValue(defaultPublicAtom);
const { addEvent, updateEvent, deleteEvent } = useEventAction(); const { addEvent, updateEvent, deleteEvent } = useEventAction();
const { moveCursorTo } = useContext(CursorContext); const { moveCursorTo } = useContext(CursorContext);
const [openId, setOpenId] = useAtom(editorEventId);
// Create / delete new events // Create / delete new events
type FieldValue = { type FieldValue = {
@@ -75,6 +80,9 @@ export default function EventListItem(props: EventListItemProps) {
} }
case 'delete': { case 'delete': {
deleteEvent(data.id); deleteEvent(data.id);
if (openId === data.id) {
setOpenId(null);
}
break; break;
} }
case 'clone': { case 'clone': {
@@ -20,13 +20,15 @@ export default function EventListWrapper() {
}, [emitError, isError]); }, [emitError, isError]);
return ( return (
<div className={styles.content}> <>
<EventListMenu /> <EventListMenu />
{status === 'success' && data ? ( <div className={styles.content}>
<EventList events={data} /> {status === 'success' && data ? (
) : ( <EventList events={data} />
<Empty text='Connecting to server' /> ) : (
)} <Empty text='Connecting to server' />
</div> )}
</div>
</>
); );
} }
@@ -145,10 +145,6 @@ export default function EventEditor() {
<label className={style.inputLabel}>Title</label> <label className={style.inputLabel}>Title</label>
<TextInput field='title' initialText={event.title} submitHandler={handleSubmit} /> <TextInput field='title' initialText={event.title} submitHandler={handleSubmit} />
</div> </div>
<div className={style.column}>
<label className={style.inputLabel}>Subtitle</label>
<TextInput field='subtitle' initialText={event.subtitle} submitHandler={handleSubmit} />
</div>
<div className={style.column}> <div className={style.column}>
<label className={style.inputLabel}>Presenter</label> <label className={style.inputLabel}>Presenter</label>
<TextInput <TextInput
@@ -157,6 +153,10 @@ export default function EventEditor() {
submitHandler={handleSubmit} submitHandler={handleSubmit}
/> />
</div> </div>
<div className={style.column}>
<label className={style.inputLabel}>Subtitle</label>
<TextInput field='subtitle' initialText={event.subtitle} submitHandler={handleSubmit} />
</div>
<div className={style.padTop}> <div className={style.padTop}>
<Button <Button
leftIcon={<FiUsers />} leftIcon={<FiUsers />}
@@ -190,7 +190,7 @@ export default function EventEditor() {
</div> </div>
</div> </div>
<div className={style.column}> <div className={style.column}>
<label className={style.inputLabel}>Notes</label> <label className={style.inputLabel}>Note</label>
<TextInput <TextInput
field='note' field='note'
initialText={event.note} initialText={event.note}
+2 -2
View File
@@ -25,14 +25,14 @@ export default function Info() {
const selected = !data.numEvents const selected = !data.numEvents
? 'No events' ? 'No events'
: `Event ${data.selectedEventIndex != null ? data.selectedEventIndex + 1 : '-'}/${ : `Event ${data.selectedEventIndex != null ? data.selectedEventIndex + 1 : '-'} / ${
data.numEvents ? data.numEvents : '-' data.numEvents ? data.numEvents : '-'
}`; }`;
return ( return (
<> <>
<div className={style.main}> <div className={style.main}>
<span>Running on port 4001</span> <span>Ontime running on port 4001</span>
<span>{selected}</span> <span>{selected}</span>
</div> </div>
<InfoNif /> <InfoNif />
+1 -2
View File
@@ -9,13 +9,12 @@
.container { .container {
@include container; @include container;
border: 1px solid transparent;
} }
.main { .main {
font-size: 0.9em; font-size: 0.9em;
color: $ontime-pink; color: $label-gray;
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
} }
@@ -2,4 +2,5 @@
align-content: center; align-content: center;
justify-content: space-between; justify-content: space-between;
gap: 8px; gap: 8px;
padding-top: 24px;
} }
+1 -1
View File
@@ -2,7 +2,7 @@
import { useEffect, useMemo, useState } from 'react'; import { useEffect, useMemo, useState } from 'react';
import { useSocket } from '../../common/context/socketContext'; import { useSocket } from '../../common/context/socketContext';
import useSubscription from '../../common/context/useSubscription'; import useSubscription from '../../common/hooks/useSubscription';
import useEvent from '../../common/hooks-query/useEvent'; import useEvent from '../../common/hooks-query/useEvent';
import useEventsList from '../../common/hooks-query/useEventsList'; import useEventsList from '../../common/hooks-query/useEventsList';
import useViewSettings from '../../common/hooks-query/useViewSettings'; import useViewSettings from '../../common/hooks-query/useViewSettings';
@@ -110,6 +110,13 @@
transition: 0.5s; transition: 0.5s;
transition-property: opacity; transition-property: opacity;
} }
.message {
font-size: 1.5vw;
line-height: 2vw;
white-space: pre-line;
overflow: hidden;
}
} }
.clock-container { .clock-container {
+2
View File
@@ -60,6 +60,7 @@ let tray = null;
// Ensure there isn't another instance of the app running already // Ensure there isn't another instance of the app running already
const lock = app.requestSingleInstanceLock(); const lock = app.requestSingleInstanceLock();
if (!lock) { if (!lock) {
dialog.showErrorBox('Multiple instances', 'An instance if the App is already running.'); dialog.showErrorBox('Multiple instances', 'An instance if the App is already running.');
app.quit(); app.quit();
@@ -111,6 +112,7 @@ function createWindow() {
win.setMenu(null); win.setMenu(null);
} }
app.disableHardwareAcceleration();
app.whenReady().then(() => { app.whenReady().then(() => {
// Set app title in windows // Set app title in windows
if (process.platform === 'win32') { if (process.platform === 'win32') {
+1 -1
View File
@@ -141,7 +141,7 @@ export const startServer = async (overrideConfig = null) => {
// init timer // init timer
global.timer = new EventTimer(socket, config.timer, oscConfig, http); global.timer = new EventTimer(socket, config.timer, oscConfig, http);
global.timer.setupWithEventList(events); global.timer.setupWithEventList(events.filter((entry) => entry.type === 'event'));
socket.info('SERVER', returnMessage); socket.info('SERVER', returnMessage);
socket.startListener(); socket.startListener();
@@ -95,7 +95,9 @@ export class DataProvider {
*/ */
static async insertEventAfterId(entry, id) { static async insertEventAfterId(entry, id) {
const index = [...data.events].findIndex((event) => event.id === id); const index = [...data.events].findIndex((event) => event.id === id);
await DataProvider.insertEventAt(entry, index + 1); // eslint-disable-next-line no-unused-vars
const { _after, ...sanitisedEvent } = entry;
await DataProvider.insertEventAt(sanitisedEvent, index + 1);
} }
static getSettings() { static getSettings() {
+34 -44
View File
@@ -772,40 +772,45 @@ export class EventTimer extends Timer {
* @param previousId * @param previousId
*/ */
insertEventAfterId(event, previousId) { insertEventAfterId(event, previousId) {
// find object in events if (typeof previousId === 'undefined') {
const previousIndex = this._eventlist.findIndex((e) => e.id === previousId); // Insert at beginning
if (previousIndex === -1) { this._eventlist.unshift(event);
throw 'Event not found';
}
if (previousIndex + 1 >= this._eventlist.length) {
this._eventlist.push(event);
} else { } else {
this._eventlist.splice(previousIndex + 1, 0, event); // find object in events
} const previousIndex = this._eventlist.findIndex((e) => e.id === previousId);
if (previousIndex === -1) {
throw 'Event not found';
}
try { if (previousIndex + 1 >= this._eventlist.length) {
// check if entry is running this._eventlist.push(event);
if (event.id === this.selectedEventId) { } else {
// handle reload selected this._eventlist.splice(previousIndex + 1, 0, event);
// Reload data if running }
const type =
this.selectedEventId === event.id && this._startedAt != null ? 'reload' : 'load'; try {
this.loadEvent(this.selectedEventIndex, type); // check if entry is running
} else if (event.id === this.nextEventId) { if (event.id === this.selectedEventId) {
// roll needs to recalculate // handle reload selected
if (this.state === 'roll') { // Reload data if running
this.rollLoad(); const type =
this.selectedEventId === event.id && this._startedAt != null ? 'reload' : 'load';
this.loadEvent(this.selectedEventIndex, type);
} else if (event.id === this.nextEventId) {
// roll needs to recalculate
if (this.state === 'roll') {
this.rollLoad();
}
} }
}
// load titles // load titles
if ('title' in event || 'subtitle' in event || 'presenter' in event) { if ('title' in event || 'subtitle' in event || 'presenter' in event) {
this._loadTitlesNext(); this._loadTitlesNext();
this._loadTitlesNow(); this._loadTitlesNow();
}
} catch (error) {
this.socket.error('SERVER', error);
} }
} catch (error) {
this.socket.error('SERVER', error);
} }
// update clients // update clients
@@ -815,21 +820,6 @@ export class EventTimer extends Timer {
this.runCycle(); this.runCycle();
} }
/**
* @description inserts an event in the first position of the list
* @param event
*/
insertEventAtStart(event) {
// Insert at beginning
this._eventlist.unshift(event);
// update clients
this.broadcastState();
// run cycle
this.runCycle();
}
/** /**
* Deleted an event from the list by its id * Deleted an event from the list by its id
* @param {string} eventId * @param {string} eventId
@@ -1,11 +1,11 @@
import { body, validationResult } from 'express-validator'; import { body, validationResult } from 'express-validator';
export const eventSanitizer = [ export const eventSanitizer = [
body('title').optional().isString().trim().escape(), body('title').optional().isString().trim(),
body('url').optional().isString().trim().escape(), body('url').optional().isString().trim(),
body('publicInfo').optional().isString().trim().escape(), body('publicInfo').optional().isString().trim(),
body('backstageInfo').optional().isString().trim().escape(), body('backstageInfo').optional().isString().trim(),
body('endMessage').optional().isString().trim().escape(), body('endMessage').optional().isString().trim(),
(req, res, next) => { (req, res, next) => {
const errors = validationResult(req); const errors = validationResult(req);
if (!errors.isEmpty()) return res.status(422).json({ errors: errors.array() }); if (!errors.isEmpty()) return res.status(422).json({ errors: errors.array() });
+32 -32
View File
@@ -14,17 +14,20 @@ import { socketProvider } from '../classes/socket/SocketController.js';
const socket = socketProvider; const socket = socketProvider;
async function _insertAndSync(newEvent) { async function _insertAndSync(newEvent) {
if (newEvent.order) { const afterId = newEvent?.after;
const events = DataProvider.getEvents(); if (typeof afterId === 'undefined') {
await DataProvider.insertEventAt(newEvent, newEvent.order);
const previousId = events?.[newEvent.order - 1]?.id;
_insertEventInTimerAfterId(newEvent, previousId);
} else if (newEvent.after) {
await DataProvider.insertEventAfterId(newEvent, newEvent.after);
_insertEventInTimerAfterId(newEvent, newEvent.after);
} else {
await DataProvider.insertEventAt(newEvent, 0); await DataProvider.insertEventAt(newEvent, 0);
_insertEventInTimerAfterId(newEvent); if (newEvent.type === 'event') {
_insertEventInTimerAfterId(newEvent);
}
} else {
delete newEvent.after;
await DataProvider.insertEventAfterId(newEvent, afterId);
if (newEvent.type === 'event') {
const events = DataProvider.getEvents();
const { id } = getPreviousPlayable(events, newEvent.id);
_insertEventInTimerAfterId(newEvent, id);
}
} }
} }
@@ -51,14 +54,10 @@ function _updateTimers() {
* @private * @private
*/ */
function _insertEventInTimerAfterId(event, previousId) { function _insertEventInTimerAfterId(event, previousId) {
if (typeof previousId === 'undefined') { try {
global.timer.insertEventAtStart(event); global.timer.insertEventAfterId(event, previousId);
} else { } catch (error) {
try { socket.error('SERVER', `Unable to update object: ${error}`);
global.timer.insertEventAfterId(event, previousId);
} catch (error) {
socket.error('SERVER', `Unable to update object: ${error}`);
}
} }
} }
@@ -147,28 +146,29 @@ export const eventsPut = async (req, res) => {
const eventDataFromRequest = req.body; const eventDataFromRequest = req.body;
const eventId = eventDataFromRequest.id; const eventId = eventDataFromRequest.id;
const event = DataProvider.getEventById(eventId); const eventInMemory = DataProvider.getEventById(eventId);
if (typeof event === 'undefined') { if (typeof eventInMemory === 'undefined') {
res.status(400).send(`No event with ID found`); res.status(400).send(`No event with ID found`);
return; return;
} }
try { try {
const newData = await DataProvider.updateEventById(eventId, eventDataFromRequest); const patchedObject = await DataProvider.updateEventById(eventId, eventDataFromRequest);
if (newData.skip) { if (patchedObject.type === 'event') {
_deleteTimerId(eventId); if (patchedObject.skip) {
// if it is a skip, make sure it is deleted from timer // if it is a skip, make sure it is deleted from timer
// event id might already not exist _deleteTimerId(patchedObject.id);
} else { } else {
try { if (eventInMemory.skip) {
_updateTimersSingle(newData.id, eventDataFromRequest); // if it was skipped before we add it to the timer
} catch (error) {
if (error === 'Event not found') {
const events = DataProvider.getEvents(); const events = DataProvider.getEvents();
const { id: previousId } = getPreviousPlayable(events, newData.id); const { id } = getPreviousPlayable(events, patchedObject.id);
_insertEventInTimerAfterId(newData, previousId); _insertEventInTimerAfterId(patchedObject, id);
} else {
// otherwise update as normal
_updateTimersSingle(patchedObject.id, patchedObject);
} }
} }
} }
@@ -1,5 +1,14 @@
import { body, param, validationResult } from 'express-validator'; import { body, param, validationResult } from 'express-validator';
export const eventsPostValidator = [
body('type').isString().exists().isIn(['event', 'delay', 'block']),
(req, res, next) => {
const errors = validationResult(req);
if (!errors.isEmpty()) return res.status(422).json({ errors: errors.array() });
next();
},
];
export const eventsPutValidator = [ export const eventsPutValidator = [
body('id').isString().exists(), body('id').isString().exists(),
(req, res, next) => { (req, res, next) => {
+1 -1
View File
@@ -56,7 +56,7 @@ const uploadAndParse = async (file, req, res, options) => {
} else { } else {
await DataProvider.mergeIntoData(result.data); await DataProvider.mergeIntoData(result.data);
} }
global.timer.setupWithEventList(newEvents); global.timer.setupWithEventList(newEvents.filter((entry) => entry.type === 'event'));
} }
res.sendStatus(200); res.sendStatus(200);
} else { } else {
+8 -7
View File
@@ -1,23 +1,24 @@
import express from 'express'; import express from 'express';
export const router = express.Router();
// import events controller // import events controller
import { import {
eventsApplyDelay,
eventsDelete,
eventsDeleteAll,
eventsGetAll, eventsGetAll,
eventsGetById, eventsGetById,
eventsPatch,
eventsPost, eventsPost,
eventsPut, eventsPut,
eventsPatch,
eventsReorder, eventsReorder,
eventsApplyDelay,
eventsDeleteAll,
eventsDelete,
} from '../controllers/eventsController.js'; } from '../controllers/eventsController.js';
import { import {
eventsPostValidator,
eventsPutValidator, eventsPutValidator,
paramsMustHaveEventId, paramsMustHaveEventId,
} from '../controllers/eventsController.validate.js'; } from '../controllers/eventsController.validate.js';
export const router = express.Router();
// create route between controller and '/events/' endpoint // create route between controller and '/events/' endpoint
router.get('/', eventsGetAll); router.get('/', eventsGetAll);
@@ -25,7 +26,7 @@ router.get('/', eventsGetAll);
router.get('/:eventId', eventsGetById); router.get('/:eventId', eventsGetById);
// create route between controller and '/events/' endpoint // create route between controller and '/events/' endpoint
router.post('/', eventsPost); router.post('/', eventsPostValidator, eventsPost);
// create route between controller and '/events/' endpoint // create route between controller and '/events/' endpoint
router.put('/', eventsPutValidator, eventsPut); router.put('/', eventsPutValidator, eventsPut);