mirror of
https://github.com/cpvalente/ontime.git
synced 2026-08-21 15:09:10 +00:00
Merge remote-tracking branch 'origin/master' into feat/electron
This commit is contained in:
@@ -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
|
||||||
|
|||||||
@@ -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
@@ -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
@@ -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>
|
||||||
|
|||||||
@@ -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,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);
|
||||||
|
|||||||
@@ -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;
|
||||||
|
};
|
||||||
|
|||||||
@@ -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,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');
|
||||||
|
|||||||
@@ -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));
|
||||||
|
});
|
||||||
@@ -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,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([[], () => {}]);
|
||||||
|
|
||||||
|
|||||||
@@ -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 (
|
||||||
|
|||||||
@@ -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>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -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'}
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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>
|
||||||
);
|
);
|
||||||
|
|||||||
+17
-10
@@ -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>
|
||||||
);
|
);
|
||||||
@@ -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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -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: '',
|
||||||
|
|||||||
@@ -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}>
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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);
|
|
||||||
@@ -1,15 +0,0 @@
|
|||||||
{
|
|
||||||
"timer": {
|
|
||||||
"refresh": 1000
|
|
||||||
},
|
|
||||||
"server": {
|
|
||||||
"port": 4001
|
|
||||||
},
|
|
||||||
"database": {
|
|
||||||
"filename": "db.json",
|
|
||||||
"tablename": "events"
|
|
||||||
},
|
|
||||||
"osc": {
|
|
||||||
"port": 8888
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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);
|
|
||||||
};
|
|
||||||
@@ -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"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
{
|
|
||||||
"current": null,
|
|
||||||
"next": null,
|
|
||||||
"currentTimer": null,
|
|
||||||
"numEvents": 0,
|
|
||||||
"state": "pause",
|
|
||||||
"prevState": "pause"
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
{
|
||||||
|
"exclude": ["node_modules", "dist"]
|
||||||
|
}
|
||||||
+4
-2
@@ -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"
|
||||||
|
|||||||
@@ -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;
|
|
||||||
@@ -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;
|
|
||||||
@@ -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;
|
|
||||||
@@ -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 };
|
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
|
};
|
||||||
+68
-36
@@ -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);
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -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 });
|
||||||
|
}
|
||||||
|
};
|
||||||
+11
-11
@@ -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 };
|
|
||||||
@@ -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);
|
||||||
@@ -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);
|
||||||
@@ -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;
|
|
||||||
@@ -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');
|
||||||
Reference in New Issue
Block a user