Merge remote-tracking branch 'origin/master' into feat/electron

This commit is contained in:
cv
2021-05-29 22:53:28 +02:00
82 changed files with 1523 additions and 896 deletions
+2
View File
@@ -33,3 +33,5 @@ db.json
yarn.lock yarn.lock
package.json package.json
package-lock.json package-lock.json
db.json
db backup.json
+1
View File
@@ -12,6 +12,7 @@
"axios": "^0.21.1", "axios": "^0.21.1",
"date-fns": "^2.20.1", "date-fns": "^2.20.1",
"framer-motion": "^4.1.6", "framer-motion": "^4.1.6",
"jotai": "^0.16.5",
"react": "^17.0.1", "react": "^17.0.1",
"react-beautiful-dnd": "^13.1.0", "react-beautiful-dnd": "^13.1.0",
"react-dom": "^17.0.1", "react-dom": "^17.0.1",
Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 48 KiB

After

Width:  |  Height:  |  Size: 47 KiB

+4 -2
View File
@@ -3,8 +3,10 @@
-moz-osx-font-smoothing: grayscale; -moz-osx-font-smoothing: grayscale;
} }
body,
html,
.App { .App {
margin: 0px auto; margin: 0px auto;
overflow: hidden; overflow: clip;
height: 100vh; height: 100vh;
} }
+3 -5
View File
@@ -4,8 +4,6 @@ import './App.css';
import { QueryClient, QueryClientProvider } from 'react-query'; import { QueryClient, QueryClientProvider } from 'react-query';
import SocketProvider from 'app/context/socketContext'; import SocketProvider from 'app/context/socketContext';
import withSocket from 'features/viewers/ViewWrapper'; import withSocket from 'features/viewers/ViewWrapper';
import { ReactQueryDevtools } from 'react-query/devtools';
import Empty from 'common/state/Empty';
const Editor = lazy(() => import('features/editors/Editor')); const Editor = lazy(() => import('features/editors/Editor'));
const PresenterView = lazy(() => const PresenterView = lazy(() =>
@@ -42,7 +40,7 @@ function App() {
<SocketProvider> <SocketProvider>
<QueryClientProvider client={queryClient}> <QueryClientProvider client={queryClient}>
<div className='App'> <div className='App'>
<Suspense fallback={<Empty text='Loading' />}> <Suspense fallback={null}>
<Switch> <Switch>
<Route exact path='/' component={SSpeaker} /> <Route exact path='/' component={SSpeaker} />
<Route exact path='/sm' component={SStageManager} /> <Route exact path='/sm' component={SStageManager} />
@@ -50,12 +48,12 @@ function App() {
<Route exact path='/speakersimple' component={SSpeakerSimple} /> <Route exact path='/speakersimple' component={SSpeakerSimple} />
<Route exact path='/editor' component={Editor} /> <Route exact path='/editor' component={Editor} />
<Route exact path='/public' component={SPublic} /> <Route exact path='/public' component={SPublic} />
<Route exact path='/lower' component={SLowerThird} />
<Route exact path='/pip' component={SPip} /> <Route exact path='/pip' component={SPip} />
{/* Lower cannot have fallback */}
<Route exact path='/lower' component={SLowerThird} />
{/* Send to default if nothing found */} {/* Send to default if nothing found */}
<Route component={SSpeaker} /> <Route component={SSpeaker} />
</Switch> </Switch>
<ReactQueryDevtools initialIsOpen={false} />
</Suspense> </Suspense>
</div> </div>
</QueryClientProvider> </QueryClientProvider>
+6
View File
@@ -1,7 +1,13 @@
export const NODE_PORT = 4001; export const NODE_PORT = 4001;
export const EVENT_TABLE = 'event';
export const EVENTS_TABLE = 'events';
const calculateServer = () => { const calculateServer = () => {
return window.location.origin.replace(window.location.port, `${NODE_PORT}/`); return window.location.origin.replace(window.location.port, `${NODE_PORT}/`);
}; };
export const serverURL = calculateServer(); export const serverURL = calculateServer();
export const eventURL = serverURL + EVENT_TABLE;
export const eventsURL = serverURL + EVENTS_TABLE;
export const playbackURL = serverURL + 'playback';
export const ontimeURL = serverURL + 'ontime';
+1 -4
View File
@@ -1,8 +1,5 @@
import axios from 'axios'; import axios from 'axios';
import { serverURL } from './apiConstants'; import { eventURL } from './apiConstants';
export const eventNamespace = 'event';
export const eventURL = serverURL + eventNamespace;
export const fetchEvent = async () => { export const fetchEvent = async () => {
const res = await axios.get(eventURL); const res = await axios.get(eventURL);
+6 -10
View File
@@ -1,14 +1,5 @@
import axios from 'axios'; import axios from 'axios';
import { NODE_PORT } from '../api/apiConstants'; import { eventsURL } from '../api/apiConstants';
// get origin from URL
const serverURL = window.location.origin.replace(
window.location.port,
`${NODE_PORT}/`
);
export const eventsNamespace = 'events';
export const eventsURL = serverURL + eventsNamespace;
export const fetchAllEvents = async () => { export const fetchAllEvents = async () => {
const res = await axios.get(eventsURL); const res = await axios.get(eventsURL);
@@ -46,3 +37,8 @@ export const requestDelete = async (eventId) => {
const res = await axios.delete(eventsURL + '/' + eventId); const res = await axios.delete(eventsURL + '/' + eventId);
return res; return res;
}; };
export const requestDeleteAll = async () => {
const res = await axios.delete(eventsURL + '/all');
return res;
};
+42
View File
@@ -0,0 +1,42 @@
import axios from 'axios';
import { ontimeURL } from './apiConstants';
export const downloadEvents = async () => {
await axios({
url: ontimeURL + '/db',
method: 'GET',
responseType: 'blob', // important
}).then((response) => {
let headerLine = response.headers['Content-Disposition'];
console.log(response);
let filename = 'events.json';
// try and get the filename from the response
if (headerLine != null) {
let startFileNameIndex = headerLine.indexOf('"') + 1;
let endFileNameIndex = headerLine.lastIndexOf('"');
filename = headerLine.substring(startFileNameIndex, endFileNameIndex);
}
const url = window.URL.createObjectURL(new Blob([response.data]));
const link = document.createElement('a');
link.href = url;
link.setAttribute('download', filename);
document.body.appendChild(link);
link.click();
});
};
export const uploadEvents = async (file) => {
console.log('uploading', file);
const formData = new FormData();
formData.append('jsondb', file); // appending file
await axios
.post(ontimeURL + '/db', formData, {
headers: {
'Content-Type': 'multipart/form-data',
},
})
.then((res) => console.log(res.data))
.catch((err) => console.error(err));
};
+1 -4
View File
@@ -1,8 +1,5 @@
import axios from 'axios'; import axios from 'axios';
import { serverURL } from '../api/apiConstants'; import { playbackURL } from '../api/apiConstants';
export const playbackNamespace = 'playback';
const playbackURL = serverURL + playbackNamespace;
export const getStart = async () => { export const getStart = async () => {
const res = await axios.get(playbackURL + '/start'); const res = await axios.get(playbackURL + '/start');
+60
View File
@@ -0,0 +1,60 @@
const { atom } = require('jotai');
const PATH = 'option-collapse';
const initialValue = {};
// collapse options object, initialised from local storage
// REFRACT: When do we read from local storage?
// on every render?
export const collapseAtom = atom(
(get) => {
const storedOptions = localStorage.getItem(PATH);
if (storedOptions == null) return initialValue;
return JSON.parse(storedOptions);
},
(get, set, newValues) => {
set(collapseAtom, newValues);
localStorage.setItem(PATH, JSON.stringify(newValues));
}
);
// get a single option, if it exists
export const SelectCollapse = (id) => {
return atom((get) => get(collapseAtom)[id]);
};
// change a single item in object
export const HandleCollapse = atom(null, (get, set, payload) => {
const updatedVal = {
...get(collapseAtom),
...payload,
};
set(collapseAtom, updatedVal);
localStorage.setItem(PATH, JSON.stringify(updatedVal));
});
// change collapsed in several items
export const BatchOperation = atom(null, (get, set, payload) => {
let prevOptions = get(collapseAtom);
let newOptions = {};
for (const item of payload.items) {
newOptions[item.id] = payload.isCollapsed;
}
if (payload.clear) {
// clear object
prevOptions = {};
// clear localstorage
localStorage.removeItem(PATH);
}
const options = { ...prevOptions, ...newOptions };
set(collapseAtom, options);
localStorage.setItem(PATH, JSON.stringify(options));
});
+34
View File
@@ -0,0 +1,34 @@
const { atom } = require('jotai');
const PATH = 'option-gen';
const initialValue = {
cursor: 'locked',
};
export const settingsAtom = atom(
(get) => {
const storedOptions = localStorage.getItem(PATH);
if (storedOptions == null) return initialValue;
return JSON.parse(storedOptions);
},
(get, set, newValues) => {
set(settingsAtom, newValues);
localStorage.setItem(PATH, JSON.stringify(newValues));
}
);
// get a single option, if it exists
export const SelectSetting = (setting) => {
return atom((get) => get(settingsAtom)[setting]);
};
// change a single item in object
export const HandleOptions = atom(null, (get, set, payload) => {
const updatedVal = {
...get(settingsAtom),
...payload,
};
set(settingsAtom, updatedVal);
localStorage.setItem(PATH, JSON.stringify(updatedVal));
});
+1 -7
View File
@@ -1,12 +1,6 @@
import { createContext, useContext, useEffect, useState } from 'react'; import { createContext, useContext, useEffect, useState } from 'react';
import io from 'socket.io-client'; import io from 'socket.io-client';
import { NODE_PORT } from '../api/apiConstants'; import { serverURL } from 'app/api/apiConstants';
// get origin from URL
const serverURL = window.location.origin.replace(
window.location.port,
`${NODE_PORT}/`
);
const SocketContext = createContext([[], () => {}]); const SocketContext = createContext([[], () => {}]);
+2 -1
View File
@@ -4,7 +4,8 @@ const refetchIntervalMs = 10000;
export const useFetch = (namespace, fn) => { export const useFetch = (namespace, fn) => {
const { data, status, isError, refetch } = useQuery(namespace, fn, { const { data, status, isError, refetch } = useQuery(namespace, fn, {
refetchInterval: refetchIntervalMs, refetchInterval: refetchIntervalMs,
cacheTime: refetchIntervalMs, cacheTime: Infinity,
notifyOnChangeProps: 'tracked',
}); });
return { data, status, isError, refetch }; return { data, status, isError, refetch };
@@ -0,0 +1,20 @@
import { IconButton } from '@chakra-ui/button';
import { Tooltip } from '@chakra-ui/tooltip';
import { FiChevronsUp } from 'react-icons/fi';
export default function CollapseBtn(props) {
const { clickhandler } = props;
return (
<Tooltip label='Collapse all'>
<IconButton
size={props.size || 'xs'}
icon={<FiChevronsUp />}
colorScheme='white'
variant='outline'
background='#fff1'
onClick={clickhandler}
_focus={{ boxShadow: 'none' }}
/>
</Tooltip>
);
}
@@ -1,17 +0,0 @@
import { IconButton } from '@chakra-ui/button';
import { FiArrowDownCircle } from 'react-icons/fi';
export default function CollapseIconBtn(props) {
const { clickhandler, active, ...rest } = props;
return (
<IconButton
size={props.size || 'xs'}
icon={<FiArrowDownCircle />}
colorScheme='orange'
variant={active ? 'solid' : 'outline'}
onClick={clickhandler}
_focus={{ boxShadow: 'none' }}
{...rest}
/>
);
}
@@ -0,0 +1,18 @@
import { Button } from '@chakra-ui/button';
import { FiTarget } from 'react-icons/fi';
export default function CurrentBtn(props) {
const { clickhandler, active } = props;
return (
<Button
size={props.size || 'xs'}
leftIcon={<FiTarget />}
colorScheme='whiteAlpha'
variant={active ? 'solid' : 'outline'}
onClick={clickhandler}
_focus={{ boxShadow: 'none' }}
>
Goto Current
</Button>
);
}
@@ -0,0 +1,20 @@
import { IconButton } from '@chakra-ui/button';
import { Tooltip } from '@chakra-ui/tooltip';
import { FiChevronsDown } from 'react-icons/fi';
export default function ExpandBtn(props) {
const { clickhandler } = props;
return (
<Tooltip label='Expand all'>
<IconButton
size={props.size || 'xs'}
icon={<FiChevronsDown />}
colorScheme='white'
variant='outline'
background='#fff1'
onClick={clickhandler}
_focus={{ boxShadow: 'none' }}
/>
</Tooltip>
);
}
@@ -0,0 +1,22 @@
import { IconButton } from '@chakra-ui/button';
import { Tooltip } from '@chakra-ui/tooltip';
import { FiTarget } from 'react-icons/fi';
export default function LockIconBtn(props) {
const { clickhandler, active, ref } = props;
return (
<Tooltip label='Lock cursor to current'>
<IconButton
ref={ref}
size={props.size || 'xs'}
icon={<FiTarget />}
color={active ? 'pink.100' : 'pink.300'}
borderColor={active ? undefined : 'pink.300'}
backgroundColor={active ? 'pink.300' : undefined}
variant={active ? 'solid' : 'outline'}
onClick={clickhandler}
_focus={{ boxShadow: 'none' }}
/>
</Tooltip>
);
}
@@ -12,7 +12,6 @@ export default function RollIconBtn(props) {
width={120} width={120}
_focus={{ boxShadow: 'none' }} _focus={{ boxShadow: 'none' }}
{...rest} {...rest}
disabled
/> />
); );
} }
@@ -1,18 +0,0 @@
import { IconButton } from '@chakra-ui/button';
import { FiSettings } from 'react-icons/fi';
export default function SettingsIconBtn(props) {
const { clickhandler, ...rest } = props;
return (
<IconButton
size={props.size || 'xs'}
icon={<FiSettings />}
isRound
variant='outline'
onClick={clickhandler}
_focus={{ boxShadow: 'none' }}
colorScheme='whiteAlpha'
{...rest}
/>
);
}
@@ -8,14 +8,8 @@ const label = {
}; };
const TimesDelayed = (props) => { const TimesDelayed = (props) => {
const { const { handleValidate, actionHandler, delay, timeStart, timeEnd, duration } =
handleValidate, props;
actionHandler,
delay,
timeStart,
timeEnd,
duration,
} = props;
const scheduledStart = stringFromMillis(timeStart, false); const scheduledStart = stringFromMillis(timeStart, false);
const scheduledEnd = stringFromMillis(timeEnd, false); const scheduledEnd = stringFromMillis(timeEnd, false);
@@ -44,7 +38,7 @@ const TimesDelayed = (props) => {
/> />
<span style={label}>Duration</span> <span style={label}>Duration</span>
<EditableTimer <EditableTimer
name='duration' name='durationOverride'
validate={handleValidate} validate={handleValidate}
actionHandler={actionHandler} actionHandler={actionHandler}
time={duration} time={duration}
@@ -6,7 +6,7 @@ import { useInterval } from 'app/hooks/useInterval';
export default function Paginator(props) { export default function Paginator(props) {
const { events, selectedId } = props; const { events, selectedId } = props;
const LIMIT_PER_PAGE = props.limit || 8; const LIMIT_PER_PAGE = props.limit || 8;
const SCROLL_TIME = props.time * 1000 || 5000; const SCROLL_TIME = props.time * 1000 || 10000;
const SCROLL_PAST = false; const SCROLL_PAST = false;
const [numEvents, setNumEvents] = useState(0); const [numEvents, setNumEvents] = useState(0);
const [page, setPage] = useState([]); const [page, setPage] = useState([]);
@@ -53,6 +53,8 @@ export default function Paginator(props) {
} }
}, SCROLL_TIME); }, SCROLL_TIME);
let selectedState = 0;
return ( return (
<> <>
<div className={style.nav}> <div className={style.nav}>
@@ -66,7 +68,7 @@ export default function Paginator(props) {
</div> </div>
<div className={style.entries}> <div className={style.entries}>
{page.map((e) => { {page.map((e) => {
let selectedState = 0; if (selectedState === 1) selectedState = 2;
if (e.id === selected) selectedState = 1; if (e.id === selected) selectedState = 1;
else if (e.id > selected) selectedState = 2; else if (e.id > selected) selectedState = 2;
return ( return (
+2 -2
View File
@@ -33,9 +33,9 @@ export default function EditableText(props) {
> >
<EditablePreview <EditablePreview
color={text === '' ? '#666' : 'inherit'} color={text === '' ? '#666' : 'inherit'}
maxWidth='20em' maxWidth='75%'
/> />
<EditableInput overflowX='hidden' maxWidth='20em' /> <EditableInput overflowX='hidden' maxWidth='75%' />
</Editable> </Editable>
</div> </div>
); );
+2 -8
View File
@@ -34,12 +34,12 @@ export default function EditableTimer(props) {
if (value === '') return false; if (value === '') return false;
// Time now and time submitedVal // Time now and time submitedVal
const original = stringFromMillis(time, false); const original = stringFromMillis(time + delay, false);
// check if time is different from before // check if time is different from before
if (value === original) return false; if (value === original) return false;
// conver to millis object // convert to millis object
const millis = timeStringToMillis(value, timeFormat); const millis = timeStringToMillis(value, timeFormat);
// validate with parent // validate with parent
@@ -51,14 +51,8 @@ export default function EditableTimer(props) {
return true; return true;
}; };
const showOriginal = () => {
setValue(stringFromMillis(time, false));
};
return ( return (
<Editable <Editable
onFocus={() => showOriginal}
onEdit={() => showOriginal}
onChange={(v) => setValue(v)} onChange={(v) => setValue(v)}
onSubmit={(v) => validateValue(v)} onSubmit={(v) => validateValue(v)}
value={value} value={value}
@@ -129,7 +129,7 @@ export default function MessageControl() {
text={lower.text} text={lower.text}
visible={lower.visible} visible={lower.visible}
changeHandler={(event) => messageControl('lower-text', event)} changeHandler={(event) => messageControl('lower-text', event)}
actionHandler={() => messageControl('toggle-publ-visible')} actionHandler={() => messageControl('toggle-lower-visible')}
/> />
</div> </div>
); );
@@ -41,10 +41,7 @@ const Transport = ({ selectedId, playbackControl }) => {
<div className={style.playbackContainer}> <div className={style.playbackContainer}>
<PrevIconBtn clickhandler={() => playbackControl('previous')} /> <PrevIconBtn clickhandler={() => playbackControl('previous')} />
<NextIconBtn clickhandler={() => playbackControl('next')} /> <NextIconBtn clickhandler={() => playbackControl('next')} />
<UnloadIconBtn <UnloadIconBtn clickhandler={() => playbackControl('unload')} />
clickhandler={() => playbackControl('unload')}
disabled={!selectedId}
/>
<ReloadIconButton <ReloadIconButton
clickhandler={() => playbackControl('reload')} clickhandler={() => playbackControl('reload')}
disabled={!selectedId} disabled={!selectedId}
@@ -90,6 +90,7 @@ export default function PlaybackControl() {
<div className={style.mainContainer}> <div className={style.mainContainer}>
<PlaybackTimer <PlaybackTimer
timer={timer} timer={timer}
playback={playback}
handleIncrement={(amount) => socket.emit('increment-timer', amount)} handleIncrement={(amount) => socket.emit('increment-timer', amount)}
/> />
<PlaybackButtons <PlaybackButtons
@@ -35,18 +35,19 @@
height: 100%; height: 100%;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
justify-content: space-around; justify-content: space-evenly;
} }
.indRoll, .indRoll,
.indDelay, .indDelay,
.indNegative { .indNegative {
background-color: rgba(0, 0, 0, 0.05); background-color: rgba(0, 0, 0, 0.05);
margin: 0 auto;
} }
.indRoll, .indRoll,
.indRollActive,
.indDelay { .indDelay {
margin: 0 auto;
border-radius: 50%; border-radius: 50%;
width: 0.8em; width: 0.8em;
height: 0.8em; height: 0.8em;
@@ -60,7 +61,7 @@
.indNegativeActive { .indNegativeActive {
margin: 0 auto; margin: 0 auto;
width: 90%; width: 90%;
height: 0.3em height: 0.3em;
} }
.indNegativeActive { .indNegativeActive {
+26 -8
View File
@@ -8,16 +8,18 @@ const areEqual = (prevProps, nextProps) => {
return ( return (
prevProps.timer.running === nextProps.timer.running && prevProps.timer.running === nextProps.timer.running &&
prevProps.timer.expectedFinish === nextProps.timer.expectedFinish && prevProps.timer.expectedFinish === nextProps.timer.expectedFinish &&
prevProps.timer.startedAt === nextProps.timer.startedAt prevProps.timer.startedAt === nextProps.timer.startedAt &&
prevProps.playback === nextProps.playback
); );
}; };
const PlaybackTimer = ({ timer, handleIncrement }) => { const PlaybackTimer = (props) => {
const { timer, playback, handleIncrement } = props;
const started = stringFromMillis(timer.startedAt, true); const started = stringFromMillis(timer.startedAt, true);
const finish = stringFromMillis(timer.expectedFinish, true); const finish = stringFromMillis(timer.expectedFinish, true);
const isNegative = timer.running < 0; const isNegative = timer.running < 0;
const isDelayed = false; const isDelayed = false;
const isRolling = false; const isRolling = playback === 'roll';
const incrementProps = { const incrementProps = {
size: 'sm', size: 'sm',
@@ -31,7 +33,7 @@ const PlaybackTimer = ({ timer, handleIncrement }) => {
<> <>
<div className={style.timeContainer}> <div className={style.timeContainer}>
<div className={style.indicators}> <div className={style.indicators}>
<div className={style.indRoll} /> <div className={isRolling ? style.indRollActive : style.indRoll} />
<div <div
className={isNegative ? style.indNegativeActive : style.indNegative} className={isNegative ? style.indNegativeActive : style.indNegative}
/> />
@@ -49,16 +51,32 @@ const PlaybackTimer = ({ timer, handleIncrement }) => {
<span className={style.time}>{finish}</span> <span className={style.time}>{finish}</span>
</div> </div>
<div className={style.btn}> <div className={style.btn}>
<Button {...incrementProps} onClick={() => handleIncrement(-1)}> <Button
{...incrementProps}
disabled={isRolling}
onClick={() => handleIncrement(-1)}
>
-1 -1
</Button> </Button>
<Button {...incrementProps} onClick={() => handleIncrement(1)}> <Button
{...incrementProps}
disabled={isRolling}
onClick={() => handleIncrement(1)}
>
+1 +1
</Button> </Button>
<Button {...incrementProps} onClick={() => handleIncrement(-5)}> <Button
{...incrementProps}
disabled={isRolling}
onClick={() => handleIncrement(-5)}
>
-5 -5
</Button> </Button>
<Button {...incrementProps} onClick={() => handleIncrement(5)}> <Button
{...incrementProps}
disabled={isRolling}
onClick={() => handleIncrement(5)}
>
+5 +5
</Button> </Button>
</div> </div>
+5 -7
View File
@@ -7,8 +7,8 @@ import styles from './Editor.module.css';
import EventListWrapper from './list/EventListWrapper'; import EventListWrapper from './list/EventListWrapper';
import { useDisclosure } from '@chakra-ui/hooks'; import { useDisclosure } from '@chakra-ui/hooks';
import SettingsModal from '../modals/SettingsModal'; import SettingsModal from '../modals/SettingsModal';
import SettingsIconBtn from 'common/components/buttons/SettingsIconBtn';
import { useEffect } from 'react'; import { useEffect } from 'react';
import MenuBar from 'features/menu/MenuBar';
export default function Editor() { export default function Editor() {
const { isOpen, onOpen, onClose } = useDisclosure(); const { isOpen, onOpen, onClose } = useDisclosure();
@@ -23,6 +23,10 @@ export default function Editor() {
<SettingsModal isOpen={isOpen} onClose={onClose} /> <SettingsModal isOpen={isOpen} onClose={onClose} />
<div className={styles.mainContainer}> <div className={styles.mainContainer}>
<Box id='settings' className={styles.settings}>
<MenuBar onOpen={onOpen} onClose={onClose} />
</Box>
<Box className={styles.editor}> <Box className={styles.editor}>
<Heading size='lg' paddingBottom={'0.25em'}> <Heading size='lg' paddingBottom={'0.25em'}>
Event List Event List
@@ -63,12 +67,6 @@ export default function Editor() {
<NumberedText number={4} text={'Running Info'} /> <NumberedText number={4} text={'Running Info'} />
<div className={styles.content}></div> <div className={styles.content}></div>
</Box> </Box>
<Box className={styles.settings}>
<div className={styles.content}>
<SettingsIconBtn size='md' clickhandler={onOpen} />
</div>
</Box>
</div> </div>
</> </>
); );
+19 -9
View File
@@ -8,21 +8,22 @@
display: grid; display: grid;
grid-template-rows: 38vh 1fr; grid-template-rows: 38vh 1fr;
grid-template-columns: 48em 27vw 25vw 4vw; grid-template-columns: 40px 48em 27vw 1fr;
grid-template-areas: grid-template-areas:
'even play info sett' 'sett even play info'
'even mess info sett'; 'sett even mess info';
gap: 2vh; gap: 2vh;
} }
/* 2/3 window, hide previews */ /* 2/3 window, hide previews */
@media (max-width: 1250px) and (min-height: 700px) { @media (max-width: 1250px) and (min-height: 700px) {
.mainContainer { .mainContainer {
height: 100%;
grid-template-rows: 1fr 1fr; grid-template-rows: 1fr 1fr;
grid-template-columns: 48em 1fr 1fr; grid-template-columns: 40px 48em 1fr 1fr;
grid-template-areas: /* grid-template-areas:
'even play sett' 'even play sett'
'even mess sett'; 'even mess sett'; */
} }
.info { .info {
@@ -40,6 +41,11 @@
'play'; 'play';
} }
.messages,
.playback {
min-width: 31em;
}
.editor, .editor,
.info, .info,
.settings { .settings {
@@ -89,12 +95,16 @@
grid-area: play; grid-area: play;
} }
.settings { .mainContainer > .settings {
grid-area: sett; grid-area: sett;
background-color: transparent;
padding: 0;
margin: 0;
width: fit-content;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
align-items: center; gap: 1em;
gap: 2.6em;
} }
.content { .content {
@@ -17,7 +17,7 @@ export default function ActionButtons(props) {
aria-label='Options' aria-label='Options'
size='xs' size='xs'
icon={<FiZap />} icon={<FiZap />}
_expanded={{ bg: 'pink.300', color: 'white' }} _expanded={{ bg: 'orange.300', color: 'white' }}
_focus={{ boxShadow: 'none' }} _focus={{ boxShadow: 'none' }}
backgroundColor={'orange.200'} backgroundColor={'orange.200'}
color={'orange.500'} color={'orange.500'}
+37 -21
View File
@@ -1,6 +1,6 @@
import Icon from '@chakra-ui/icon'; import Icon from '@chakra-ui/icon';
import { FiChevronDown, FiChevronUp, FiMoreVertical } from 'react-icons/fi'; import { FiChevronDown, FiChevronUp, FiMoreVertical } from 'react-icons/fi';
import { useState } from 'react'; import { useMemo } from 'react';
import { Draggable } from 'react-beautiful-dnd'; import { Draggable } from 'react-beautiful-dnd';
import EventTimes from 'common/components/eventTimes/EventTimes'; import EventTimes from 'common/components/eventTimes/EventTimes';
import EventTimesVertical from 'common/components/eventTimes/EventTimesVertical'; import EventTimesVertical from 'common/components/eventTimes/EventTimesVertical';
@@ -10,9 +10,12 @@ import VisibleIconBtn from 'common/components/buttons/VisibleIconBtn';
import DeleteIconBtn from 'common/components/buttons/DeleteIconBtn'; import DeleteIconBtn from 'common/components/buttons/DeleteIconBtn';
import { millisToMinutes } from 'common/dateConfig'; import { millisToMinutes } from 'common/dateConfig';
import style from './EventBlock.module.css'; import style from './EventBlock.module.css';
import { SelectCollapse, HandleCollapse } from 'app/context/collapseAtom';
import { useAtom } from 'jotai';
const ExpandedBlock = (props) => { const ExpandedBlock = (props) => {
const { provided, data, next, delay, delayValue, actionHandler } = props; const { provided, data, eventIndex, next, delay, delayValue, actionHandler } =
props;
const oscid = data.id.length > 4 ? '...' : data.id; const oscid = data.id.length > 4 ? '...' : data.id;
@@ -78,14 +81,16 @@ const ExpandedBlock = (props) => {
actionHandler('update', { field: 'note', value: v }) actionHandler('update', { field: 'note', value: v })
} }
/> />
<span className={style.oscLabel}>{`OSC ID: ${oscid}`}</span> <span className={style.oscLabel}>
{`/ontime/goto ${eventIndex + 1} << OSC >> /ontime/gotoid ${oscid}`}
</span>
</div> </div>
<Icon <Icon
className={style.more} className={style.more}
as={FiChevronUp} as={FiChevronUp}
marginTop='0.2em' marginTop='0.2em'
gridArea='more' gridArea='more'
onClick={() => props.setExpanded(false)} onClick={() => props.setCollapsed(true)}
/> />
<div className={style.actionOverlay}> <div className={style.actionOverlay}>
<VisibleIconBtn actionHandler={actionHandler} active={data.isPublic} /> <VisibleIconBtn actionHandler={actionHandler} active={data.isPublic} />
@@ -138,7 +143,7 @@ const CollapsedBlock = (props) => {
as={FiChevronDown} as={FiChevronDown}
marginTop='0.2em' marginTop='0.2em'
gridArea='more' gridArea='more'
onClick={() => props.setExpanded(true)} onClick={() => props.setCollapsed(false)}
/> />
<div className={style.actionOverlay}> <div className={style.actionOverlay}>
<VisibleIconBtn actionHandler={actionHandler} active={data.isPublic} /> <VisibleIconBtn actionHandler={actionHandler} active={data.isPublic} />
@@ -154,19 +159,29 @@ const CollapsedBlock = (props) => {
}; };
export default function EventBlock(props) { export default function EventBlock(props) {
const { data, selected, delay, index, actionHandler } = props; const { data, selected, delay, index, eventIndex, actionHandler } = props;
const [expanded, setExpanded] = useState(true); // const [collapsed, setCollapsed] = useState(checkLocalStorage(data.id));
// const collapsed = useSelector(itemsAtom, (state) => state === data.id);
const [collapsed] = useAtom(
useMemo(() => SelectCollapse(data.id), [data.id])
);
const [, setCollapsed] = useAtom(HandleCollapse);
// TODO: should this go inside useEffect() // TODO: should this go inside useEffect()
// Would I then need to add this to state? // Would I then need to add this to state?
const isSelected = selected ? style.active : ''; const isSelected = selected ? style.active : '';
const isExpanded = expanded ? style.expanded : style.collapsed; const isCollapsed = collapsed ? style.collapsed : style.expanded;
const classSelect = `${style.event} ${isExpanded} ${isSelected}`; const classSelect = `${style.event} ${isCollapsed} ${isSelected}`;
// Calculate delay in min // Calculate delay in min
const delayValue = delay > 0 ? millisToMinutes(delay) : null; const delayValue = delay > 0 ? millisToMinutes(delay) : null;
const handleCollapse = (isCollapsed) => {
setCollapsed({ [data.id]: isCollapsed });
};
return ( return (
<Draggable key={data.id} draggableId={data.id} index={index}> <Draggable key={data.id} draggableId={data.id} index={index}>
{(provided) => ( {(provided) => (
@@ -175,17 +190,7 @@ export default function EventBlock(props) {
{...provided.draggableProps} {...provided.draggableProps}
ref={provided.innerRef} ref={provided.innerRef}
> >
{expanded ? ( {collapsed ? (
<ExpandedBlock
provided={provided}
data={data}
next={props.next}
delay={delay}
delayValue={delayValue}
actionHandler={actionHandler}
setExpanded={setExpanded}
/>
) : (
<CollapsedBlock <CollapsedBlock
provided={provided} provided={provided}
data={data} data={data}
@@ -193,7 +198,18 @@ export default function EventBlock(props) {
delay={delay} delay={delay}
delayValue={delayValue} delayValue={delayValue}
actionHandler={actionHandler} actionHandler={actionHandler}
setExpanded={setExpanded} setCollapsed={handleCollapse}
/>
) : (
<ExpandedBlock
provided={provided}
eventIndex={eventIndex}
data={data}
next={props.next}
delay={delay}
delayValue={delayValue}
actionHandler={actionHandler}
setCollapsed={handleCollapse}
/> />
)} )}
</div> </div>
+69 -56
View File
@@ -1,18 +1,22 @@
import style from './List.module.css'; import style from './List.module.css';
import { Fragment, useEffect, useState } from 'react'; import { createRef, useEffect, useMemo, useState } from 'react';
import { useSocket } from 'app/context/socketContext'; import { useSocket } from 'app/context/socketContext';
import tinykeys from 'tinykeys'; import tinykeys from 'tinykeys';
import Empty from 'common/state/Empty'; import Empty from 'common/state/Empty';
import EventListItem from './EventListItem'; import EventListItem from './EventListItem';
import { AnimatePresence, motion } from 'framer-motion';
import { DragDropContext, Droppable } from 'react-beautiful-dnd'; import { DragDropContext, Droppable } from 'react-beautiful-dnd';
import { useAtom } from 'jotai';
import { SelectSetting } from 'app/context/settingsAtom';
export default function EventList(props) { export default function EventList(props) {
const { events, eventsHandler } = props; const { events, eventsHandler } = props;
const socket = useSocket(); const socket = useSocket();
const [selected, setSelected] = useState(null); const [selectedId, setSelectedId] = useState(null);
const [next, setNext] = useState(null); const [nextId, setNextId] = useState(null);
const [cursor, setCursor] = useState(null); const [cursor, setCursor] = useState(0);
const [cursorSettings] = useAtom(useMemo(() => SelectSetting('cursor'), []));
const cursorRef = createRef();
// Handle keyboard shortcuts // Handle keyboard shortcuts
useEffect(() => { useEffect(() => {
@@ -23,7 +27,7 @@ export default function EventList(props) {
}, },
'Alt+ArrowUp': () => { 'Alt+ArrowUp': () => {
if (cursor == null) setCursor(0); if (cursor == null) setCursor(0);
else if (cursor >= 0) setCursor(cursor - 1); else if (cursor > 0) setCursor(cursor - 1);
}, },
'Alt+KeyE': (event) => { 'Alt+KeyE': (event) => {
event.preventDefault(); event.preventDefault();
@@ -45,51 +49,68 @@ export default function EventList(props) {
return () => { return () => {
unsubscribe(); unsubscribe();
}; };
}, [cursor, events.length, eventsHandler]); }, [cursor, events, eventsHandler]);
// handle incoming messages // handle incoming messages
useEffect(() => { useEffect(() => {
if (socket == null) return; if (socket == null) return;
// ask for playstate // ask for playstate
socket.emit('get-selected-id'); socket.emit('get-selected');
socket.emit('get-next-id'); socket.emit('get-next-id');
// Handle playstate // Handle playstate
socket.on('selected-id', (data) => { socket.on('selected', (data) => {
setSelected(data); setSelectedId(data.id);
}); });
socket.on('next-id', (data) => { socket.on('next-id', (data) => {
setNext(data); setNextId(data);
}); });
// Clear listener // Clear listener
return () => { return () => {
socket.off('selected-id'); socket.off('selected');
socket.off('next-id'); socket.off('next-id');
}; };
}, [socket]); }, [socket]);
// when cursor moves, view should follow
useEffect(() => {
if (cursorRef.current == null) return;
cursorRef.current.scrollIntoView({
behavior: 'smooth',
block: 'nearest',
inline: 'start',
});
}, [cursor]);
// if selected event
// or cursor settings changed
useEffect(() => {
// and if we are locked
if (cursorSettings !== 'locked' || selectedId == null) return;
// move cursor
let gotoIndex = -1;
let found = false;
for (const e of events) {
gotoIndex++;
if (e.id === selectedId) {
found = true;
break;
}
}
if (found) {
// move cursor
setCursor(gotoIndex);
}
}, [selectedId, cursorSettings]);
if (events.length < 1) { if (events.length < 1) {
return <Empty text='No Events' />; return <Empty text='No Events' />;
} }
// motion
const cursorVariants = {
hidden: {
scale: 0,
},
visible: {
scale: 1,
transition: {
duration: 0.3,
},
},
exit: {
scale: 0,
},
};
// DND // DND
const handleOnDragEnd = (result) => { const handleOnDragEnd = (result) => {
// drop outside of area // drop outside of area
@@ -108,20 +129,10 @@ export default function EventList(props) {
console.log('EventList: events in event list', events); console.log('EventList: events in event list', events);
let cumulativeDelay = 0; let cumulativeDelay = 0;
let eventIndex = -1;
return ( return (
<div className={style.eventContainer}> <div className={style.eventContainer}>
<AnimatePresence>
{cursor === -1 && (
<motion.div
className={style.cursor}
variants={cursorVariants}
initial='hidden'
animate='visible'
exit='exit'
/>
)}
</AnimatePresence>
<DragDropContext onDragEnd={handleOnDragEnd}> <DragDropContext onDragEnd={handleOnDragEnd}>
<Droppable droppableId='eventlist'> <Droppable droppableId='eventlist'>
{(provided) => ( {(provided) => (
@@ -131,33 +142,35 @@ export default function EventList(props) {
ref={provided.innerRef} ref={provided.innerRef}
> >
{events.map((e, index) => { {events.map((e, index) => {
if (index === 0) cumulativeDelay = 0; if (index === 0) {
cumulativeDelay = 0;
eventIndex = -1;
}
if (e.type === 'delay' && e.duration != null) { if (e.type === 'delay' && e.duration != null) {
cumulativeDelay += e.duration; cumulativeDelay += e.duration;
} else if (e.type === 'block') cumulativeDelay = 0; } else if (e.type === 'block') {
cumulativeDelay = 0;
} else if (e.type === 'event') {
eventIndex++;
}
return ( return (
<Fragment key={e.id}> <div
ref={cursor === index ? cursorRef : undefined}
key={e.id}
className={cursor === index ? style.cursor : undefined}
>
<EventListItem <EventListItem
type={e.type} type={e.type}
index={index} index={index}
eventIndex={eventIndex}
data={e} data={e}
selected={selected === e.id} selected={selectedId === e.id}
next={next === e.id} next={nextId === e.id}
eventsHandler={eventsHandler} eventsHandler={eventsHandler}
delay={cumulativeDelay} delay={cumulativeDelay}
/> />
<AnimatePresence> </div>
{cursor === index && (
<motion.div
className={style.cursor}
variants={cursorVariants}
initial='hidden'
animate='visible'
exit='exit'
/>
)}
</AnimatePresence>
</Fragment>
); );
})} })}
{provided.placeholder} {provided.placeholder}
@@ -18,6 +18,7 @@ const EventListItem = (props) => {
const { const {
type, type,
index, index,
eventIndex,
data, data,
selected, selected,
next, next,
@@ -71,6 +72,7 @@ const EventListItem = (props) => {
return ( return (
<EventBlock <EventBlock
index={index} index={index}
eventIndex={eventIndex}
data={data} data={data}
selected={selected} selected={selected}
next={next} next={next}
@@ -1,12 +1,12 @@
import { useMutation, useQueryClient } from 'react-query'; import { useMutation, useQueryClient } from 'react-query';
import { useCallback, useEffect } from 'react'; import { useCallback, useEffect } from 'react';
import { import {
eventsNamespace,
fetchAllEvents, fetchAllEvents,
requestPatch, requestPatch,
requestPost, requestPost,
requestPut, requestPut,
requestDelete, requestDelete,
requestDeleteAll,
requestReorder, requestReorder,
requestApplyDelay, requestApplyDelay,
} from 'app/api/eventsApi.js'; } from 'app/api/eventsApi.js';
@@ -15,11 +15,15 @@ import EventListMenu from 'features/menu/EventListMenu.jsx';
import { showErrorToast } from 'common/helpers/toastManager'; import { showErrorToast } from 'common/helpers/toastManager';
import { useFetch } from 'app/hooks/useFetch.js'; import { useFetch } from 'app/hooks/useFetch.js';
import Empty from 'common/state/Empty'; import Empty from 'common/state/Empty';
import { EVENTS_TABLE } from 'app/api/apiConstants';
import { BatchOperation } from 'app/context/collapseAtom';
import { useAtom } from 'jotai';
export default function EventListWrapper() { export default function EventListWrapper() {
const [, setCollapsed] = useAtom(BatchOperation);
const queryClient = useQueryClient(); const queryClient = useQueryClient();
const { data, status, isError, refetch } = useFetch( const { data, status, isError, refetch } = useFetch(
eventsNamespace, EVENTS_TABLE,
fetchAllEvents fetchAllEvents
); );
@@ -27,14 +31,14 @@ export default function EventListWrapper() {
// we optimistically update here // we optimistically update here
onMutate: async (newEvent) => { onMutate: async (newEvent) => {
// cancel ongoing queries // cancel ongoing queries
queryClient.cancelQueries(eventsNamespace, { exact: true }); queryClient.cancelQueries(EVENTS_TABLE, { exact: true });
// Snapshot the previous value // Snapshot the previous value
let previousEvents = queryClient.getQueryData(eventsNamespace); let previousEvents = queryClient.getQueryData(EVENTS_TABLE);
console.log('debug', previousEvents); console.log('debug', previousEvents);
if (previousEvents == null) { if (previousEvents == null) {
refetch(); refetch();
previousEvents = queryClient.getQueryData(eventsNamespace); previousEvents = queryClient.getQueryData(EVENTS_TABLE);
} }
console.log('debug 2', previousEvents); console.log('debug 2', previousEvents);
@@ -44,7 +48,7 @@ export default function EventListWrapper() {
...newEvent, ...newEvent,
id: new Date().toISOString(), id: new Date().toISOString(),
}); });
queryClient.setQueryData(eventsNamespace, optimistic); queryClient.setQueryData(EVENTS_TABLE, optimistic);
// Return a context with the previous and new todo // Return a context with the previous and new todo
return { previousEvents }; return { previousEvents };
@@ -52,12 +56,12 @@ export default function EventListWrapper() {
// Mutation fails, rollback undos optimist update // Mutation fails, rollback undos optimist update
onError: (error, newEvent, context) => { onError: (error, newEvent, context) => {
queryClient.setQueryData(eventsNamespace, context.previousEvents); queryClient.setQueryData(EVENTS_TABLE, context.previousEvents);
}, },
// Mutation finished, failed or successful // Mutation finished, failed or successful
// Fetch anyway, just to be sure // Fetch anyway, just to be sure
onSettled: () => { onSettled: () => {
queryClient.invalidateQueries(eventsNamespace); queryClient.invalidateQueries(EVENTS_TABLE);
}, },
}); });
@@ -65,16 +69,16 @@ export default function EventListWrapper() {
// we optimistically update here // we optimistically update here
onMutate: async (newEvent) => { onMutate: async (newEvent) => {
// cancel ongoing queries // cancel ongoing queries
queryClient.cancelQueries([eventsNamespace, newEvent.id]); queryClient.cancelQueries([EVENTS_TABLE, newEvent.id]);
// Snapshot the previous value // Snapshot the previous value
const previousEvent = queryClient.getQueryData([ const previousEvent = queryClient.getQueryData([
eventsNamespace, EVENTS_TABLE,
newEvent.id, newEvent.id,
]); ]);
// optimistically update object // optimistically update object
queryClient.setQueryData([eventsNamespace, newEvent.id], newEvent); queryClient.setQueryData([EVENTS_TABLE, newEvent.id], newEvent);
// Return a context with the previous and new todo // Return a context with the previous and new todo
return { previousEvent, newEvent }; return { previousEvent, newEvent };
@@ -83,14 +87,14 @@ export default function EventListWrapper() {
// Mutation fails, rollback undos optimist update // Mutation fails, rollback undos optimist update
onError: (error, newEvent, context) => { onError: (error, newEvent, context) => {
queryClient.setQueryData( queryClient.setQueryData(
[eventsNamespace, context.newEvent.id], [EVENTS_TABLE, context.newEvent.id],
context.previousEvent context.previousEvent
); );
}, },
// Mutation finished, failed or successful // Mutation finished, failed or successful
// Fetch anyway, just to be sure // Fetch anyway, just to be sure
onSettled: (newEvent) => { onSettled: (newEvent) => {
queryClient.invalidateQueries([eventsNamespace, newEvent.id]); queryClient.invalidateQueries([EVENTS_TABLE, newEvent.id]);
}, },
}); });
@@ -98,16 +102,16 @@ export default function EventListWrapper() {
// we optimistically update here // we optimistically update here
onMutate: async (newEvent) => { onMutate: async (newEvent) => {
// cancel ongoing queries // cancel ongoing queries
queryClient.cancelQueries([eventsNamespace, newEvent.id]); queryClient.cancelQueries([EVENTS_TABLE, newEvent.id]);
// Snapshot the previous value // Snapshot the previous value
const previousEvent = queryClient.getQueryData([ const previousEvent = queryClient.getQueryData([
eventsNamespace, EVENTS_TABLE,
newEvent.id, newEvent.id,
]); ]);
// optimistically update object // optimistically update object
queryClient.setQueryData([eventsNamespace, newEvent.id], newEvent); queryClient.setQueryData([EVENTS_TABLE, newEvent.id], newEvent);
// Return a context with the previous and new todo // Return a context with the previous and new todo
return { previousEvent, newEvent }; return { previousEvent, newEvent };
@@ -116,14 +120,14 @@ export default function EventListWrapper() {
// Mutation fails, rollback undos optimist update // Mutation fails, rollback undos optimist update
onError: (error, newEvent, context) => { onError: (error, newEvent, context) => {
queryClient.setQueryData( queryClient.setQueryData(
[eventsNamespace, context.newEvent.id], [EVENTS_TABLE, context.newEvent.id],
context.previousEvent context.previousEvent
); );
}, },
// Mutation finished, failed or successful // Mutation finished, failed or successful
// Fetch anyway, just to be sure // Fetch anyway, just to be sure
onSettled: (newEvent) => { onSettled: (newEvent) => {
queryClient.invalidateQueries([eventsNamespace, newEvent.id]); queryClient.invalidateQueries([EVENTS_TABLE, newEvent.id]);
}, },
}); });
@@ -131,16 +135,16 @@ export default function EventListWrapper() {
// we optimistically update here // we optimistically update here
onMutate: async (eventId) => { onMutate: async (eventId) => {
// cancel ongoing queries // cancel ongoing queries
queryClient.cancelQueries([eventsNamespace, eventId]); queryClient.cancelQueries([EVENTS_TABLE, eventId]);
// Snapshot the previous value // Snapshot the previous value
const previousEvents = queryClient.getQueryData(eventsNamespace); const previousEvents = queryClient.getQueryData(EVENTS_TABLE);
let filtered = [...previousEvents]; let filtered = [...previousEvents];
filtered.filter((e) => e.id === 'eventId'); filtered.filter((e) => e.id === 'eventId');
// optimistically update object // optimistically update object
queryClient.setQueryData(eventsNamespace, filtered); queryClient.setQueryData(EVENTS_TABLE, filtered);
// Return a context with the previous and new todo // Return a context with the previous and new todo
return { previousEvents }; return { previousEvents };
@@ -148,19 +152,48 @@ export default function EventListWrapper() {
// Mutation fails, rollback undos optimist update // Mutation fails, rollback undos optimist update
onError: (error, eventId, context) => { onError: (error, eventId, context) => {
queryClient.setQueryData(eventsNamespace, context.previousEvents); queryClient.setQueryData(EVENTS_TABLE, context.previousEvents);
}, },
// Mutation finished, failed or successful // Mutation finished, failed or successful
// Fetch anyway, just to be sure // Fetch anyway, just to be sure
onSettled: () => { onSettled: () => {
queryClient.invalidateQueries(eventsNamespace); queryClient.invalidateQueries(EVENTS_TABLE);
},
});
const deleteAllEvents = useMutation(requestDeleteAll, {
// we optimistically update here
onMutate: async () => {
// cancel ongoing queries
queryClient.cancelQueries(EVENTS_TABLE, { exact: true });
// Snapshot the previous value
const previousEvents = queryClient.getQueryData(EVENTS_TABLE);
let clear = [];
// optimistically update object
queryClient.setQueryData(EVENTS_TABLE, clear);
// Return a context with the previous and new todo
return { previousEvents };
},
// Mutation fails, rollback undos optimist update
onError: (error, eventId, context) => {
queryClient.setQueryData(EVENTS_TABLE, context.previousEvents);
},
// Mutation finished, failed or successful
// Fetch anyway, just to be sure
onSettled: () => {
queryClient.invalidateQueries(EVENTS_TABLE);
}, },
}); });
const applyDelay = useMutation(requestApplyDelay, { const applyDelay = useMutation(requestApplyDelay, {
// Mutation finished, failed or successful // Mutation finished, failed or successful
onSettled: () => { onSettled: () => {
queryClient.invalidateQueries(eventsNamespace); queryClient.invalidateQueries(EVENTS_TABLE);
}, },
}); });
@@ -168,17 +201,17 @@ export default function EventListWrapper() {
// we optimistically update here // we optimistically update here
onMutate: async (data) => { onMutate: async (data) => {
// cancel ongoing queries // cancel ongoing queries
queryClient.cancelQueries(eventsNamespace, { exact: true }); queryClient.cancelQueries(EVENTS_TABLE, { exact: true });
// Snapshot the previous value // Snapshot the previous value
const previousEvents = queryClient.getQueryData(eventsNamespace); const previousEvents = queryClient.getQueryData(EVENTS_TABLE);
const e = [...previousEvents]; const e = [...previousEvents];
const [reorderedItem] = e.splice(data.from, 1); const [reorderedItem] = e.splice(data.from, 1);
e.splice(data.to, 0, reorderedItem); e.splice(data.to, 0, reorderedItem);
// optimistically update object // optimistically update object
queryClient.setQueryData(eventsNamespace, e); queryClient.setQueryData(EVENTS_TABLE, e);
// Return a context with the previous and new todo // Return a context with the previous and new todo
return { previousEvents }; return { previousEvents };
@@ -186,12 +219,12 @@ export default function EventListWrapper() {
// Mutation fails, rollback undos optimist update // Mutation fails, rollback undos optimist update
onError: (error, eventId, context) => { onError: (error, eventId, context) => {
queryClient.setQueryData(eventsNamespace, context.previousEvents); queryClient.setQueryData(EVENTS_TABLE, context.previousEvents);
}, },
// Mutation finished, failed or successful // Mutation finished, failed or successful
// Fetch anyway, just to be sure // Fetch anyway, just to be sure
onSettled: () => { onSettled: () => {
queryClient.invalidateQueries(eventsNamespace); queryClient.invalidateQueries(EVENTS_TABLE);
}, },
}); });
@@ -203,93 +236,114 @@ export default function EventListWrapper() {
}, [isError]); }, [isError]);
// Events API // Events API
const eventsHandler = useCallback(async (action, payload) => { const eventsHandler = useCallback(
switch (action) { async (action, payload) => {
case 'add': switch (action) {
try { case 'add':
let t = Date.now();
await addEvent.mutateAsync(payload);
console.log('debug m add', Date.now() - t);
} catch (error) {
showErrorToast('Error creating event', error.message);
}
break;
case 'update':
try {
let t = Date.now();
await updateEvent.mutateAsync(payload);
console.log('debug m update', Date.now() - t);
} catch (error) {
showErrorToast('Error updating event', error.message);
}
break;
case 'patch':
try {
let t = Date.now();
await patchEvent.mutateAsync(payload);
console.log('debug m patch', Date.now() - t);
} catch (error) {
showErrorToast('Error updating event', error.message);
}
break;
case 'delete':
try {
let t = Date.now();
await deleteEvent.mutateAsync(payload);
console.log('debug m delete', Date.now() - t);
} catch (error) {
showErrorToast('Error deleting event', error.message);
}
break;
case 'reorder':
try {
let t = Date.now();
await reorderEvent.mutateAsync(payload);
console.log('debug m reorder', Date.now() - t);
} catch (error) {
showErrorToast('Error reordering event', error.message);
}
break;
case 'applyDelay':
let t = Date.now();
// if delay <= 0 delete delay and next block
if (payload.duration <= 0) {
try { try {
// look for block after let t = Date.now();
let afterId = false; await addEvent.mutateAsync(payload);
let blockAfter = null; console.log('debug m add', Date.now() - t);
for (const d of data) { } catch (error) {
if (d.id === payload.id) afterId = true; showErrorToast('Error creating event', error.message);
if (afterId && d.type === 'block') { }
blockAfter = d.id; break;
break; case 'update':
try {
let t = Date.now();
await updateEvent.mutateAsync(payload);
console.log('debug m update', Date.now() - t);
} catch (error) {
showErrorToast('Error updating event', error.message);
}
break;
case 'patch':
try {
let t = Date.now();
await patchEvent.mutateAsync(payload);
console.log('debug m patch', Date.now() - t);
} catch (error) {
showErrorToast('Error updating event', error.message);
}
break;
case 'delete':
try {
let t = Date.now();
await deleteEvent.mutateAsync(payload);
console.log('debug m delete', Date.now() - t);
} catch (error) {
showErrorToast('Error deleting event', error.message);
}
break;
case 'reorder':
try {
let t = Date.now();
await reorderEvent.mutateAsync(payload);
console.log('debug m reorder', Date.now() - t);
} catch (error) {
showErrorToast('Error reordering event', error.message);
}
break;
case 'applyDelay':
let t = Date.now();
// if delay <= 0 delete delay and next block
if (payload.duration <= 0) {
try {
// look for block after
let afterId = false;
let blockAfter = null;
for (const d of data) {
if (d.id === payload.id) afterId = true;
if (afterId && d.type === 'block') {
blockAfter = d.id;
break;
}
} }
// delete delay
await deleteEvent.mutateAsync(payload.id);
// delete block after, if any
if (blockAfter) await deleteEvent.mutateAsync(blockAfter);
} catch (error) {
showErrorToast('Error applying delay', error.message);
}
} else {
console.log('debug applydelay', payload.id);
try {
await applyDelay.mutateAsync(payload.id);
} catch (error) {
showErrorToast('Error applying delay', error.message);
} }
// delete delay
await deleteEvent.mutateAsync(payload.id);
// delete block after, if any
if (blockAfter) await deleteEvent.mutateAsync(blockAfter);
} catch (error) {
showErrorToast('Error applying delay', error.message);
} }
} else { console.log('debug m apply', Date.now() - t);
console.log('debug applydelay', payload.id); break;
case 'collapseall':
if (data == null) return;
setCollapsed({ clear: true, items: data, isCollapsed: true });
break;
case 'expandall':
if (data == null) return;
setCollapsed({ clear: true, items: data, isCollapsed: false });
break;
case 'deleteall':
try { try {
await applyDelay.mutateAsync(payload.id); let t = Date.now();
await deleteAllEvents.mutateAsync();
console.log('debug m deleteall', Date.now() - t);
} catch (error) { } catch (error) {
showErrorToast('Error applying delay', error.message); showErrorToast('Error deleting events', error.message);
} }
} break;
console.log('debug m apply', Date.now() - t); default:
showErrorToast('Unrecognised request', action);
break; break;
default: }
showErrorToast('Unrecognised request', action); },
break; [data]
} );
}, []);
return ( return (
<> <>
@@ -21,11 +21,14 @@
} }
.cursor { .cursor {
width: 90%; width: 100%;
min-height: 2px; background: linear-gradient(
background-color: #ff7597; 180deg,
border-radius: 2px; #ff7597 2%,
margin: 0 auto; #0001 3%,
/* transition: 1s; #0001 97%,
transition-property: all; */ #ff7597 98%
);
border-radius: 14px;
} }
+32 -49
View File
@@ -1,31 +1,16 @@
import { memo } from 'react'; import { memo, useMemo } from 'react';
import { FiChevronDown } from 'react-icons/fi'; import { Divider } from '@chakra-ui/react';
import {
Button,
ButtonGroup,
Menu,
MenuButton,
MenuItem,
MenuList,
} from '@chakra-ui/react';
import style from './EventListMenu.module.css'; import style from './EventListMenu.module.css';
import MenuActionButtons from '../editors/list/MenuActionButtons'; import MenuActionButtons from './MenuActionButtons';
import CollapseBtn from 'common/components/buttons/CollapseBtn';
import ExpandBtn from 'common/components/buttons/ExpandBtn';
import { SelectSetting, HandleOptions } from 'app/context/settingsAtom';
import { useAtom } from 'jotai';
import LockIconBtn from 'common/components/buttons/LockIconBtn';
const EventListMenu = ({ eventsHandler }) => { const EventListMenu = ({ eventsHandler }) => {
const buttonProps = { const [cursorSettings] = useAtom(useMemo(() => SelectSetting('cursor'), []));
size: 'sm', const [, SetOption] = useAtom(HandleOptions);
variant: 'outline',
colorScheme: 'whiteAlpha',
backgroundColor: '#ffffff05',
_hover: { bg: 'blue.800' },
_expanded: { bg: 'blue.400' },
_focus: { boxShadow: 'none' },
};
const menuStyle = {
color: 'initial',
backgroundColor: 'rgba(255,255,255,0.67)',
};
const actionHandler = (action) => { const actionHandler = (action) => {
switch (action) { switch (action) {
@@ -38,6 +23,16 @@ const EventListMenu = ({ eventsHandler }) => {
case 'block': case 'block':
eventsHandler('add', { type: action, order: 0 }); eventsHandler('add', { type: action, order: 0 });
break; break;
case 'togglelock':
let newSet = 'locked';
if (cursorSettings === 'locked') {
newSet = 'unlocked';
}
SetOption({ cursor: newSet });
break;
case 'deleteall':
eventsHandler('deleteall');
break;
default: default:
break; break;
} }
@@ -45,30 +40,18 @@ const EventListMenu = ({ eventsHandler }) => {
return ( return (
<div className={style.headerButtons}> <div className={style.headerButtons}>
<Menu className={style.menu} isLazy> <CollapseBtn
<ButtonGroup isAttached> size='sm'
<Button {...buttonProps}>Upload</Button> clickhandler={() => eventsHandler('collapseall')}
<MenuButton as={Button} {...buttonProps}> />
<FiChevronDown /> <ExpandBtn size='sm' clickhandler={() => eventsHandler('expandall')} />
</MenuButton> <Divider orientation='vertical' />
</ButtonGroup> <LockIconBtn
<MenuList style={menuStyle}> size='sm'
<MenuItem>Upload Excel</MenuItem> clickhandler={() => actionHandler('togglelock')}
<MenuItem>Upload CSV</MenuItem> active={cursorSettings === 'locked'}
</MenuList> />
</Menu> <Divider orientation='vertical' />
<Menu>
<ButtonGroup isAttached>
<Button {...buttonProps}>Save</Button>
<MenuButton as={Button} {...buttonProps}>
<FiChevronDown />
</MenuButton>
</ButtonGroup>
<MenuList style={menuStyle}>
<MenuItem>Download Excel</MenuItem>
<MenuItem>Download CSV</MenuItem>
</MenuList>
</Menu>
<MenuActionButtons actionHandler={actionHandler} size='sm' /> <MenuActionButtons actionHandler={actionHandler} size='sm' />
</div> </div>
); );
@@ -1,7 +1,13 @@
import { Menu, MenuButton, MenuItem, MenuList } from '@chakra-ui/menu'; import { Menu, MenuButton, MenuItem, MenuList } from '@chakra-ui/menu';
import { IconButton } from '@chakra-ui/button'; import { IconButton } from '@chakra-ui/button';
import { FiZap, FiPlus, FiClock, FiMinusCircle } from 'react-icons/fi'; import {
import { useEffect } from 'react'; FiTrash2,
FiZap,
FiPlus,
FiClock,
FiMinusCircle,
} from 'react-icons/fi';
import { Divider } from '@chakra-ui/layout';
export default function MenuActionButtons(props) { export default function MenuActionButtons(props) {
const { actionHandler } = props; const { actionHandler } = props;
@@ -10,10 +16,6 @@ export default function MenuActionButtons(props) {
backgroundColor: 'rgba(255,255,255,1)', backgroundColor: 'rgba(255,255,255,1)',
}; };
useEffect(() =>{
console.log('debug action button render')
})
return ( return (
<Menu isLazy lazyBehavior='unmount'> <Menu isLazy lazyBehavior='unmount'>
<MenuButton <MenuButton
@@ -21,15 +23,12 @@ export default function MenuActionButtons(props) {
aria-label='Options' aria-label='Options'
size={props.size || 'xs'} size={props.size || 'xs'}
icon={<FiZap />} icon={<FiZap />}
_expanded={{ bg: 'pink.300', color: 'white' }} _expanded={{ bg: 'orange.300', color: 'white' }}
_focus={{ boxShadow: 'none' }} _focus={{ boxShadow: 'none' }}
backgroundColor={'orange.200'} backgroundColor={'orange.200'}
color={'orange.500'} color={'orange.500'}
/> />
<MenuList style={menuStyle}> <MenuList style={menuStyle}>
{/* <MenuItem icon={<FiTrash2 />} onClick={props.deleteAllHandler}>
Delete All
</MenuItem> */}
<MenuItem icon={<FiPlus />} onClick={() => actionHandler('event')}> <MenuItem icon={<FiPlus />} onClick={() => actionHandler('event')}>
Event first Event first
</MenuItem> </MenuItem>
@@ -42,6 +41,14 @@ export default function MenuActionButtons(props) {
> >
Block first Block first
</MenuItem> </MenuItem>
<Divider />
<MenuItem
icon={<FiTrash2 />}
onClick={() => actionHandler('deleteall')}
color='red.500'
>
Delete All
</MenuItem>
</MenuList> </MenuList>
</Menu> </Menu>
); );
+30
View File
@@ -0,0 +1,30 @@
import { downloadEvents } from 'app/api/ontimeApi';
import DownloadIconBtn from './buttons/DownloadIconBtn';
import SettingsIconBtn from './buttons/SettingsIconBtn';
import InfoIconBtn from './buttons/InfoIconBtn';
import MaxIconBtn from './buttons/MaxIconBtn';
import MinIconBtn from './buttons/MinIconBtn';
import QuitIconBtn from './buttons/QuitIconBtn';
import style from './MenuBar.module.css';
import HelpIconBtn from './buttons/HelpIconBtn';
export default function MenuBar(props) {
const { onOpen, onClose } = props;
const handleDownload = () => {
downloadEvents();
};
return (
<>
<QuitIconBtn size='md' />
<MaxIconBtn size='md' />
<MinIconBtn size='md' />
<div className={style.gap} />
<HelpIconBtn size='md' disabled />
<SettingsIconBtn size='md' disabled />
<InfoIconBtn size='md' clickhandler={onOpen} />
<DownloadIconBtn size='md' clickhandler={handleDownload} />
</>
);
}
@@ -0,0 +1,3 @@
.gap {
height: 1em;
}
@@ -0,0 +1,22 @@
import { IconButton } from '@chakra-ui/button';
import { Tooltip } from '@chakra-ui/tooltip';
import { FiDownload } from 'react-icons/fi';
export default function DownloadIconBtn(props) {
const { clickhandler, ...rest } = props;
return (
<Tooltip label='Download File'>
<IconButton
size={props.size || 'xs'}
icon={<FiDownload />}
colorScheme='white'
variant='outline'
isRound
borderColor='#fff1'
onClick={clickhandler}
_focus={{ boxShadow: 'none' }}
{...rest}
/>
</Tooltip>
);
}
@@ -0,0 +1,22 @@
import { IconButton } from '@chakra-ui/button';
import { Tooltip } from '@chakra-ui/tooltip';
import { FiHelpCircle } from 'react-icons/fi';
export default function HelpIconBtn(props) {
const { clickhandler, ...rest } = props;
return (
<Tooltip label='Help'>
<IconButton
size={props.size || 'xs'}
icon={<FiHelpCircle />}
colorScheme='white'
variant='outline'
isRound
borderColor='#fff1'
onClick={clickhandler}
_focus={{ boxShadow: 'none' }}
{...rest}
/>
</Tooltip>
);
}
@@ -0,0 +1,22 @@
import { IconButton } from '@chakra-ui/button';
import { Tooltip } from '@chakra-ui/tooltip';
import { FiHome } from 'react-icons/fi';
export default function InfoIconBtn(props) {
const { clickhandler, ...rest } = props;
return (
<Tooltip label='Event Main'>
<IconButton
size={props.size || 'xs'}
icon={<FiHome />}
colorScheme='white'
variant='outline'
isRound
borderColor='#fff1'
onClick={clickhandler}
_focus={{ boxShadow: 'none' }}
{...rest}
/>
</Tooltip>
);
}
@@ -0,0 +1,22 @@
import { IconButton } from '@chakra-ui/button';
import { Tooltip } from '@chakra-ui/tooltip';
import { FiMaximize } from 'react-icons/fi';
export default function MaxIconBtn(props) {
const { clickhandler, ...rest } = props;
return (
<Tooltip label='Show full window'>
<IconButton
size={props.size || 'xs'}
icon={<FiMaximize />}
colorScheme='white'
variant='outline'
isRound
borderColor='#fff1'
onClick={clickhandler}
_focus={{ boxShadow: 'none' }}
{...rest}
/>
</Tooltip>
);
}
@@ -0,0 +1,22 @@
import { IconButton } from '@chakra-ui/button';
import { Tooltip } from '@chakra-ui/tooltip';
import { FiMinimize } from 'react-icons/fi';
export default function MinIconBtn(props) {
const { clickhandler, ...rest } = props;
return (
<Tooltip label='Close to tray'>
<IconButton
size={props.size || 'xs'}
icon={<FiMinimize />}
colorScheme='white'
variant='outline'
isRound
borderColor='#fff1'
onClick={clickhandler}
_focus={{ boxShadow: 'none' }}
{...rest}
/>
</Tooltip>
);
}
@@ -0,0 +1,21 @@
import { IconButton } from '@chakra-ui/button';
import { Tooltip } from '@chakra-ui/tooltip';
import { FiPower } from 'react-icons/fi';
export default function QuitIconBtn(props) {
const { clickhandler, ...rest } = props;
return (
<Tooltip label='Quit Application'>
<IconButton
size={props.size || 'xs'}
icon={<FiPower />}
colorScheme='red'
variant='outline'
isRound
onClick={clickhandler}
_focus={{ boxShadow: 'none' }}
{...rest}
/>
</Tooltip>
);
}
@@ -0,0 +1,22 @@
import { IconButton } from '@chakra-ui/button';
import { Tooltip } from '@chakra-ui/tooltip';
import { FiSettings } from 'react-icons/fi';
export default function SettingsIconBtn(props) {
const { clickhandler, ...rest } = props;
return (
<Tooltip label='Application Settings'>
<IconButton
size={props.size || 'xs'}
icon={<FiSettings />}
colorScheme='white'
variant='outline'
isRound
borderColor='#fff1'
onClick={clickhandler}
_focus={{ boxShadow: 'none' }}
{...rest}
/>
</Tooltip>
);
}
+3 -2
View File
@@ -14,12 +14,13 @@ import {
Button, Button,
Textarea, Textarea,
} from '@chakra-ui/react'; } from '@chakra-ui/react';
import { fetchEvent, postEvent, eventNamespace } from 'app/api/eventApi'; import { fetchEvent, postEvent } from 'app/api/eventApi';
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
import { useFetch } from 'app/hooks/useFetch'; import { useFetch } from 'app/hooks/useFetch';
import { EVENT_TABLE } from 'app/api/apiConstants';
export default function SettingsModal(props) { export default function SettingsModal(props) {
const { data, status, isError } = useFetch(eventNamespace, fetchEvent); const { data, status, isError } = useFetch(EVENT_TABLE, fetchEvent);
const [formData, setFormData] = useState({ const [formData, setFormData] = useState({
title: '', title: '',
url: '', url: '',
+7 -6
View File
@@ -1,9 +1,10 @@
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
import { eventsNamespace, fetchAllEvents } from 'app/api/eventsApi'; import { fetchAllEvents } from 'app/api/eventsApi';
import { fetchEvent, eventNamespace } from 'app/api/eventApi'; import { fetchEvent } from 'app/api/eventApi';
import { useSocket } from 'app/context/socketContext'; import { useSocket } from 'app/context/socketContext';
import { stringFromMillis } from 'common/dateConfig'; import { stringFromMillis } from 'common/dateConfig';
import { useFetch } from 'app/hooks/useFetch'; import { useFetch } from 'app/hooks/useFetch';
import { EVENTS_TABLE, EVENT_TABLE } from 'app/api/apiConstants';
const withSocket = (Component) => { const withSocket = (Component) => {
const WrappedComponent = (props) => { const WrappedComponent = (props) => {
@@ -11,12 +12,12 @@ const withSocket = (Component) => {
data: eventsData, data: eventsData,
status: eventsDataStatus, status: eventsDataStatus,
isError: eventsDataIsError, isError: eventsDataIsError,
} = useFetch(eventsNamespace, fetchAllEvents); } = useFetch(EVENTS_TABLE, fetchAllEvents);
const { const {
data: genData, data: genData,
status: genDataStatus, status: genDataStatus,
isError: genDataIsError, isError: genDataIsError,
} = useFetch(eventNamespace, fetchEvent); } = useFetch(EVENT_TABLE, fetchEvent);
const [publicEvents, setPublicEvents] = useState([]); const [publicEvents, setPublicEvents] = useState([]);
const [backstageEvents, setBackstageEvents] = useState([]); const [backstageEvents, setBackstageEvents] = useState([]);
@@ -36,7 +37,7 @@ const withSocket = (Component) => {
}); });
const [timer, setTimer] = useState({ const [timer, setTimer] = useState({
clock: null, clock: null,
currentSeconds: null, running: null,
startedAt: null, startedAt: null,
expectedFinish: null, expectedFinish: null,
}); });
@@ -220,7 +221,7 @@ const withSocket = (Component) => {
// get clock string // get clock string
const timeManager = { const timeManager = {
...timer, ...timer,
finished: timer.running <= 0 && timer.startedAt, finished: playback === 'start' && timer.running <= 0 && timer.startedAt,
clock: stringFromMillis(timer.clock), clock: stringFromMillis(timer.clock),
playstate: playback, playstate: playback,
}; };
@@ -42,7 +42,7 @@ export default function StageManager(props) {
// Format messages // Format messages
const showPubl = publ.text !== '' && publ.visible; const showPubl = publ.text !== '' && publ.visible;
let stageTimer = formatDisplay(Math.abs(time.running), true); let stageTimer = formatDisplay(Math.abs(time.running), true);
if (time.running < 0) stageTimer = '-' + stageTimer; if (time.running < 0) stageTimer = `-${stageTimer}`;
// motion // motion
const titleVariants = { const titleVariants = {
@@ -14,10 +14,9 @@ export default function PresenterView(props) {
document.title = 'ontime - Speaker Screen'; document.title = 'ontime - Speaker Screen';
}, []); }, []);
const showOverlay = pres.text !== '' && pres.visible; const showOverlay = pres.text !== '' && pres.visible;
const isPlaying = time.playstate === 'start'; const isPlaying = time.playstate !== 'pause';
const normalisedTime = Math.max(time.running, 0);
// motion // motion
const titleVariants = { const titleVariants = {
@@ -61,7 +60,7 @@ export default function PresenterView(props) {
<div className={style.finished}>TIME UP</div> <div className={style.finished}>TIME UP</div>
) : ( ) : (
<div className={isPlaying ? style.countdown : style.countdownPaused}> <div className={isPlaying ? style.countdown : style.countdownPaused}>
<Countdown time={time.currentSeconds} hideZeroHours /> <Countdown time={normalisedTime} hideZeroHours />
</div> </div>
)} )}
</div> </div>
@@ -73,7 +72,7 @@ export default function PresenterView(props) {
} }
> >
<MyProgressBar <MyProgressBar
now={time.currentSeconds} now={normalisedTime}
complete={time.durationSeconds} complete={time.durationSeconds}
showElapsed showElapsed
/> />
@@ -3,7 +3,7 @@ import style from './Pip.module.css';
import Paginator from 'common/components/views/Paginator'; import Paginator from 'common/components/views/Paginator';
import NavLogo from 'common/components/nav/NavLogo'; import NavLogo from 'common/components/nav/NavLogo';
import { AnimatePresence, motion } from 'framer-motion'; import { AnimatePresence, motion } from 'framer-motion';
import { useEffect, useRef, useState } from 'react'; import { useEffect, useLayoutEffect, useRef, useState } from 'react';
import { formatDisplay } from 'common/dateConfig'; import { formatDisplay } from 'common/dateConfig';
import { ReactComponent as Emptyimage } from 'assets/images/empty.svg'; import { ReactComponent as Emptyimage } from 'assets/images/empty.svg';
@@ -14,7 +14,7 @@ export default function Pip(props) {
const [filteredEvents, setFilteredEvents] = useState(null); const [filteredEvents, setFilteredEvents] = useState(null);
// calculcate pip size // calculcate pip size
useEffect(() => { useLayoutEffect(() => {
const h = ref.current.clientHeight; const h = ref.current.clientHeight;
const w = ref.current.clientWidth; const w = ref.current.clientWidth;
setSize(`${w} x ${h}`); setSize(`${w} x ${h}`);
@@ -51,11 +51,8 @@ export default function Pip(props) {
// Format messages // Format messages
const showInfo = const showInfo =
general.backstageInfo !== '' && general.backstageInfo != null; general.backstageInfo !== '' && general.backstageInfo != null;
let stageTimer = formatDisplay(Math.abs(time.running), true);
const stageTimer = if (time.running < 0) stageTimer = `-${stageTimer}`;
time.currentSeconds != null && !isNaN(time.currentSeconds)
? formatDisplay(time.currentSeconds, true)
: '';
return ( return (
<div className={style.container__gray}> <div className={style.container__gray}>
+5
View File
@@ -7575,6 +7575,11 @@ jest@26.6.0:
import-local "^3.0.2" import-local "^3.0.2"
jest-cli "^26.6.0" jest-cli "^26.6.0"
jotai@^0.16.5:
version "0.16.5"
resolved "https://registry.yarnpkg.com/jotai/-/jotai-0.16.5.tgz#493bab65b69c045c508d8a3a4996fcc4d75e8228"
integrity sha512-exiddLOPp22P0c7z+X5jQEraoEdFm80BaNeuT2WTMAFNRZY2l1XmefR/PXj9JEaAMrHRQk7BKbJ51C5AddoSWA==
js-sha3@0.8.0: js-sha3@0.8.0:
version "0.8.0" version "0.8.0"
resolved "https://registry.yarnpkg.com/js-sha3/-/js-sha3-0.8.0.tgz#b9b7a5da73afad7dedd0f8c463954cbde6818840" resolved "https://registry.yarnpkg.com/js-sha3/-/js-sha3-0.8.0.tgz#b9b7a5da73afad7dedd0f8c463954cbde6818840"
-83
View File
@@ -1,83 +0,0 @@
// get config
const config = require('./config.json');
// init database
const low = require('lowdb');
const FileSync = require('lowdb/adapters/FileSync');
const adapter = new FileSync(config.database.filename);
const db = low(adapter);
// dependencies
const express = require('express');
const http = require('http');
const cors = require('cors');
const { dbModel } = require('./data/dataModel.js');
const path = require('path');
db.defaults(dbModel).write();
// export db
module.exports.db = db;
// Import Routes
const eventsRouter = require('./routes/eventsRouter.js');
const eventRouter = require('./routes/eventRouter.js');
// No settings yet
// const settingsRouter = require('./routes/settingsRouter.js');
// Setup default port
const port = process.env.PORT || config.server.port;
// Global Objects
const EventTimer = require('./classes/EventTimer.js');
// Create express APP
const app = express();
// setup cors for all routes
app.use(cors());
// enable pre-flight cors
app.options('*', cors());
// Implement middleware
app.use(express.urlencoded({ extended: true }));
app.use(express.json());
// Implement route endpoints
app.use('/events', eventsRouter);
app.use('/event', eventRouter);
// serve react
app.use(express.static(path.join(__dirname, '../client/build')));
app.get('*', (req, res) => {
res.sendFile(path.resolve(__dirname, '../client', 'build', 'index.html'));
});
// Implement route for errors
app.use((err, req, res, next) => {
console.error(err.stack);
res.status(500).send(err.stack);
});
// create HTTP server
const server = http.createServer(app);
// get data (if any)
const eventlist = db.get('events').value();
// init timer
global.timer = new EventTimer(server, config);
global.timer.setupWithEventList(eventlist);
// Start server
server.listen(port, '0.0.0.0', () =>
console.log(`HTTP Server is listening on port ${port}`)
);
// Start OSC server
const { initiateOSC } = require('./controllers/OscController.js');
initiateOSC(config.osc);
-15
View File
@@ -1,15 +0,0 @@
{
"timer": {
"refresh": 1000
},
"server": {
"port": 4001
},
"database": {
"filename": "db.json",
"tablename": "events"
},
"osc": {
"port": 8888
}
}
-144
View File
@@ -1,144 +0,0 @@
// get database
const { db } = require('../app.js');
const table = 'event';
function getSettings() {
return db.get(table).value();
}
// Create controller for GET request to 'event'
// Returns ACK message
exports.getAll = async (req, res) => {
const settings = getSettings();
if (settings) res.json(settings);
else res.sendStatus(400);
};
// Create controller for POST request to 'event'
// Returns ACK message
exports.post = async (req, res) => {
if (!req.body) {
res.status(400).send('No object found in request');
return;
}
// TODO: validate data
try {
db.get(table)
.assign({ ...req.body })
.write();
res.sendStatus(200);
} catch (error) {
res.status(400).send(error);
}
};
// Create controller for GET request to 'event/title'
// Returns ACK message
exports.titleGet = async (req, res) => {
const settings = getSettings();
if (settings) res.json(settings.title);
else res.sendStatus(400);
};
// Create controller for POST request to 'event/title'
// Returns ACK message
exports.titlePost = async (req, res) => {
if (!req.body) {
res.status(400).send('No object found in request');
return;
}
// TODO: validate data
try {
db.get(table).assign({ title: req.body.title }).write();
res.sendStatus(200);
} catch (error) {
res.status(400).send(error);
}
};
// Create controller for GET request to '/event/url'
// Returns ACK message
exports.urlGet = async (req, res) => {
const event = getSettings();
if (event) res.json(event.url);
else res.sendStatus(400);
};
// Create controller for POST request to '/event/url'
// Returns ACK message
exports.urlPost = async (req, res) => {
if (!req.body) {
res.status(400).send('No object found in request');
return;
}
// TODO: validate data
try {
db.get(table).assign({ url: req.body.url }).write();
res.sendStatus(200);
} catch (error) {
res.status(400).send(error);
}
};
// Create controller for GET request to 'event/publicInfo'
// Returns ACK message
exports.publicInfoGet = async (req, res) => {
const settings = getSettings();
if (settings) res.json(settings.publicInfo);
else res.sendStatus(400);
};
// Create controller for POST request to '/event/publicInfo'
// Returns ACK message
exports.publicInfoPost = async (req, res) => {
if (!req.body) {
res.status(400).send('No object found in request');
return;
}
// TODO: validate data
try {
db.get(table).assign({ publicInfo: req.body.publicInfo }).write();
res.sendStatus(200);
} catch (error) {
res.status(400).send(error);
}
};
// Create controller for GET request to 'event/backstageInfo'
// Returns ACK message
exports.backstageInfoGet = async (req, res) => {
const settings = getSettings();
if (settings) res.json(settings.backstageInfo);
else res.sendStatus(400);
};
// Create controller for POST request to '/event/info'
// Returns ACK message
exports.backstageInfoPost = async (req, res) => {
if (!req.body) {
res.status(400).send('No object found in request');
return;
}
// TODO: validate data
try {
db.get(table).assign({ backstageInfo: req.body.backstageInfo }).write();
res.sendStatus(200);
} catch (error) {
res.status(400).send(error);
}
};
// Create controller for GET request to 'event/osc'
// Returns ACK message
exports.osc = async (req, res) => {
res.send('Not yet implemented').status(500);
};
View File
-57
View File
@@ -1,57 +0,0 @@
[
{
"id":"xxxxx0",
"duration": 300000,
"type": "delay"
},
{
"id":"xxxxx1",
"title": "Is the internet a fad?",
"subtitle": "It is",
"presenter": "Carlos Valente",
"timeStart": 32400000,
"timeEnd": 34200000,
"type": "event"
},
{
"id":"xxxxx2",
"duration": 1500000,
"type": "delay"
},
{
"id":"xxxxx3",
"title": "Is reddit a dictatorship?",
"subtitle": "It is",
"presenter": "Carlos Valente",
"timeStart": 34200000,
"timeEnd": 36000000,
"type": "event"
},
{
"id":"xxxxx4",
"title": "Out of words",
"subtitle": "",
"presenter": "Carlos Valente",
"timeStart": 36000000,
"timeEnd": 39600000,
"type": "event"
},
{
"id":"xxxxx5",
"title": "Really",
"subtitle": "",
"presenter": "Carlos Valente",
"timeStart": 39600000,
"timeEnd": 41400000,
"type": "event"
},
{
"id":"xxxxx6",
"title": "...",
"subtitle": "",
"presenter": "Carlos Valente",
"timeStart": null,
"timeEnd": null,
"type": "event"
}
]
-8
View File
@@ -1,8 +0,0 @@
{
"current": null,
"next": null,
"currentTimer": null,
"numEvents": 0,
"state": "pause",
"prevState": "pause"
}
+3
View File
@@ -0,0 +1,3 @@
{
"exclude": ["node_modules", "dist"]
}
+4 -2
View File
@@ -3,14 +3,16 @@
"version": "0.1.0", "version": "0.1.0",
"author": "Carlos Valente", "author": "Carlos Valente",
"description": "Time keeping for live events", "description": "Time keeping for live events",
"type": "module",
"main": "main.js", "main": "main.js",
"dependencies": { "dependencies": {
"body-parser": "~1.19.0", "body-parser": "~1.19.0",
"express": "~4.17.1", "express": "~4.17.1",
"express-session": "~1.17.1", "express-session": "~1.17.1",
"lowdb": "^1.0.0", "lowdb": "2.1.0",
"multer": "^1.4.2",
"nanoid": "^3.1.22", "nanoid": "^3.1.22",
"node-osc": "^6.0.1", "node-osc": "6.0.2",
"passport": "~0.4.1", "passport": "~0.4.1",
"passport-local": "~1.0.0", "passport-local": "~1.0.0",
"socket.io": "^4.0.0" "socket.io": "^4.0.0"
-37
View File
@@ -1,37 +0,0 @@
const express = require('express');
const router = express.Router();
// import event controller
const eventController = require('../controllers/eventController');
// create route between controller and '/settings' endpoint
router.get('/', eventController.getAll);
// create route between controller and '/settings' endpoint
router.post('/', eventController.post);
// create route between controller and '/event/title' endpoint
router.get('/title', eventController.titleGet);
// create route between controller and '/event/title' endpoint
router.post('/title', eventController.titlePost);
// create route between controller and '/event/info' endpoint
router.get('/publicInfo', eventController.publicInfoGet);
// create route between controller and '/event/info' endpoint
router.post('/publicInfo', eventController.publicInfoPost);
// create route between controller and '/event/info' endpoint
router.get('/backstageInfo', eventController.backstageInfoGet);
// create route between controller and '/event/info' endpoint
router.post('/backstageInfo', eventController.backstageInfoPost);
// create route between controller and '/event/url' endpoint
router.get('/url', eventController.urlGet);
// create route between controller and '/event/url' endpoint
router.post('/url', eventController.urlPost);
module.exports = router;
-31
View File
@@ -1,31 +0,0 @@
const express = require('express');
const router = express.Router();
// import events controller
const eventsController = require('../controllers/eventsController');
// create route between controller and '/events/' endpoint
router.get('/', eventsController.eventsGetAll);
// create route between controller and '/events/:eventId' endpoint
router.get('/:eventId', eventsController.eventsGetById);
// create route between controller and '/events/' endpoint
router.post('/', eventsController.eventsPost);
// create route between controller and '/events/' endpoint
router.put('/', eventsController.eventsPut);
// create route between controller and '/events/' endpoint
router.patch('/', eventsController.eventsPatch);
// create route between controller and '/events/reorder' endpoint
router.patch('/reorder/', eventsController.eventsReorder);
// create route between controller and '/events/applydelay/:eventId' endpoint
router.patch('/applydelay/:eventId', eventsController.eventsApplyDelay);
// create route between controller and '/events/:eventId' endpoint
router.delete('/:eventId', eventsController.eventsDelete);
module.exports = router;
+89
View File
@@ -0,0 +1,89 @@
// get config
import { config } from './config/config.js';
// init database
import { Low, JSONFile } from 'lowdb';
import { join } from 'path';
const file = join('data/', config.database.filename);
const adapter = new JSONFile(file);
export const db = new Low(adapter);
// dependencies
import express from 'express';
import http from 'http';
import cors from 'cors';
import { dbModel } from './data/dataModel.js';
// Read data from JSON file, this will set db.data content
await db.read();
// If file.json doesn't exist, db.data will be null
// Set default data
// db.data ||= { events: [] }; NODE v15 - v16
if (db.data == null) {
db.data = dbModel;
db.write();
}
// get data
export const data = db.data;
// Import Routes
import { router as eventsRouter } from './routes/eventsRouter.js';
import { router as eventRouter } from './routes/eventRouter.js';
import { router as ontimeRouter } from './routes/ontimeRouter.js';
// Setup default port
const port = process.env.PORT || config.server.port;
// Global Objects
import { EventTimer } from './classes/EventTimer.js';
// Create express APP
const app = express();
// setup cors for all routes
app.use(cors());
// enable pre-flight cors
app.options('*', cors());
// Implement middleware
app.use('/uploads', express.static('uploads'));
app.use(express.urlencoded({ extended: true }));
app.use(express.json({ limit: '1mb' }));
// Implement route endpoints
app.use('/events', eventsRouter);
app.use('/event', eventRouter);
app.use('/ontime', ontimeRouter);
// implement general router
app.get('/', (req, res) => {
res.send('ontime API');
});
// Implement route for errors
app.use((err, req, res, next) => {
console.error(err.stack);
res.status(500).send('Something broke!');
});
// create HTTP server
const server = http.createServer(app);
// init timer
global.timer = new EventTimer(server, config);
global.timer.setupWithEventList(data.events);
// Start server
server.listen(port, () =>
console.log(`HTTP Server is listening on port ${port}`)
);
// Start OSC server
import { initiateOSC } from './controllers/OscController.js';
initiateOSC(config.osc);
@@ -1,5 +1,5 @@
const Timer = require('./Timer'); import { Timer } from './Timer.js';
const socketIo = require('socket.io'); import { Server } from 'socket.io';
/* /*
* EventTimer adds functions specific to APP * EventTimer adds functions specific to APP
@@ -9,7 +9,7 @@ const socketIo = require('socket.io');
* *
*/ */
class EventTimer extends Timer { export class EventTimer extends Timer {
// AUX // AUX
DAYMS = 86400000; DAYMS = 86400000;
@@ -57,12 +57,12 @@ class EventTimer extends Timer {
numEvents = null; numEvents = null;
_eventlist = null; _eventlist = null;
constructor(server, config) { constructor(httpServer, config) {
// call super constructor // call super constructor
super(); super();
// initialise socketIO server // initialise socketIO server
this.io = socketIo(server, { this.io = new Server(httpServer, {
cors: { cors: {
origin: '*', origin: '*',
methods: 'GET,HEAD,PUT,PATCH,POST,DELETE', methods: 'GET,HEAD,PUT,PATCH,POST,DELETE',
@@ -90,6 +90,10 @@ class EventTimer extends Timer {
broadcastState() { broadcastState() {
this.io.emit('timer', this.getObject()); this.io.emit('timer', this.getObject());
this.io.emit('playstate', this.state); this.io.emit('playstate', this.state);
this.io.emit('selected', {
id: this.selectedEventId,
index: this.selectedEventIndex,
});
this.io.emit('selected-id', this.selectedEventId); this.io.emit('selected-id', this.selectedEventId);
this.io.emit('next-id', this.nextEventId); this.io.emit('next-id', this.nextEventId);
this.io.emit('publicselected-id', this.selectedPublicEventId); this.io.emit('publicselected-id', this.selectedPublicEventId);
@@ -105,8 +109,29 @@ class EventTimer extends Timer {
update() { update() {
// if there is nothing selected, no nothing // if there is nothing selected, no nothing
if (this.selectedEventId == null) return; if (this.selectedEventId == null && this.state !== 'roll') return;
super.update();
// only implement roll here
if (this.state !== 'roll') {
super.update();
return;
}
// get current time
const now = this._getCurrentTime();
this.clock = now;
if (this.selectedEventId && this.current > 0) {
// update timer as usual
this.current = this._finishAt - now;
} else {
// look for event if none is loaded
if (this.current <= 0 || this.secondaryTimer <= 0) this.rollLoad();
// count to next event
// TODO: replace with proper counter
if (this.secondaryTimer != null) this.secondaryTimer -= 1000;
}
} }
start() { start() {
@@ -136,10 +161,11 @@ class EventTimer extends Timer {
else if (payload === 'next') this.next(); else if (payload === 'next') this.next();
else if (payload === 'reload') this.reload(); else if (payload === 'reload') this.reload();
else if (payload === 'unload') this.unload(); else if (payload === 'unload') this.unload();
else if (payload === 'roll') this.roll();
// Not yet implemented // TODO: Cleanup
// else if (payload === 'roll') this.roll(); // here tdo this.broadcastState;
// remove broadcast from functions
this.broadcastThis('playstate', this.state); this.broadcastThis('playstate', this.state);
this.broadcastThis('selected-id', this.selectedEventId); this.broadcastThis('selected-id', this.selectedEventId);
this.broadcastThis('titles', this.titles); this.broadcastThis('titles', this.titles);
@@ -259,6 +285,13 @@ class EventTimer extends Timer {
/*******************************************/ /*******************************************/
// selection data // selection data
socket.on('get-selected', () => {
socket.emit('selected', {
id: this.selectedEventId,
index: this.selectedEventIndex,
});
});
socket.on('get-selected-id', () => { socket.on('get-selected-id', () => {
socket.emit('selected-id', this.selectedEventId); socket.emit('selected-id', this.selectedEventId);
}); });
@@ -342,6 +375,8 @@ class EventTimer extends Timer {
} }
setupWithEventList(eventlist) { setupWithEventList(eventlist) {
if (!Array.isArray(eventlist) || eventlist.length < 1) return;
// filter only events // filter only events
const events = eventlist.filter((e) => e.type === 'event'); const events = eventlist.filter((e) => e.type === 'event');
const numEvents = events.length; const numEvents = events.length;
@@ -441,7 +476,7 @@ class EventTimer extends Timer {
// update selected event index // update selected event index
this.selectedEventIndex = this._eventlist.findIndex( this.selectedEventIndex = this._eventlist.findIndex(
(e) => e.id === eventId (e) => e.id === this.selectedEventId
); );
// reload titles if necessary // reload titles if necessary
@@ -459,18 +494,15 @@ class EventTimer extends Timer {
if (eventIndex === -1) return; if (eventIndex === -1) return;
this.pause(); this.pause();
this.loadEvent(eventIndex); this.loadEvent(eventIndex, 'load', true);
this.broadcastState();
} }
// Loads a given event // Loads a given event
// load timers // load timers
// load selectedEventIndex // load selectedEventIndex
// load titles // load titles
loadEvent(eventIndex, type = 'load') { loadEvent(eventIndex, type = 'load', broadcastChange = 'false') {
const e = this._eventlist[eventIndex]; const e = this._eventlist[eventIndex];
if (e == null) return; if (e == null) return;
const start = e.timeStart == null || e.timeStart === '' ? 0 : e.timeStart; const start = e.timeStart == null || e.timeStart === '' ? 0 : e.timeStart;
@@ -501,6 +533,10 @@ class EventTimer extends Timer {
// look for event after // look for event after
this._loadTitlesNext(); this._loadTitlesNext();
if (broadcastChange)
// broadcast current state
this.broadcastState();
} }
_loadTitlesNow() { _loadTitlesNow() {
@@ -508,18 +544,12 @@ class EventTimer extends Timer {
if (e == null) return; if (e == null) return;
// private title is always current // private title is always current
this.titles.titleNow = e.title;
this.titles.subtitleNow = e.subtitle;
this.titles.presenterNow = e.presenter;
this.selectedEventId = e.id;
// check if current is also public // check if current is also public
if (e.isPublic) { if (e.isPublic) {
this.titlesPublic.titleNow = e.title; this._loadThisTitles(e, 'now');
this.titlesPublic.subtitleNow = e.subtitle;
this.titlesPublic.presenterNow = e.presenter;
this.selectedPublicEventId = e.id;
} else { } else {
this._loadThisTitles(e, 'now-private');
// assume there is no public event // assume there is no public event
this.titlesPublic.titleNow = null; this.titlesPublic.titleNow = null;
this.titlesPublic.subtitleNow = null; this.titlesPublic.subtitleNow = null;
@@ -535,16 +565,77 @@ class EventTimer extends Timer {
this._eventlist[i].type === 'event' && this._eventlist[i].type === 'event' &&
this._eventlist[i].isPublic this._eventlist[i].isPublic
) { ) {
this.titlesPublic.titleNow = this._eventlist[i].title; this._loadThisTitles(this._eventlist[i], 'now-public');
this.titlesPublic.subtitleNow = this._eventlist[i].subtitle;
this.titlesPublic.presenterNow = this._eventlist[i].presenter;
this.selectedPublicEventId = this._eventlist[i].id;
break; break;
} }
} }
} }
} }
_loadThisTitles(e, type) {
if (e == null) return;
switch (type) {
// now, load to both public and private
case 'now':
// public
this.titlesPublic.titleNow = e.title;
this.titlesPublic.subtitleNow = e.subtitle;
this.titlesPublic.presenterNow = e.presenter;
this.selectedPublicEventId = e.id;
// private
this.titles.titleNow = e.title;
this.titles.subtitleNow = e.subtitle;
this.titles.presenterNow = e.presenter;
this.selectedEventId = e.id;
break;
case 'now-public':
this.titlesPublic.titleNow = e.title;
this.titlesPublic.subtitleNow = e.subtitle;
this.titlesPublic.presenterNow = e.presenter;
this.selectedPublicEventId = e.id;
break;
case 'now-private':
this.titles.titleNow = e.title;
this.titles.subtitleNow = e.subtitle;
this.titles.presenterNow = e.presenter;
this.selectedEventId = e.id;
break;
// next, load to both public and private
case 'next':
// public
this.titlesPublic.titleNext = e.title;
this.titlesPublic.subtitleNext = e.subtitle;
this.titlesPublic.presenterNext = e.presenter;
this.nextPublicEventId = e.id;
// private
this.titles.titleNext = e.title;
this.titles.subtitleNext = e.subtitle;
this.titles.presenterNext = e.presenter;
this.nextEventId = e.id;
break;
case 'next-public':
this.titlesPublic.titleNext = e.title;
this.titlesPublic.subtitleNext = e.subtitle;
this.titlesPublic.presenterNext = e.presenter;
this.nextPublicEventId = e.id;
break;
case 'next-private':
this.titles.titleNext = e.title;
this.titles.subtitleNext = e.subtitle;
this.titles.presenterNext = e.presenter;
this.nextEventId = e.id;
break;
default:
break;
}
}
_loadTitlesNext() { _loadTitlesNext() {
// maybe there is nothing to load // maybe there is nothing to load
if (this.selectedEventIndex == null) return; if (this.selectedEventIndex == null) return;
@@ -569,19 +660,13 @@ class EventTimer extends Timer {
if (this._eventlist[i].type === 'event') { if (this._eventlist[i].type === 'event') {
// if we have not set private // if we have not set private
if (!nextPrivate) { if (!nextPrivate) {
this.titles.titleNext = this._eventlist[i].title; this._loadThisTitles(this._eventlist[i], 'next-private');
this.titles.subtitleNext = this._eventlist[i].subtitle;
this.titles.presenterNext = this._eventlist[i].presenter;
this.nextEventId = this._eventlist[i].id;
nextPrivate = true; nextPrivate = true;
} }
// if event is public // if event is public
if (this._eventlist[i].isPublic) { if (this._eventlist[i].isPublic) {
this.titlesPublic.titleNext = this._eventlist[i].title; this._loadThisTitles(this._eventlist[i], 'next-public');
this.titlesPublic.subtitleNext = this._eventlist[i].subtitle;
this.titlesPublic.presenterNext = this._eventlist[i].presenter;
this.nextPublicEventId = this._eventlist[i].id;
nextPublic = true; nextPublic = true;
} }
} }
@@ -628,6 +713,7 @@ class EventTimer extends Timer {
state = ${this.state} state = ${this.state}
current = ${this.current} current = ${this.current}
duration = ${this.duration} duration = ${this.duration}
secondaryTimer = ${this.secondaryTimer}
Events Events
------------------------------ ------------------------------
@@ -716,10 +802,77 @@ class EventTimer extends Timer {
this.broadcastState(); this.broadcastState();
} }
rollLoad() {
const now = this._getCurrentTime();
this._resetTimers(true);
this._resetSelection();
let foundNow = null;
let nextIndex = null;
let nextStart = null;
// loop through events, look for where we should be
for (const [index, e] of this._eventlist.entries()) {
if (!foundNow) {
if (e.timeStart <= now && now < e.timeEnd) {
// set flag
foundNow = true;
// set timers
this._startedAt = e.timeStart;
this._finishAt = e.timeEnd;
this.duration = e.timeEnd - e.timeStart;
this.current = e.timeEnd - now;
// set selection
this.selectedEventId = e.id;
this.selectedEventIndex = index;
// set titles
this._loadTitlesNow();
// skip this entry for next
continue;
}
}
// check how far the start is from now
let wait = e.timeStart - now;
if (wait > 0) {
if (nextStart == null || wait < nextStart) {
nextStart = wait;
nextIndex = index;
}
}
}
// nothing to play next, unload
if (!foundNow && !nextIndex) {
this.unload();
return;
}
if (nextIndex) {
// load titles
this._loadThisTitles(nextIndex, 'next');
if (!foundNow) {
// timer counts to nextStart
this.secondaryTimer = nextStart;
}
}
}
roll() { roll() {
console.log('roll: not yet implemented'); // do we need to change
return false; if (this.state === 'roll') return;
// load into event
this.rollLoad();
// set state
this.state = 'roll'; this.state = 'roll';
this.broadcastState();
} }
previous() { previous() {
@@ -731,14 +884,15 @@ class EventTimer extends Timer {
this.loadEvent(0); this.loadEvent(0);
return; return;
} }
// change playstate
this.pause();
const gotoEvent = const gotoEvent =
this.selectedEventIndex > 0 ? this.selectedEventIndex - 1 : 0; this.selectedEventIndex > 0 ? this.selectedEventIndex - 1 : 0;
if (gotoEvent === this.selectedEventIndex) return; if (gotoEvent === this.selectedEventIndex) return;
this.loadEvent(gotoEvent); this.loadEvent(gotoEvent);
// change playstate
this.pause();
} }
next() { next() {
@@ -751,6 +905,9 @@ class EventTimer extends Timer {
return; return;
} }
// change playstate
this.pause();
const gotoEvent = const gotoEvent =
this.selectedEventIndex < this.numEvents - 1 this.selectedEventIndex < this.numEvents - 1
? this.selectedEventIndex + 1 ? this.selectedEventIndex + 1
@@ -758,9 +915,6 @@ class EventTimer extends Timer {
if (gotoEvent === this.selectedEventIndex) return; if (gotoEvent === this.selectedEventIndex) return;
this.loadEvent(gotoEvent); this.loadEvent(gotoEvent);
// change playstate
this.pause();
} }
unload() { unload() {
@@ -775,12 +929,10 @@ class EventTimer extends Timer {
} }
reload() { reload() {
// reload data
this.loadEvent(this.selectedEventIndex);
// reset playstate // reset playstate
this.pause(); this.pause();
// reload data
this.loadEvent(this.selectedEventIndex);
} }
} }
module.exports = EventTimer;
@@ -4,10 +4,11 @@
* *
*/ */
class Timer { export class Timer {
clock = null; clock = null;
duration = null; duration = null;
current = null; current = null;
secondaryTimer = null;
_finishAt = null; _finishAt = null;
_finishedAt = null; _finishedAt = null;
_startedAt = null; _startedAt = null;
@@ -50,6 +51,9 @@ class Timer {
// check playstate // check playstate
switch (this.state) { switch (this.state) {
case 'start': case 'start':
// ensure we have a start time
if (this._startedAt == null) this._startedAt = now;
// update current timer // update current timer
this.current = this.current =
this._startedAt + this.duration + this._pausedTotal - now; this._startedAt + this.duration + this._pausedTotal - now;
@@ -110,8 +114,10 @@ class Timer {
); );
} }
_resetTimers() { _resetTimers(total = false) {
if (total) this.duration = null;
this.current = this.duration; this.current = this.duration;
this.secondaryTimer = null;
this._finishAt = null; this._finishAt = null;
this._finishedAt = null; this._finishedAt = null;
this._startedAt = null; this._startedAt = null;
@@ -132,7 +138,7 @@ class Timer {
return { return {
clock: this.clock, clock: this.clock,
running: Timer.toSeconds(this.current), running: Timer.toSeconds(this.current),
currentSeconds: Timer.toSeconds(Math.max(this.current, 0)), secondary: Timer.toSeconds(this.secondaryTimer),
durationSeconds: Timer.toSeconds(this.duration), durationSeconds: Timer.toSeconds(this.duration),
expectedFinish: this._getExpectedFinish(), expectedFinish: this._getExpectedFinish(),
startedAt: this._startedAt, startedAt: this._startedAt,
@@ -210,6 +216,4 @@ class Timer {
this._finishedAt = null; this._finishedAt = null;
} }
} }
} }
module.exports = Timer;
+15
View File
@@ -0,0 +1,15 @@
export const config = {
timer: {
refresh: 1000,
},
server: {
port: 4001,
},
database: {
filename: 'db.json',
tablename: 'events',
},
osc: {
port: 8888,
},
};
@@ -1,10 +1,13 @@
const osc = require('node-osc'); import { Server } from 'node-osc';
const initiateOSC = (config) => { export const initiateOSC = (config) => {
const oscServer = new osc.Server(config.port, '0.0.0.0', () => { const oscServer = new Server(config.port, '0.0.0.0', () => {
console.log(`OSC Server is listening on port ${config.port}`); console.log(`OSC Server is listening on port ${config.port}`);
}); });
// error
oscServer.on('error', console.error);
oscServer.on('message', function (msg) { oscServer.on('message', function (msg) {
// message should look like /ontime/{path}/{args} where // message should look like /ontime/{path}/{args} where
// ontime: fixed message for app // ontime: fixed message for app
@@ -20,7 +23,7 @@ const initiateOSC = (config) => {
if (address !== 'ontime') return; if (address !== 'ontime') return;
// get second part (command) // get second part (command)
switch (path) { switch (path.toLocaleLowerCase()) {
case 'start': case 'start':
case 'play': case 'play':
console.log('calling play'); console.log('calling play');
@@ -61,7 +64,19 @@ const initiateOSC = (config) => {
case 'goto': case 'goto':
console.log('calling goto with', args); console.log('calling goto with', args);
try { try {
global.timer.loadEventById(args.toLowerCase()); let eventIndex = parseInt(args);
if (isNaN(eventIndex) || eventIndex <= 0 || eventIndex == null)
return;
global.timer.loadEvent(eventIndex - 1, undefined, true);
} catch (error) {
console.log('error calling goto: ', error);
}
break;
case 'gotoid':
console.log('calling gotoid with', args);
if (args == null) return;
try {
global.timer.loadEventById(args.toString().toLowerCase());
} catch (error) { } catch (error) {
console.log('error calling goto: ', error); console.log('error calling goto: ', error);
} }
@@ -73,5 +88,3 @@ const initiateOSC = (config) => {
} }
}); });
}; };
module.exports = { initiateOSC };
+26
View File
@@ -0,0 +1,26 @@
// get database
import { db, data } from '../app.js';
// Create controller for GET request to 'event'
// Returns ACK message
export const getEvent = async (req, res) => {
res.json(data.event);
};
// Create controller for POST request to 'event'
// Returns ACK message
export const postEvent = async (req, res) => {
if (!req.body) {
res.status(400).send('No object found in request');
return;
}
// TODO: validate data
try {
data.event = { ...data.event, ...req.body };
await db.write();
res.sendStatus(200);
} catch (error) {
res.status(400).send(error);
console.log(error);
}
};
@@ -1,22 +1,26 @@
// get database // get database
const db = require('../app.js').db; import { db, data } from '../app.js';
// utils // utils
const customAlphabet = require('nanoid').customAlphabet; import { customAlphabet } from 'nanoid';
const nanoid = customAlphabet('1234567890abcdef', 4); const nanoid = customAlphabet('1234567890abcdef', 4);
const eventDefs = require('../data/eventsDefinition.js'); import {
event as eventDef,
delay as delayDef,
block as blockDef,
} from '../data/eventsDefinition.js';
function _getEventsCount() { function _getEventsCount() {
return db.get('events').size().value(); return Array.from(data.events).length;
} }
function _pushNew(entry) { function _pushNew(entry) {
return db.get('events').push(entry).write(); return data.events.push(entry).write();
} }
function _insertAt(entry, index) { async function _insertAt(entry, index) {
// get events // get events
let events = db.get('events').value(); let events = data.events;
let count = events.length; let count = events.length;
let order = entry.order; let order = entry.order;
@@ -39,15 +43,18 @@ function _insertAt(entry, index) {
} }
// save events // save events
db.set('events', events).write(); data.events = events;
await db.write();
} }
function _removeById(eventId) { async function _removeById(eventId) {
return db.get('events').remove({ id: eventId }).write(); data.events = Array.from(data.events).filter((e) => e.id != eventId);
await db.write();
} }
function getEventEvents() { function getEventEvents() {
return db.get('events').chain().filter({ type: 'event' }).value(); // return data.events.filter((e) => e.type === 'event');
return Array.from(data.events).filter((e) => e.type === 'event');
} }
// Updates timer object // Updates timer object
@@ -68,21 +75,21 @@ function _deleteTimerId(entryId) {
// Create controller for GET request to '/events' // Create controller for GET request to '/events'
// Returns - // Returns -
exports.eventsGetAll = async (req, res) => { export const eventsGetAll = async (req, res) => {
const results = db.get('events').value(); res.json(data.events);
res.json(results);
}; };
// Create controller for GET request to '/events/:eventId' // Create controller for GET request to '/events/:eventId'
// Returns - // Returns -
exports.eventsGetById = async (req, res) => { export const eventsGetById = async (req, res) => {
const e = db.get('events').find({ id: req.params.eventId }).value(); const e = data.events.find({ id: req.params.eventId }).value();
console.log('event by id', e);
res.json(e); res.json(e);
}; };
// Create controller for POST request to '/events/' // Create controller for POST request to '/events/'
// Returns - // Returns -
exports.eventsPost = async (req, res) => { export const eventsPost = async (req, res) => {
// TODO: Validate event // TODO: Validate event
if (!req.body) { if (!req.body) {
res.status(400).send(`No object found in request`); res.status(400).send(`No object found in request`);
@@ -95,13 +102,13 @@ exports.eventsPost = async (req, res) => {
switch (req.body.type) { switch (req.body.type) {
case 'event': case 'event':
newEvent = { ...eventDefs.event, ...req.body }; newEvent = { ...eventDef, ...req.body };
break; break;
case 'delay': case 'delay':
newEvent = { ...eventDefs.delay, ...req.body }; newEvent = { ...delayDef, ...req.body };
break; break;
case 'block': case 'block':
newEvent = { ...eventDefs.block, ...req.body }; newEvent = { ...blockDef, ...req.body };
break; break;
default: default:
@@ -131,7 +138,7 @@ exports.eventsPost = async (req, res) => {
// Create controller for PUT request to '/events/' // Create controller for PUT request to '/events/'
// Returns - // Returns -
exports.eventsPut = async (req, res) => { export const eventsPut = async (req, res) => {
// no valid params // no valid params
if (!req.body) { if (!req.body) {
res.status(400).send(`No object found`); res.status(400).send(`No object found`);
@@ -145,29 +152,35 @@ exports.eventsPut = async (req, res) => {
} }
try { try {
db.get('events') const eventIndex = data.events.findIndex((e) => e.id === eventId);
.find({ id: req.body.id }) if (eventIndex === -1) {
.assign({ ...req.body }) res.status(400).send(`No Id found found`);
.update('revision', (n) => n + 1) return;
.write(); }
const e = data.events[eventIndex];
data.events[eventIndex] = { ...e, ...req.body };
data.events[eventIndex].revision++;
db.write();
// update timer // update timer
_updateTimersSingle(req.body.id, req.body); _updateTimersSingle(eventId, req.body);
res.sendStatus(200); res.sendStatus(200);
} catch (error) { } catch (error) {
console.log(error);
res.status(400).send(error); res.status(400).send(error);
} }
}; };
// Create controller for PATCH request to '/events/' // Create controller for PATCH request to '/events/'
// Returns - // Returns -
exports.eventsPatch = async (req, res) => { export const eventsPatch = async (req, res) => {
// Code is the same as put, call that // Code is the same as put, call that
this.eventsPut(req, res); eventsPut(req, res);
}; };
exports.eventsReorder = async (req, res) => { export const eventsReorder = async (req, res) => {
// TODO: Validate event // TODO: Validate event
if (!req.body) { if (!req.body) {
res.status(400).send(`No object found in request`); res.status(400).send(`No object found in request`);
@@ -177,7 +190,7 @@ exports.eventsReorder = async (req, res) => {
const { index, from, to } = req.body; const { index, from, to } = req.body;
// get events // get events
let events = db.get('events').value(); let events = data.events;
let idx = events.findIndex((e) => e.id === index, from); let idx = events.findIndex((e) => e.id === index, from);
// Check if item is at given index // Check if item is at given index
@@ -194,7 +207,8 @@ exports.eventsReorder = async (req, res) => {
events.splice(to, 0, reorderedItem); events.splice(to, 0, reorderedItem);
// save events // save events
db.set('events', events).write(); data.events = events;
db.write();
// TODO: would it be more efficient to reorder at timer? // TODO: would it be more efficient to reorder at timer?
// update timer // update timer
@@ -209,7 +223,7 @@ exports.eventsReorder = async (req, res) => {
// Create controller for PATCH request to '/events/applydelay/:eventId' // Create controller for PATCH request to '/events/applydelay/:eventId'
// Returns - // Returns -
exports.eventsApplyDelay = async (req, res) => { export const eventsApplyDelay = async (req, res) => {
// no valid params // no valid params
if (!req.params.eventId) { if (!req.params.eventId) {
res.status(400).send(`No id found in request`); res.status(400).send(`No id found in request`);
@@ -218,7 +232,7 @@ exports.eventsApplyDelay = async (req, res) => {
try { try {
// get events // get events
let events = db.get('events').value(); let events = data.events;
// AUX // AUX
let delayIndex = null; let delayIndex = null;
@@ -259,7 +273,8 @@ exports.eventsApplyDelay = async (req, res) => {
if (blockIndex) events.splice(blockIndex - 1, 1); if (blockIndex) events.splice(blockIndex - 1, 1);
// update events // update events
db.set('events', events).write(); data.events = events;
db.write();
// update timer // update timer
_updateTimers(); _updateTimers();
@@ -274,7 +289,7 @@ exports.eventsApplyDelay = async (req, res) => {
// Create controller for DELETE request to '/events/:eventId' // Create controller for DELETE request to '/events/:eventId'
// Returns - // Returns -
exports.eventsDelete = async (req, res) => { export const eventsDelete = async (req, res) => {
// no valid params // no valid params
if (!req.params.eventId) { if (!req.params.eventId) {
res.status(400).send(`No id found in request`); res.status(400).send(`No id found in request`);
@@ -295,3 +310,20 @@ exports.eventsDelete = async (req, res) => {
res.status(400).send(error); res.status(400).send(error);
} }
}; };
// Create controller for DELETE request to '/events/:eventId'
// Returns -
export const eventsDeleteAll = async (req, res) => {
try {
// set with nothing
data.events = [];
db.write();
// update timer object
_updateTimersSingle();
res.sendStatus(201);
} catch (error) {
res.status(400).send(error);
}
};
+128
View File
@@ -0,0 +1,128 @@
// get database
import { db, data } from '../app.js';
import fs from 'fs';
import {
event as eventDef,
delay as delayDef,
block as blockDef,
} from '../data/eventsDefinition.js';
import { dbModel } from '../data/dataModel.js';
function getEventTitle() {
return data.event.title;
}
async function deleteFile(file) {
// delete a file
fs.unlink(file, (err) => {
if (err) {
console.log(err);
}
});
}
// parses version 1 of the data system
async function parsev1(jsonData) {
if ('events' in jsonData) {
let events = [];
let ids = [];
for (const e of jsonData.events) {
if (e.type === 'event') {
// doublecheck unique ids
if (e.id == null || ids.indexOf(e.id) !== -1) continue;
ids.push(e.id);
// make sure all properties exits
// dont load any extra properties than the ones known
events.push({
...eventDef,
title: e.title,
subtitle: e.subtitle,
presenter: e.presenter,
note: e.note,
timeStart: e.timeStart,
timeEnd: e.timeEnd,
isPublic: e.isPublic,
id: e.id,
});
} else if (e.type === 'delay') {
events.push({ ...delayDef, duration: e.duration });
} else if (e.type === 'block') {
events.push({ ...blockDef });
}
}
// write to db
db.data.events = events;
db.write();
}
if ('event' in jsonData) {
const e = jsonData.event;
// filter known properties
const event = {
...dbModel.event,
title: e.title,
url: e.url,
publicInfo: e.publicInfo,
backstageInfo: e.backstageInfo,
};
// write to db
db.data.event = event;
db.write();
}
// Not handling settings yet
// let settings = {};
// if ('settings' in jsonData) {
// }
}
// Create controller for GET request to '/ontime/db'
// Returns -
export const dbDownload = async (req, res) => {
const fileTitle = getEventTitle() || 'ontime events';
res.download('db.json', `${fileTitle}.json`, (err) => {
if (err) {
res.status(500).send({
message: 'Could not download the file. ' + err,
});
}
});
};
// Create controller for POST request to '/ontime/db'
// Returns -
export const dbUpload = async (req, res) => {
if (!req.file) {
res.status(400).send({ message: 'File not found' });
return;
}
const file = req.file.path;
if (!fs.existsSync(file)) {
res.status(500).send({ message: 'Upload failed' });
return;
}
try {
// get file
let rawdata = fs.readFileSync(file);
let uploadedJson = JSON.parse(rawdata);
// delete file
deleteFile(file);
// check version
if (uploadedJson.settings.version === 1) parsev1(uploadedJson);
else {
res.status(400).send({ message: 'Error parsing file, version unknown' });
return;
}
res.sendStatus(200);
} catch (error) {
console.log('Error parsing file', error);
res.status(400).send({ message: error });
}
};
@@ -1,64 +1,64 @@
// Create controller for GET request to '/playback' // Create controller for GET request to '/playback'
// Returns ACK message // Returns ACK message
exports.pbGet = async (req, res) => { export const pbGet = async (req, res) => {
res.send(global.timer.playState); res.send(global.timer.playState);
}; };
// Create controller for GET request to '/playback/start' // Create controller for GET request to '/playback/start'
// Starts timer object // Starts timer object
exports.pbStart = async (req, res) => { export const pbStart = async (req, res) => {
global.timer.start(); global.timer.start();
res.sendStatus(200); res.sendStatus(200);
}; };
// Create controller for GET request to '/playback/pause' // Create controller for GET request to '/playback/pause'
// Pauses timer object // Pauses timer object
exports.pbPause = async (req, res) => { export const pbPause = async (req, res) => {
global.timer.pause(); global.timer.pause();
res.sendStatus(200); res.sendStatus(200);
}; };
// Create controller for GET request to '/playback/stop' // Create controller for GET request to '/playback/stop'
// Stops timer object // Stops timer object
exports.pbStop = async (req, res) => { export const pbStop = async (req, res) => {
global.timer.stop(); global.timer.stop();
res.sendStatus(200); res.sendStatus(200);
}; };
// Create controller for GET request to '/playback/roll' // Create controller for GET request to '/playback/roll'
// Sets timer object to roll mode // Sets timer object to roll mode
exports.pbRoll = async (req, res) => { export const pbRoll = async (req, res) => {
global.timer.roll(); global.timer.roll();
res.sendStatus(501); res.sendStatus(501);
}; };
// Create controller for GET request to '/playback/previous' // Create controller for GET request to '/playback/previous'
// Sets timer object to roll mode // Sets timer object to roll mode
exports.pbPrevious = async (req, res) => { export const pbPrevious = async (req, res) => {
global.timer.previous(); global.timer.previous();
res.sendStatus(200); res.sendStatus(200);
}; };
// Create controller for GET request to '/playback/next' // Create controller for GET request to '/playback/next'
// Sets timer object to roll mode // Sets timer object to roll mode
exports.pbNext = async (req, res) => { export const pbNext = async (req, res) => {
global.timer.next(); global.timer.next();
res.sendStatus(200); res.sendStatus(200);
}; };
// Create controller for GET request to '/playback/unload' // Create controller for GET request to '/playback/unload'
// Unloads any events // Unloads any events
exports.pbUnload = async (req, res) => { export const pbUnload = async (req, res) => {
global.timer.unload(); global.timer.unload();
console.log('debug: unload called') console.log('debug: unload called');
res.sendStatus(200); res.sendStatus(200);
}; };
// Create controller for GET request to '/playback/reload' // Create controller for GET request to '/playback/reload'
// Reloads current event // Reloads current event
exports.pbReload = async (req, res) => { export const pbReload = async (req, res) => {
global.timer.reload(); global.timer.reload();
console.log('debug: reload called') console.log('debug: reload called');
res.sendStatus(200); res.sendStatus(200);
}; };
@@ -1,10 +1,10 @@
const dbModel = { export const dbModel = {
events: [], events: [],
event: { event: {
title: '', title: '',
url: '', url: '',
publicInfo: '', publicInfo: '',
backStageInfo: '', backstageInfo: '',
}, },
settings: { settings: {
app: 'ontime', app: 'ontime',
@@ -14,5 +14,3 @@ const dbModel = {
lock: false, lock: false,
}, },
}; };
module.exports = { dbModel };
@@ -1,4 +1,4 @@
const event = { export const event = {
title: '', title: '',
subtitle: '', subtitle: '',
presenter: '', presenter: '',
@@ -10,13 +10,12 @@ const event = {
revision: 0, revision: 0,
}; };
const delay = { export const delay = {
duration: 0, duration: 0,
type: 'delay', type: 'delay',
revision: 0,
}; };
const block = { export const block = {
type: 'block', type: 'block',
}; };
module.exports = { event, delay, block };
+11
View File
@@ -0,0 +1,11 @@
import express from 'express';
export const router = express.Router();
// import event controller
import { getEvent, postEvent } from '../controllers/eventController.js';
// create route between controller and 'GET /event' endpoint
router.get('/', getEvent);
// create route between controller and 'POST /event' endpoint
router.post('/', postEvent);
+42
View File
@@ -0,0 +1,42 @@
import express from 'express';
export const router = express.Router();
// import events controller
import {
eventsGetAll,
eventsGetById,
eventsPost,
eventsPut,
eventsPatch,
eventsReorder,
eventsApplyDelay,
eventsDeleteAll,
eventsDelete,
} from '../controllers/eventsController.js';
// create route between controller and '/events/' endpoint
router.get('/', eventsGetAll);
// create route between controller and '/events/:eventId' endpoint
router.get('/:eventId', eventsGetById);
// create route between controller and '/events/' endpoint
router.post('/', eventsPost);
// create route between controller and '/events/' endpoint
router.put('/', eventsPut);
// create route between controller and '/events/' endpoint
router.patch('/', eventsPatch);
// create route between controller and '/events/reorder' endpoint
router.patch('/reorder/', eventsReorder);
// create route between controller and '/events/applydelay/:eventId' endpoint
router.patch('/applydelay/:eventId', eventsApplyDelay);
// create route between controller and '/events/all' endpoint
router.delete('/all', eventsDeleteAll);
// create route between controller and '/events/:eventId' endpoint
router.delete('/:eventId', eventsDelete);
+11
View File
@@ -0,0 +1,11 @@
import express from 'express';
import uploadJson from '../utils/upload.js';
export const router = express.Router();
import { dbDownload, dbUpload } from '../controllers/ontimeController.js';
// create route between controller and '/ontime/db' endpoint
router.get('/db', dbDownload);
// create route between controller and '/ontime/db' endpoint
router.post('/db', uploadJson, dbUpload);
@@ -1,5 +1,5 @@
const express = require('express'); import express from 'express';
const router = express.Router(); export const router = express.Router();
// import event controller // import event controller
const playbackController = require('../controllers/playbackController'); const playbackController = require('../controllers/playbackController');
@@ -30,5 +30,3 @@ router.get('/unload', playbackController.pbUnload);
// create route between controller and '/playback/reload' endpoint // create route between controller and '/playback/reload' endpoint
router.get('/reload', playbackController.pbReload); router.get('/reload', playbackController.pbReload);
module.exports = router;
+23
View File
@@ -0,0 +1,23 @@
import multer from 'multer';
const storage = multer.diskStorage({
destination: function (req, file, cb) {
cb(null, 'uploads/');
},
filename: function (req, file, cb) {
cb(null, Date.now() + '--' + file.originalname);
},
});
// filter only json
const filterJson = (req, file, cb) => {
if (file.mimetype.includes('application/json')) {
cb(null, true);
} else {
cb(null, false);
}
};
const uploadJson = multer({ storage: storage, fileFilter: filterJson });
export default uploadJson.single('jsondb');