diff --git a/.gitignore b/.gitignore index 51e40fb1a..480d45493 100644 --- a/.gitignore +++ b/.gitignore @@ -33,3 +33,5 @@ db.json yarn.lock package.json package-lock.json +db.json +db backup.json diff --git a/client/package.json b/client/package.json index 2411db248..886e0b989 100644 --- a/client/package.json +++ b/client/package.json @@ -12,6 +12,7 @@ "axios": "^0.21.1", "date-fns": "^2.20.1", "framer-motion": "^4.1.6", + "jotai": "^0.16.5", "react": "^17.0.1", "react-beautiful-dnd": "^13.1.0", "react-dom": "^17.0.1", diff --git a/client/public/logo192.png b/client/public/logo192.png index 85aa1a0c0..7508eedfa 100644 Binary files a/client/public/logo192.png and b/client/public/logo192.png differ diff --git a/client/public/logo512.png b/client/public/logo512.png index 6f83fe686..e7aaee290 100644 Binary files a/client/public/logo512.png and b/client/public/logo512.png differ diff --git a/client/src/App.css b/client/src/App.css index edce8e121..27fa5fcf3 100644 --- a/client/src/App.css +++ b/client/src/App.css @@ -3,8 +3,10 @@ -moz-osx-font-smoothing: grayscale; } +body, +html, .App { margin: 0px auto; - overflow: hidden; + overflow: clip; height: 100vh; -} \ No newline at end of file +} diff --git a/client/src/App.jsx b/client/src/App.jsx index 1d009bcba..8ef8295f6 100644 --- a/client/src/App.jsx +++ b/client/src/App.jsx @@ -4,8 +4,6 @@ import './App.css'; import { QueryClient, QueryClientProvider } from 'react-query'; import SocketProvider from 'app/context/socketContext'; 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 PresenterView = lazy(() => @@ -42,7 +40,7 @@ function App() {
- }> + @@ -50,12 +48,12 @@ function App() { - + {/* Lower cannot have fallback */} + {/* Send to default if nothing found */} -
diff --git a/client/src/app/api/apiConstants.js b/client/src/app/api/apiConstants.js index 26f818293..ba1a35c67 100644 --- a/client/src/app/api/apiConstants.js +++ b/client/src/app/api/apiConstants.js @@ -1,7 +1,13 @@ export const NODE_PORT = 4001; +export const EVENT_TABLE = 'event'; +export const EVENTS_TABLE = 'events'; const calculateServer = () => { return window.location.origin.replace(window.location.port, `${NODE_PORT}/`); }; 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'; diff --git a/client/src/app/api/eventApi.js b/client/src/app/api/eventApi.js index 0118acb68..65982241e 100644 --- a/client/src/app/api/eventApi.js +++ b/client/src/app/api/eventApi.js @@ -1,8 +1,5 @@ import axios from 'axios'; -import { serverURL } from './apiConstants'; - -export const eventNamespace = 'event'; -export const eventURL = serverURL + eventNamespace; +import { eventURL } from './apiConstants'; export const fetchEvent = async () => { const res = await axios.get(eventURL); diff --git a/client/src/app/api/eventsApi.js b/client/src/app/api/eventsApi.js index ec19409be..17a2ada40 100644 --- a/client/src/app/api/eventsApi.js +++ b/client/src/app/api/eventsApi.js @@ -1,14 +1,5 @@ import axios from 'axios'; -import { NODE_PORT } 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; +import { eventsURL } from '../api/apiConstants'; export const fetchAllEvents = async () => { const res = await axios.get(eventsURL); @@ -46,3 +37,8 @@ export const requestDelete = async (eventId) => { const res = await axios.delete(eventsURL + '/' + eventId); return res; }; + +export const requestDeleteAll = async () => { + const res = await axios.delete(eventsURL + '/all'); + return res; +}; diff --git a/client/src/app/api/ontimeApi.js b/client/src/app/api/ontimeApi.js new file mode 100644 index 000000000..4ebc5f73b --- /dev/null +++ b/client/src/app/api/ontimeApi.js @@ -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)); +}; diff --git a/client/src/app/api/playbackApi.js b/client/src/app/api/playbackApi.js index 1dabea106..0145da7f5 100644 --- a/client/src/app/api/playbackApi.js +++ b/client/src/app/api/playbackApi.js @@ -1,8 +1,5 @@ import axios from 'axios'; -import { serverURL } from '../api/apiConstants'; - -export const playbackNamespace = 'playback'; -const playbackURL = serverURL + playbackNamespace; +import { playbackURL } from '../api/apiConstants'; export const getStart = async () => { const res = await axios.get(playbackURL + '/start'); diff --git a/client/src/app/context/collapseAtom.js b/client/src/app/context/collapseAtom.js new file mode 100644 index 000000000..ed6b63bc3 --- /dev/null +++ b/client/src/app/context/collapseAtom.js @@ -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)); +}); diff --git a/client/src/app/context/settingsAtom.js b/client/src/app/context/settingsAtom.js new file mode 100644 index 000000000..c49055d5b --- /dev/null +++ b/client/src/app/context/settingsAtom.js @@ -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)); +}); diff --git a/client/src/app/context/socketContext.js b/client/src/app/context/socketContext.js index 6767173ed..ecfb9a99e 100644 --- a/client/src/app/context/socketContext.js +++ b/client/src/app/context/socketContext.js @@ -1,12 +1,6 @@ import { createContext, useContext, useEffect, useState } from 'react'; import io from 'socket.io-client'; -import { NODE_PORT } from '../api/apiConstants'; - -// get origin from URL -const serverURL = window.location.origin.replace( - window.location.port, - `${NODE_PORT}/` -); +import { serverURL } from 'app/api/apiConstants'; const SocketContext = createContext([[], () => {}]); diff --git a/client/src/app/hooks/useFetch.js b/client/src/app/hooks/useFetch.js index 6daf49350..eceaf10d8 100644 --- a/client/src/app/hooks/useFetch.js +++ b/client/src/app/hooks/useFetch.js @@ -4,7 +4,8 @@ const refetchIntervalMs = 10000; export const useFetch = (namespace, fn) => { const { data, status, isError, refetch } = useQuery(namespace, fn, { refetchInterval: refetchIntervalMs, - cacheTime: refetchIntervalMs, + cacheTime: Infinity, + notifyOnChangeProps: 'tracked', }); return { data, status, isError, refetch }; diff --git a/client/src/common/components/buttons/CollapseBtn.jsx b/client/src/common/components/buttons/CollapseBtn.jsx new file mode 100644 index 000000000..77891b087 --- /dev/null +++ b/client/src/common/components/buttons/CollapseBtn.jsx @@ -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 ( + + } + colorScheme='white' + variant='outline' + background='#fff1' + onClick={clickhandler} + _focus={{ boxShadow: 'none' }} + /> + + ); +} diff --git a/client/src/common/components/buttons/CollapseIconBtn.jsx b/client/src/common/components/buttons/CollapseIconBtn.jsx deleted file mode 100644 index 830e038dd..000000000 --- a/client/src/common/components/buttons/CollapseIconBtn.jsx +++ /dev/null @@ -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 ( - } - colorScheme='orange' - variant={active ? 'solid' : 'outline'} - onClick={clickhandler} - _focus={{ boxShadow: 'none' }} - {...rest} - /> - ); -} diff --git a/client/src/common/components/buttons/CurrentBtn.jsx b/client/src/common/components/buttons/CurrentBtn.jsx new file mode 100644 index 000000000..a15c62aa0 --- /dev/null +++ b/client/src/common/components/buttons/CurrentBtn.jsx @@ -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 ( + + ); +} diff --git a/client/src/common/components/buttons/ExpandBtn.jsx b/client/src/common/components/buttons/ExpandBtn.jsx new file mode 100644 index 000000000..a692e163b --- /dev/null +++ b/client/src/common/components/buttons/ExpandBtn.jsx @@ -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 ( + + } + colorScheme='white' + variant='outline' + background='#fff1' + onClick={clickhandler} + _focus={{ boxShadow: 'none' }} + /> + + ); +} diff --git a/client/src/common/components/buttons/LockIconBtn.jsx b/client/src/common/components/buttons/LockIconBtn.jsx new file mode 100644 index 000000000..c499594d0 --- /dev/null +++ b/client/src/common/components/buttons/LockIconBtn.jsx @@ -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 ( + + } + 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' }} + /> + + ); +} diff --git a/client/src/common/components/buttons/RollIconBtn.jsx b/client/src/common/components/buttons/RollIconBtn.jsx index f34b935c2..079e76f55 100644 --- a/client/src/common/components/buttons/RollIconBtn.jsx +++ b/client/src/common/components/buttons/RollIconBtn.jsx @@ -12,7 +12,6 @@ export default function RollIconBtn(props) { width={120} _focus={{ boxShadow: 'none' }} {...rest} - disabled /> ); } diff --git a/client/src/common/components/buttons/SettingsIconBtn.jsx b/client/src/common/components/buttons/SettingsIconBtn.jsx deleted file mode 100644 index 8a4e3b65f..000000000 --- a/client/src/common/components/buttons/SettingsIconBtn.jsx +++ /dev/null @@ -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 ( - } - isRound - variant='outline' - onClick={clickhandler} - _focus={{ boxShadow: 'none' }} - colorScheme='whiteAlpha' - {...rest} - /> - ); -} diff --git a/client/src/common/components/eventTimes/EventTimesVertical.jsx b/client/src/common/components/eventTimes/EventTimesVertical.jsx index 2944de8aa..244054503 100644 --- a/client/src/common/components/eventTimes/EventTimesVertical.jsx +++ b/client/src/common/components/eventTimes/EventTimesVertical.jsx @@ -8,14 +8,8 @@ const label = { }; const TimesDelayed = (props) => { - const { - handleValidate, - actionHandler, - delay, - timeStart, - timeEnd, - duration, - } = props; + const { handleValidate, actionHandler, delay, timeStart, timeEnd, duration } = + props; const scheduledStart = stringFromMillis(timeStart, false); const scheduledEnd = stringFromMillis(timeEnd, false); @@ -44,7 +38,7 @@ const TimesDelayed = (props) => { /> Duration
@@ -66,7 +68,7 @@ export default function Paginator(props) {
{page.map((e) => { - let selectedState = 0; + if (selectedState === 1) selectedState = 2; if (e.id === selected) selectedState = 1; else if (e.id > selected) selectedState = 2; return ( diff --git a/client/src/common/input/EditableText.jsx b/client/src/common/input/EditableText.jsx index 73739d6c2..f41d8ad0a 100644 --- a/client/src/common/input/EditableText.jsx +++ b/client/src/common/input/EditableText.jsx @@ -33,9 +33,9 @@ export default function EditableText(props) { > - +
); diff --git a/client/src/common/input/EditableTimer.jsx b/client/src/common/input/EditableTimer.jsx index 2c771dbbf..2c39dd6d8 100644 --- a/client/src/common/input/EditableTimer.jsx +++ b/client/src/common/input/EditableTimer.jsx @@ -34,12 +34,12 @@ export default function EditableTimer(props) { if (value === '') return false; // Time now and time submitedVal - const original = stringFromMillis(time, false); + const original = stringFromMillis(time + delay, false); // check if time is different from before if (value === original) return false; - // conver to millis object + // convert to millis object const millis = timeStringToMillis(value, timeFormat); // validate with parent @@ -51,14 +51,8 @@ export default function EditableTimer(props) { return true; }; - const showOriginal = () => { - setValue(stringFromMillis(time, false)); - }; - return ( showOriginal} - onEdit={() => showOriginal} onChange={(v) => setValue(v)} onSubmit={(v) => validateValue(v)} value={value} diff --git a/client/src/features/control/MessageControl.jsx b/client/src/features/control/MessageControl.jsx index 9d66ad89a..e881cb5f1 100644 --- a/client/src/features/control/MessageControl.jsx +++ b/client/src/features/control/MessageControl.jsx @@ -129,7 +129,7 @@ export default function MessageControl() { text={lower.text} visible={lower.visible} changeHandler={(event) => messageControl('lower-text', event)} - actionHandler={() => messageControl('toggle-publ-visible')} + actionHandler={() => messageControl('toggle-lower-visible')} /> ); diff --git a/client/src/features/control/PlaybackButtons.jsx b/client/src/features/control/PlaybackButtons.jsx index 4b46ab07b..8958bba51 100644 --- a/client/src/features/control/PlaybackButtons.jsx +++ b/client/src/features/control/PlaybackButtons.jsx @@ -41,10 +41,7 @@ const Transport = ({ selectedId, playbackControl }) => {
playbackControl('previous')} /> playbackControl('next')} /> - playbackControl('unload')} - disabled={!selectedId} - /> + playbackControl('unload')} /> playbackControl('reload')} disabled={!selectedId} diff --git a/client/src/features/control/PlaybackControl.jsx b/client/src/features/control/PlaybackControl.jsx index d5881f9a2..af1201774 100644 --- a/client/src/features/control/PlaybackControl.jsx +++ b/client/src/features/control/PlaybackControl.jsx @@ -90,6 +90,7 @@ export default function PlaybackControl() {
socket.emit('increment-timer', amount)} /> { return ( prevProps.timer.running === nextProps.timer.running && 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 finish = stringFromMillis(timer.expectedFinish, true); const isNegative = timer.running < 0; const isDelayed = false; - const isRolling = false; + const isRolling = playback === 'roll'; const incrementProps = { size: 'sm', @@ -31,7 +33,7 @@ const PlaybackTimer = ({ timer, handleIncrement }) => { <>
-
+
@@ -49,16 +51,32 @@ const PlaybackTimer = ({ timer, handleIncrement }) => { {finish}
- - - -
diff --git a/client/src/features/editors/Editor.jsx b/client/src/features/editors/Editor.jsx index bc727071b..7490f0439 100644 --- a/client/src/features/editors/Editor.jsx +++ b/client/src/features/editors/Editor.jsx @@ -7,8 +7,8 @@ import styles from './Editor.module.css'; import EventListWrapper from './list/EventListWrapper'; import { useDisclosure } from '@chakra-ui/hooks'; import SettingsModal from '../modals/SettingsModal'; -import SettingsIconBtn from 'common/components/buttons/SettingsIconBtn'; import { useEffect } from 'react'; +import MenuBar from 'features/menu/MenuBar'; export default function Editor() { const { isOpen, onOpen, onClose } = useDisclosure(); @@ -23,6 +23,10 @@ export default function Editor() {
+ + + + Event List @@ -63,12 +67,6 @@ export default function Editor() {
- - -
- -
-
); diff --git a/client/src/features/editors/Editor.module.css b/client/src/features/editors/Editor.module.css index 3e20053c6..00186eecf 100644 --- a/client/src/features/editors/Editor.module.css +++ b/client/src/features/editors/Editor.module.css @@ -8,21 +8,22 @@ display: grid; grid-template-rows: 38vh 1fr; - grid-template-columns: 48em 27vw 25vw 4vw; + grid-template-columns: 40px 48em 27vw 1fr; grid-template-areas: - 'even play info sett' - 'even mess info sett'; + 'sett even play info' + 'sett even mess info'; gap: 2vh; } /* 2/3 window, hide previews */ @media (max-width: 1250px) and (min-height: 700px) { .mainContainer { + height: 100%; grid-template-rows: 1fr 1fr; - grid-template-columns: 48em 1fr 1fr; - grid-template-areas: + grid-template-columns: 40px 48em 1fr 1fr; + /* grid-template-areas: 'even play sett' - 'even mess sett'; + 'even mess sett'; */ } .info { @@ -40,6 +41,11 @@ 'play'; } + .messages, + .playback { + min-width: 31em; + } + .editor, .info, .settings { @@ -89,12 +95,16 @@ grid-area: play; } -.settings { +.mainContainer > .settings { grid-area: sett; + background-color: transparent; + padding: 0; + margin: 0; + width: fit-content; + display: flex; flex-direction: column; - align-items: center; - gap: 2.6em; + gap: 1em; } .content { diff --git a/client/src/features/editors/list/ActionButtons.jsx b/client/src/features/editors/list/ActionButtons.jsx index 8c1d2bff4..ddd673728 100644 --- a/client/src/features/editors/list/ActionButtons.jsx +++ b/client/src/features/editors/list/ActionButtons.jsx @@ -17,7 +17,7 @@ export default function ActionButtons(props) { aria-label='Options' size='xs' icon={} - _expanded={{ bg: 'pink.300', color: 'white' }} + _expanded={{ bg: 'orange.300', color: 'white' }} _focus={{ boxShadow: 'none' }} backgroundColor={'orange.200'} color={'orange.500'} diff --git a/client/src/features/editors/list/EventBlock.jsx b/client/src/features/editors/list/EventBlock.jsx index dc1b332c4..9b26ea583 100644 --- a/client/src/features/editors/list/EventBlock.jsx +++ b/client/src/features/editors/list/EventBlock.jsx @@ -1,6 +1,6 @@ import Icon from '@chakra-ui/icon'; import { FiChevronDown, FiChevronUp, FiMoreVertical } from 'react-icons/fi'; -import { useState } from 'react'; +import { useMemo } from 'react'; import { Draggable } from 'react-beautiful-dnd'; import EventTimes from 'common/components/eventTimes/EventTimes'; 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 { millisToMinutes } from 'common/dateConfig'; import style from './EventBlock.module.css'; +import { SelectCollapse, HandleCollapse } from 'app/context/collapseAtom'; +import { useAtom } from 'jotai'; 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; @@ -78,14 +81,16 @@ const ExpandedBlock = (props) => { actionHandler('update', { field: 'note', value: v }) } /> - {`OSC ID: ${oscid}`} + + {`/ontime/goto ${eventIndex + 1} << OSC >> /ontime/gotoid ${oscid}`} +
props.setExpanded(false)} + onClick={() => props.setCollapsed(true)} />
@@ -138,7 +143,7 @@ const CollapsedBlock = (props) => { as={FiChevronDown} marginTop='0.2em' gridArea='more' - onClick={() => props.setExpanded(true)} + onClick={() => props.setCollapsed(false)} />
@@ -154,19 +159,29 @@ const CollapsedBlock = (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() // Would I then need to add this to state? const isSelected = selected ? style.active : ''; - const isExpanded = expanded ? style.expanded : style.collapsed; - const classSelect = `${style.event} ${isExpanded} ${isSelected}`; + const isCollapsed = collapsed ? style.collapsed : style.expanded; + const classSelect = `${style.event} ${isCollapsed} ${isSelected}`; // Calculate delay in min const delayValue = delay > 0 ? millisToMinutes(delay) : null; + const handleCollapse = (isCollapsed) => { + setCollapsed({ [data.id]: isCollapsed }); + }; + return ( {(provided) => ( @@ -175,17 +190,7 @@ export default function EventBlock(props) { {...provided.draggableProps} ref={provided.innerRef} > - {expanded ? ( - - ) : ( + {collapsed ? ( + ) : ( + )}
diff --git a/client/src/features/editors/list/EventList.jsx b/client/src/features/editors/list/EventList.jsx index 51c582c90..df66b4a9d 100644 --- a/client/src/features/editors/list/EventList.jsx +++ b/client/src/features/editors/list/EventList.jsx @@ -1,18 +1,22 @@ 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 tinykeys from 'tinykeys'; import Empty from 'common/state/Empty'; import EventListItem from './EventListItem'; -import { AnimatePresence, motion } from 'framer-motion'; import { DragDropContext, Droppable } from 'react-beautiful-dnd'; +import { useAtom } from 'jotai'; +import { SelectSetting } from 'app/context/settingsAtom'; export default function EventList(props) { const { events, eventsHandler } = props; const socket = useSocket(); - const [selected, setSelected] = useState(null); - const [next, setNext] = useState(null); - const [cursor, setCursor] = useState(null); + const [selectedId, setSelectedId] = useState(null); + const [nextId, setNextId] = useState(null); + const [cursor, setCursor] = useState(0); + const [cursorSettings] = useAtom(useMemo(() => SelectSetting('cursor'), [])); + + const cursorRef = createRef(); // Handle keyboard shortcuts useEffect(() => { @@ -23,7 +27,7 @@ export default function EventList(props) { }, 'Alt+ArrowUp': () => { if (cursor == null) setCursor(0); - else if (cursor >= 0) setCursor(cursor - 1); + else if (cursor > 0) setCursor(cursor - 1); }, 'Alt+KeyE': (event) => { event.preventDefault(); @@ -45,51 +49,68 @@ export default function EventList(props) { return () => { unsubscribe(); }; - }, [cursor, events.length, eventsHandler]); + }, [cursor, events, eventsHandler]); // handle incoming messages useEffect(() => { if (socket == null) return; // ask for playstate - socket.emit('get-selected-id'); + socket.emit('get-selected'); socket.emit('get-next-id'); // Handle playstate - socket.on('selected-id', (data) => { - setSelected(data); + socket.on('selected', (data) => { + setSelectedId(data.id); }); + socket.on('next-id', (data) => { - setNext(data); + setNextId(data); }); // Clear listener return () => { - socket.off('selected-id'); + socket.off('selected'); socket.off('next-id'); }; }, [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) { return ; } - // motion - const cursorVariants = { - hidden: { - scale: 0, - }, - visible: { - scale: 1, - transition: { - duration: 0.3, - }, - }, - exit: { - scale: 0, - }, - }; - // DND const handleOnDragEnd = (result) => { // drop outside of area @@ -108,20 +129,10 @@ export default function EventList(props) { console.log('EventList: events in event list', events); let cumulativeDelay = 0; + let eventIndex = -1; return (
- - {cursor === -1 && ( - - )} - {(provided) => ( @@ -131,33 +142,35 @@ export default function EventList(props) { ref={provided.innerRef} > {events.map((e, index) => { - if (index === 0) cumulativeDelay = 0; + if (index === 0) { + cumulativeDelay = 0; + eventIndex = -1; + } if (e.type === 'delay' && e.duration != null) { cumulativeDelay += e.duration; - } else if (e.type === 'block') cumulativeDelay = 0; + } else if (e.type === 'block') { + cumulativeDelay = 0; + } else if (e.type === 'event') { + eventIndex++; + } + return ( - +
- - {cursor === index && ( - - )} - - +
); })} {provided.placeholder} diff --git a/client/src/features/editors/list/EventListItem.jsx b/client/src/features/editors/list/EventListItem.jsx index 1517850e7..df8ad7d44 100644 --- a/client/src/features/editors/list/EventListItem.jsx +++ b/client/src/features/editors/list/EventListItem.jsx @@ -18,6 +18,7 @@ const EventListItem = (props) => { const { type, index, + eventIndex, data, selected, next, @@ -71,6 +72,7 @@ const EventListItem = (props) => { return ( { // cancel ongoing queries - queryClient.cancelQueries(eventsNamespace, { exact: true }); + queryClient.cancelQueries(EVENTS_TABLE, { exact: true }); // Snapshot the previous value - let previousEvents = queryClient.getQueryData(eventsNamespace); + let previousEvents = queryClient.getQueryData(EVENTS_TABLE); console.log('debug', previousEvents); if (previousEvents == null) { refetch(); - previousEvents = queryClient.getQueryData(eventsNamespace); + previousEvents = queryClient.getQueryData(EVENTS_TABLE); } console.log('debug 2', previousEvents); @@ -44,7 +48,7 @@ export default function EventListWrapper() { ...newEvent, id: new Date().toISOString(), }); - queryClient.setQueryData(eventsNamespace, optimistic); + queryClient.setQueryData(EVENTS_TABLE, optimistic); // Return a context with the previous and new todo return { previousEvents }; @@ -52,12 +56,12 @@ export default function EventListWrapper() { // Mutation fails, rollback undos optimist update onError: (error, newEvent, context) => { - queryClient.setQueryData(eventsNamespace, context.previousEvents); + queryClient.setQueryData(EVENTS_TABLE, context.previousEvents); }, // Mutation finished, failed or successful // Fetch anyway, just to be sure onSettled: () => { - queryClient.invalidateQueries(eventsNamespace); + queryClient.invalidateQueries(EVENTS_TABLE); }, }); @@ -65,16 +69,16 @@ export default function EventListWrapper() { // we optimistically update here onMutate: async (newEvent) => { // cancel ongoing queries - queryClient.cancelQueries([eventsNamespace, newEvent.id]); + queryClient.cancelQueries([EVENTS_TABLE, newEvent.id]); // Snapshot the previous value const previousEvent = queryClient.getQueryData([ - eventsNamespace, + EVENTS_TABLE, newEvent.id, ]); // 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 { previousEvent, newEvent }; @@ -83,14 +87,14 @@ export default function EventListWrapper() { // Mutation fails, rollback undos optimist update onError: (error, newEvent, context) => { queryClient.setQueryData( - [eventsNamespace, context.newEvent.id], + [EVENTS_TABLE, context.newEvent.id], context.previousEvent ); }, // Mutation finished, failed or successful // Fetch anyway, just to be sure 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 onMutate: async (newEvent) => { // cancel ongoing queries - queryClient.cancelQueries([eventsNamespace, newEvent.id]); + queryClient.cancelQueries([EVENTS_TABLE, newEvent.id]); // Snapshot the previous value const previousEvent = queryClient.getQueryData([ - eventsNamespace, + EVENTS_TABLE, newEvent.id, ]); // 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 { previousEvent, newEvent }; @@ -116,14 +120,14 @@ export default function EventListWrapper() { // Mutation fails, rollback undos optimist update onError: (error, newEvent, context) => { queryClient.setQueryData( - [eventsNamespace, context.newEvent.id], + [EVENTS_TABLE, context.newEvent.id], context.previousEvent ); }, // Mutation finished, failed or successful // Fetch anyway, just to be sure 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 onMutate: async (eventId) => { // cancel ongoing queries - queryClient.cancelQueries([eventsNamespace, eventId]); + queryClient.cancelQueries([EVENTS_TABLE, eventId]); // Snapshot the previous value - const previousEvents = queryClient.getQueryData(eventsNamespace); + const previousEvents = queryClient.getQueryData(EVENTS_TABLE); let filtered = [...previousEvents]; filtered.filter((e) => e.id === 'eventId'); // optimistically update object - queryClient.setQueryData(eventsNamespace, filtered); + queryClient.setQueryData(EVENTS_TABLE, filtered); // Return a context with the previous and new todo return { previousEvents }; @@ -148,19 +152,48 @@ export default function EventListWrapper() { // Mutation fails, rollback undos optimist update onError: (error, eventId, context) => { - queryClient.setQueryData(eventsNamespace, context.previousEvents); + queryClient.setQueryData(EVENTS_TABLE, context.previousEvents); }, // Mutation finished, failed or successful // Fetch anyway, just to be sure 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, { // Mutation finished, failed or successful onSettled: () => { - queryClient.invalidateQueries(eventsNamespace); + queryClient.invalidateQueries(EVENTS_TABLE); }, }); @@ -168,17 +201,17 @@ export default function EventListWrapper() { // we optimistically update here onMutate: async (data) => { // cancel ongoing queries - queryClient.cancelQueries(eventsNamespace, { exact: true }); + queryClient.cancelQueries(EVENTS_TABLE, { exact: true }); // Snapshot the previous value - const previousEvents = queryClient.getQueryData(eventsNamespace); + const previousEvents = queryClient.getQueryData(EVENTS_TABLE); const e = [...previousEvents]; const [reorderedItem] = e.splice(data.from, 1); e.splice(data.to, 0, reorderedItem); // optimistically update object - queryClient.setQueryData(eventsNamespace, e); + queryClient.setQueryData(EVENTS_TABLE, e); // Return a context with the previous and new todo return { previousEvents }; @@ -186,12 +219,12 @@ export default function EventListWrapper() { // Mutation fails, rollback undos optimist update onError: (error, eventId, context) => { - queryClient.setQueryData(eventsNamespace, context.previousEvents); + queryClient.setQueryData(EVENTS_TABLE, context.previousEvents); }, // Mutation finished, failed or successful // Fetch anyway, just to be sure onSettled: () => { - queryClient.invalidateQueries(eventsNamespace); + queryClient.invalidateQueries(EVENTS_TABLE); }, }); @@ -203,93 +236,114 @@ export default function EventListWrapper() { }, [isError]); // Events API - const eventsHandler = useCallback(async (action, payload) => { - switch (action) { - case 'add': - try { - 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) { + const eventsHandler = useCallback( + async (action, payload) => { + switch (action) { + case 'add': 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; + 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 { + // 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 applydelay', payload.id); + console.log('debug m apply', Date.now() - t); + 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 { - await applyDelay.mutateAsync(payload.id); + let t = Date.now(); + await deleteAllEvents.mutateAsync(); + console.log('debug m deleteall', Date.now() - t); } catch (error) { - showErrorToast('Error applying delay', error.message); + showErrorToast('Error deleting events', error.message); } - } - console.log('debug m apply', Date.now() - t); - - break; - default: - showErrorToast('Unrecognised request', action); - break; - } - }, []); + break; + default: + showErrorToast('Unrecognised request', action); + break; + } + }, + [data] + ); return ( <> diff --git a/client/src/features/editors/list/List.module.css b/client/src/features/editors/list/List.module.css index e676a162c..6670524f3 100644 --- a/client/src/features/editors/list/List.module.css +++ b/client/src/features/editors/list/List.module.css @@ -21,11 +21,14 @@ } .cursor { - width: 90%; - min-height: 2px; - background-color: #ff7597; - border-radius: 2px; - margin: 0 auto; - /* transition: 1s; - transition-property: all; */ + width: 100%; + background: linear-gradient( + 180deg, + #ff7597 2%, + #0001 3%, + #0001 97%, + #ff7597 98% + ); + + border-radius: 14px; } diff --git a/client/src/features/menu/EventListMenu.jsx b/client/src/features/menu/EventListMenu.jsx index 9f8a85f07..54bc4f46d 100644 --- a/client/src/features/menu/EventListMenu.jsx +++ b/client/src/features/menu/EventListMenu.jsx @@ -1,31 +1,16 @@ -import { memo } from 'react'; -import { FiChevronDown } from 'react-icons/fi'; -import { - Button, - ButtonGroup, - Menu, - MenuButton, - MenuItem, - MenuList, -} from '@chakra-ui/react'; +import { memo, useMemo } from 'react'; +import { Divider } from '@chakra-ui/react'; 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 buttonProps = { - size: 'sm', - 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 [cursorSettings] = useAtom(useMemo(() => SelectSetting('cursor'), [])); + const [, SetOption] = useAtom(HandleOptions); const actionHandler = (action) => { switch (action) { @@ -38,6 +23,16 @@ const EventListMenu = ({ eventsHandler }) => { case 'block': eventsHandler('add', { type: action, order: 0 }); break; + case 'togglelock': + let newSet = 'locked'; + if (cursorSettings === 'locked') { + newSet = 'unlocked'; + } + SetOption({ cursor: newSet }); + break; + case 'deleteall': + eventsHandler('deleteall'); + break; default: break; } @@ -45,30 +40,18 @@ const EventListMenu = ({ eventsHandler }) => { return (
- - - - - - - - - Upload Excel - Upload CSV - - - - - - - - - - - Download Excel - Download CSV - - + eventsHandler('collapseall')} + /> + eventsHandler('expandall')} /> + + actionHandler('togglelock')} + active={cursorSettings === 'locked'} + /> +
); diff --git a/client/src/features/editors/list/MenuActionButtons.jsx b/client/src/features/menu/MenuActionButtons.jsx similarity index 74% rename from client/src/features/editors/list/MenuActionButtons.jsx rename to client/src/features/menu/MenuActionButtons.jsx index 24eb63ce2..4bb954e40 100644 --- a/client/src/features/editors/list/MenuActionButtons.jsx +++ b/client/src/features/menu/MenuActionButtons.jsx @@ -1,7 +1,13 @@ import { Menu, MenuButton, MenuItem, MenuList } from '@chakra-ui/menu'; import { IconButton } from '@chakra-ui/button'; -import { FiZap, FiPlus, FiClock, FiMinusCircle } from 'react-icons/fi'; -import { useEffect } from 'react'; +import { + FiTrash2, + FiZap, + FiPlus, + FiClock, + FiMinusCircle, +} from 'react-icons/fi'; +import { Divider } from '@chakra-ui/layout'; export default function MenuActionButtons(props) { const { actionHandler } = props; @@ -10,10 +16,6 @@ export default function MenuActionButtons(props) { backgroundColor: 'rgba(255,255,255,1)', }; - useEffect(() =>{ - console.log('debug action button render') - }) - return ( } - _expanded={{ bg: 'pink.300', color: 'white' }} + _expanded={{ bg: 'orange.300', color: 'white' }} _focus={{ boxShadow: 'none' }} backgroundColor={'orange.200'} color={'orange.500'} /> - {/* } onClick={props.deleteAllHandler}> - Delete All - */} } onClick={() => actionHandler('event')}> Event first @@ -42,6 +41,14 @@ export default function MenuActionButtons(props) { > Block first + + } + onClick={() => actionHandler('deleteall')} + color='red.500' + > + Delete All + ); diff --git a/client/src/features/menu/MenuBar.jsx b/client/src/features/menu/MenuBar.jsx new file mode 100644 index 000000000..f2679fc2b --- /dev/null +++ b/client/src/features/menu/MenuBar.jsx @@ -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 ( + <> + + + +
+ + + + + + ); +} diff --git a/client/src/features/menu/MenuBar.module.css b/client/src/features/menu/MenuBar.module.css new file mode 100644 index 000000000..00d82cfe7 --- /dev/null +++ b/client/src/features/menu/MenuBar.module.css @@ -0,0 +1,3 @@ +.gap { + height: 1em; +} diff --git a/client/src/features/menu/buttons/DownloadIconBtn.jsx b/client/src/features/menu/buttons/DownloadIconBtn.jsx new file mode 100644 index 000000000..d55c41738 --- /dev/null +++ b/client/src/features/menu/buttons/DownloadIconBtn.jsx @@ -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 ( + + } + colorScheme='white' + variant='outline' + isRound + borderColor='#fff1' + onClick={clickhandler} + _focus={{ boxShadow: 'none' }} + {...rest} + /> + + ); +} diff --git a/client/src/features/menu/buttons/HelpIconBtn.jsx b/client/src/features/menu/buttons/HelpIconBtn.jsx new file mode 100644 index 000000000..99eb6d12a --- /dev/null +++ b/client/src/features/menu/buttons/HelpIconBtn.jsx @@ -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 ( + + } + colorScheme='white' + variant='outline' + isRound + borderColor='#fff1' + onClick={clickhandler} + _focus={{ boxShadow: 'none' }} + {...rest} + /> + + ); +} diff --git a/client/src/features/menu/buttons/InfoIconBtn.jsx b/client/src/features/menu/buttons/InfoIconBtn.jsx new file mode 100644 index 000000000..118342c9f --- /dev/null +++ b/client/src/features/menu/buttons/InfoIconBtn.jsx @@ -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 ( + + } + colorScheme='white' + variant='outline' + isRound + borderColor='#fff1' + onClick={clickhandler} + _focus={{ boxShadow: 'none' }} + {...rest} + /> + + ); +} diff --git a/client/src/features/menu/buttons/MaxIconBtn.jsx b/client/src/features/menu/buttons/MaxIconBtn.jsx new file mode 100644 index 000000000..9724b7a8a --- /dev/null +++ b/client/src/features/menu/buttons/MaxIconBtn.jsx @@ -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 ( + + } + colorScheme='white' + variant='outline' + isRound + borderColor='#fff1' + onClick={clickhandler} + _focus={{ boxShadow: 'none' }} + {...rest} + /> + + ); +} diff --git a/client/src/features/menu/buttons/MinIconBtn.jsx b/client/src/features/menu/buttons/MinIconBtn.jsx new file mode 100644 index 000000000..2c51306d8 --- /dev/null +++ b/client/src/features/menu/buttons/MinIconBtn.jsx @@ -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 ( + + } + colorScheme='white' + variant='outline' + isRound + borderColor='#fff1' + onClick={clickhandler} + _focus={{ boxShadow: 'none' }} + {...rest} + /> + + ); +} diff --git a/client/src/features/menu/buttons/QuitIconBtn.jsx b/client/src/features/menu/buttons/QuitIconBtn.jsx new file mode 100644 index 000000000..e22b9cd05 --- /dev/null +++ b/client/src/features/menu/buttons/QuitIconBtn.jsx @@ -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 ( + + } + colorScheme='red' + variant='outline' + isRound + onClick={clickhandler} + _focus={{ boxShadow: 'none' }} + {...rest} + /> + + ); +} diff --git a/client/src/features/menu/buttons/SettingsIconBtn.jsx b/client/src/features/menu/buttons/SettingsIconBtn.jsx new file mode 100644 index 000000000..a8a3233af --- /dev/null +++ b/client/src/features/menu/buttons/SettingsIconBtn.jsx @@ -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 ( + + } + colorScheme='white' + variant='outline' + isRound + borderColor='#fff1' + onClick={clickhandler} + _focus={{ boxShadow: 'none' }} + {...rest} + /> + + ); +} diff --git a/client/src/features/modals/SettingsModal.jsx b/client/src/features/modals/SettingsModal.jsx index 401168e5f..7d4c507a0 100644 --- a/client/src/features/modals/SettingsModal.jsx +++ b/client/src/features/modals/SettingsModal.jsx @@ -14,12 +14,13 @@ import { Button, Textarea, } 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 { useFetch } from 'app/hooks/useFetch'; +import { EVENT_TABLE } from 'app/api/apiConstants'; export default function SettingsModal(props) { - const { data, status, isError } = useFetch(eventNamespace, fetchEvent); + const { data, status, isError } = useFetch(EVENT_TABLE, fetchEvent); const [formData, setFormData] = useState({ title: '', url: '', diff --git a/client/src/features/viewers/ViewWrapper.jsx b/client/src/features/viewers/ViewWrapper.jsx index 453625511..738d6acfb 100644 --- a/client/src/features/viewers/ViewWrapper.jsx +++ b/client/src/features/viewers/ViewWrapper.jsx @@ -1,9 +1,10 @@ import { useEffect, useState } from 'react'; -import { eventsNamespace, fetchAllEvents } from 'app/api/eventsApi'; -import { fetchEvent, eventNamespace } from 'app/api/eventApi'; +import { fetchAllEvents } from 'app/api/eventsApi'; +import { fetchEvent } from 'app/api/eventApi'; import { useSocket } from 'app/context/socketContext'; import { stringFromMillis } from 'common/dateConfig'; import { useFetch } from 'app/hooks/useFetch'; +import { EVENTS_TABLE, EVENT_TABLE } from 'app/api/apiConstants'; const withSocket = (Component) => { const WrappedComponent = (props) => { @@ -11,12 +12,12 @@ const withSocket = (Component) => { data: eventsData, status: eventsDataStatus, isError: eventsDataIsError, - } = useFetch(eventsNamespace, fetchAllEvents); + } = useFetch(EVENTS_TABLE, fetchAllEvents); const { data: genData, status: genDataStatus, isError: genDataIsError, - } = useFetch(eventNamespace, fetchEvent); + } = useFetch(EVENT_TABLE, fetchEvent); const [publicEvents, setPublicEvents] = useState([]); const [backstageEvents, setBackstageEvents] = useState([]); @@ -36,7 +37,7 @@ const withSocket = (Component) => { }); const [timer, setTimer] = useState({ clock: null, - currentSeconds: null, + running: null, startedAt: null, expectedFinish: null, }); @@ -220,7 +221,7 @@ const withSocket = (Component) => { // get clock string const timeManager = { ...timer, - finished: timer.running <= 0 && timer.startedAt, + finished: playback === 'start' && timer.running <= 0 && timer.startedAt, clock: stringFromMillis(timer.clock), playstate: playback, }; diff --git a/client/src/features/viewers/backstage/StageManager.jsx b/client/src/features/viewers/backstage/StageManager.jsx index 0fa299413..6ee5cadc8 100644 --- a/client/src/features/viewers/backstage/StageManager.jsx +++ b/client/src/features/viewers/backstage/StageManager.jsx @@ -42,7 +42,7 @@ export default function StageManager(props) { // Format messages const showPubl = publ.text !== '' && publ.visible; let stageTimer = formatDisplay(Math.abs(time.running), true); - if (time.running < 0) stageTimer = '-' + stageTimer; + if (time.running < 0) stageTimer = `-${stageTimer}`; // motion const titleVariants = { diff --git a/client/src/features/viewers/presenter/PresenterView.jsx b/client/src/features/viewers/presenter/PresenterView.jsx index 5d76d6df7..53a74faab 100644 --- a/client/src/features/viewers/presenter/PresenterView.jsx +++ b/client/src/features/viewers/presenter/PresenterView.jsx @@ -14,10 +14,9 @@ export default function PresenterView(props) { document.title = 'ontime - Speaker Screen'; }, []); - - const showOverlay = pres.text !== '' && pres.visible; - const isPlaying = time.playstate === 'start'; + const isPlaying = time.playstate !== 'pause'; + const normalisedTime = Math.max(time.running, 0); // motion const titleVariants = { @@ -61,7 +60,7 @@ export default function PresenterView(props) {
TIME UP
) : (
- +
)}
@@ -73,7 +72,7 @@ export default function PresenterView(props) { } > diff --git a/client/src/features/viewers/production/Pip.jsx b/client/src/features/viewers/production/Pip.jsx index c2dbe6d8d..b793d6e2b 100644 --- a/client/src/features/viewers/production/Pip.jsx +++ b/client/src/features/viewers/production/Pip.jsx @@ -3,7 +3,7 @@ import style from './Pip.module.css'; import Paginator from 'common/components/views/Paginator'; import NavLogo from 'common/components/nav/NavLogo'; 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 { ReactComponent as Emptyimage } from 'assets/images/empty.svg'; @@ -14,7 +14,7 @@ export default function Pip(props) { const [filteredEvents, setFilteredEvents] = useState(null); // calculcate pip size - useEffect(() => { + useLayoutEffect(() => { const h = ref.current.clientHeight; const w = ref.current.clientWidth; setSize(`${w} x ${h}`); @@ -51,11 +51,8 @@ export default function Pip(props) { // Format messages const showInfo = general.backstageInfo !== '' && general.backstageInfo != null; - - const stageTimer = - time.currentSeconds != null && !isNaN(time.currentSeconds) - ? formatDisplay(time.currentSeconds, true) - : ''; + let stageTimer = formatDisplay(Math.abs(time.running), true); + if (time.running < 0) stageTimer = `-${stageTimer}`; return (
diff --git a/client/yarn.lock b/client/yarn.lock index e0d883923..1062924a6 100644 --- a/client/yarn.lock +++ b/client/yarn.lock @@ -7575,6 +7575,11 @@ jest@26.6.0: import-local "^3.0.2" 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: version "0.8.0" resolved "https://registry.yarnpkg.com/js-sha3/-/js-sha3-0.8.0.tgz#b9b7a5da73afad7dedd0f8c463954cbde6818840" diff --git a/server/app.js b/server/app.js deleted file mode 100644 index 33c2af76a..000000000 --- a/server/app.js +++ /dev/null @@ -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); diff --git a/server/config.json b/server/config.json deleted file mode 100644 index 40d18e4a8..000000000 --- a/server/config.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "timer": { - "refresh": 1000 - }, - "server": { - "port": 4001 - }, - "database": { - "filename": "db.json", - "tablename": "events" - }, - "osc": { - "port": 8888 - } -} \ No newline at end of file diff --git a/server/controllers/eventController.js b/server/controllers/eventController.js deleted file mode 100644 index f78e4204e..000000000 --- a/server/controllers/eventController.js +++ /dev/null @@ -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); -}; diff --git a/server/data/.keep b/server/data/.keep new file mode 100644 index 000000000..e69de29bb diff --git a/server/data/eventsData.json b/server/data/eventsData.json deleted file mode 100644 index f5aadad75..000000000 --- a/server/data/eventsData.json +++ /dev/null @@ -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" - } -] \ No newline at end of file diff --git a/server/data/playbackData.json b/server/data/playbackData.json deleted file mode 100644 index 0337f8752..000000000 --- a/server/data/playbackData.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "current": null, - "next": null, - "currentTimer": null, - "numEvents": 0, - "state": "pause", - "prevState": "pause" -} \ No newline at end of file diff --git a/server/jsconfig.json b/server/jsconfig.json new file mode 100644 index 000000000..bbf408cd7 --- /dev/null +++ b/server/jsconfig.json @@ -0,0 +1,3 @@ +{ + "exclude": ["node_modules", "dist"] +} diff --git a/server/package.json b/server/package.json index 7442f2e1c..02d8cbab2 100644 --- a/server/package.json +++ b/server/package.json @@ -3,14 +3,16 @@ "version": "0.1.0", "author": "Carlos Valente", "description": "Time keeping for live events", + "type": "module", "main": "main.js", "dependencies": { "body-parser": "~1.19.0", "express": "~4.17.1", "express-session": "~1.17.1", - "lowdb": "^1.0.0", + "lowdb": "2.1.0", + "multer": "^1.4.2", "nanoid": "^3.1.22", - "node-osc": "^6.0.1", + "node-osc": "6.0.2", "passport": "~0.4.1", "passport-local": "~1.0.0", "socket.io": "^4.0.0" diff --git a/server/routes/eventRouter.js b/server/routes/eventRouter.js deleted file mode 100644 index 327fe30b5..000000000 --- a/server/routes/eventRouter.js +++ /dev/null @@ -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; diff --git a/server/routes/eventsRouter.js b/server/routes/eventsRouter.js deleted file mode 100644 index a156906cf..000000000 --- a/server/routes/eventsRouter.js +++ /dev/null @@ -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; diff --git a/server/src/app.js b/server/src/app.js new file mode 100644 index 000000000..e6d4dd214 --- /dev/null +++ b/server/src/app.js @@ -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); diff --git a/server/classes/EventTimer.js b/server/src/classes/EventTimer.js similarity index 78% rename from server/classes/EventTimer.js rename to server/src/classes/EventTimer.js index dd9a3cecf..e7bc9c0f5 100644 --- a/server/classes/EventTimer.js +++ b/server/src/classes/EventTimer.js @@ -1,5 +1,5 @@ -const Timer = require('./Timer'); -const socketIo = require('socket.io'); +import { Timer } from './Timer.js'; +import { Server } from 'socket.io'; /* * 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 DAYMS = 86400000; @@ -57,12 +57,12 @@ class EventTimer extends Timer { numEvents = null; _eventlist = null; - constructor(server, config) { + constructor(httpServer, config) { // call super constructor super(); // initialise socketIO server - this.io = socketIo(server, { + this.io = new Server(httpServer, { cors: { origin: '*', methods: 'GET,HEAD,PUT,PATCH,POST,DELETE', @@ -90,6 +90,10 @@ class EventTimer extends Timer { broadcastState() { this.io.emit('timer', this.getObject()); 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('next-id', this.nextEventId); this.io.emit('publicselected-id', this.selectedPublicEventId); @@ -105,8 +109,29 @@ class EventTimer extends Timer { update() { // if there is nothing selected, no nothing - if (this.selectedEventId == null) return; - super.update(); + if (this.selectedEventId == null && this.state !== 'roll') return; + + // 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() { @@ -136,10 +161,11 @@ class EventTimer extends Timer { else if (payload === 'next') this.next(); else if (payload === 'reload') this.reload(); else if (payload === 'unload') this.unload(); + else if (payload === 'roll') this.roll(); - // Not yet implemented - // else if (payload === 'roll') this.roll(); - + // TODO: Cleanup + // here tdo this.broadcastState; + // remove broadcast from functions this.broadcastThis('playstate', this.state); this.broadcastThis('selected-id', this.selectedEventId); this.broadcastThis('titles', this.titles); @@ -259,6 +285,13 @@ class EventTimer extends Timer { /*******************************************/ // selection data + socket.on('get-selected', () => { + socket.emit('selected', { + id: this.selectedEventId, + index: this.selectedEventIndex, + }); + }); + socket.on('get-selected-id', () => { socket.emit('selected-id', this.selectedEventId); }); @@ -342,6 +375,8 @@ class EventTimer extends Timer { } setupWithEventList(eventlist) { + if (!Array.isArray(eventlist) || eventlist.length < 1) return; + // filter only events const events = eventlist.filter((e) => e.type === 'event'); const numEvents = events.length; @@ -441,7 +476,7 @@ class EventTimer extends Timer { // update selected event index this.selectedEventIndex = this._eventlist.findIndex( - (e) => e.id === eventId + (e) => e.id === this.selectedEventId ); // reload titles if necessary @@ -459,18 +494,15 @@ class EventTimer extends Timer { if (eventIndex === -1) return; this.pause(); - this.loadEvent(eventIndex); - - this.broadcastState(); + this.loadEvent(eventIndex, 'load', true); } // Loads a given event // load timers // load selectedEventIndex // load titles - loadEvent(eventIndex, type = 'load') { + loadEvent(eventIndex, type = 'load', broadcastChange = 'false') { const e = this._eventlist[eventIndex]; - if (e == null) return; const start = e.timeStart == null || e.timeStart === '' ? 0 : e.timeStart; @@ -501,6 +533,10 @@ class EventTimer extends Timer { // look for event after this._loadTitlesNext(); + + if (broadcastChange) + // broadcast current state + this.broadcastState(); } _loadTitlesNow() { @@ -508,18 +544,12 @@ class EventTimer extends Timer { if (e == null) return; // 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 if (e.isPublic) { - this.titlesPublic.titleNow = e.title; - this.titlesPublic.subtitleNow = e.subtitle; - this.titlesPublic.presenterNow = e.presenter; - this.selectedPublicEventId = e.id; + this._loadThisTitles(e, 'now'); } else { + this._loadThisTitles(e, 'now-private'); + // assume there is no public event this.titlesPublic.titleNow = null; this.titlesPublic.subtitleNow = null; @@ -535,16 +565,77 @@ class EventTimer extends Timer { this._eventlist[i].type === 'event' && this._eventlist[i].isPublic ) { - this.titlesPublic.titleNow = this._eventlist[i].title; - this.titlesPublic.subtitleNow = this._eventlist[i].subtitle; - this.titlesPublic.presenterNow = this._eventlist[i].presenter; - this.selectedPublicEventId = this._eventlist[i].id; + this._loadThisTitles(this._eventlist[i], 'now-public'); 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() { // maybe there is nothing to load if (this.selectedEventIndex == null) return; @@ -569,19 +660,13 @@ class EventTimer extends Timer { if (this._eventlist[i].type === 'event') { // if we have not set private if (!nextPrivate) { - this.titles.titleNext = this._eventlist[i].title; - this.titles.subtitleNext = this._eventlist[i].subtitle; - this.titles.presenterNext = this._eventlist[i].presenter; - this.nextEventId = this._eventlist[i].id; + this._loadThisTitles(this._eventlist[i], 'next-private'); nextPrivate = true; } // if event is public if (this._eventlist[i].isPublic) { - this.titlesPublic.titleNext = this._eventlist[i].title; - this.titlesPublic.subtitleNext = this._eventlist[i].subtitle; - this.titlesPublic.presenterNext = this._eventlist[i].presenter; - this.nextPublicEventId = this._eventlist[i].id; + this._loadThisTitles(this._eventlist[i], 'next-public'); nextPublic = true; } } @@ -628,6 +713,7 @@ class EventTimer extends Timer { state = ${this.state} current = ${this.current} duration = ${this.duration} + secondaryTimer = ${this.secondaryTimer} Events ------------------------------ @@ -716,10 +802,77 @@ class EventTimer extends Timer { 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() { - console.log('roll: not yet implemented'); - return false; + // do we need to change + if (this.state === 'roll') return; + + // load into event + this.rollLoad(); + + // set state this.state = 'roll'; + + this.broadcastState(); } previous() { @@ -731,14 +884,15 @@ class EventTimer extends Timer { this.loadEvent(0); return; } + + // change playstate + this.pause(); + const gotoEvent = this.selectedEventIndex > 0 ? this.selectedEventIndex - 1 : 0; if (gotoEvent === this.selectedEventIndex) return; this.loadEvent(gotoEvent); - - // change playstate - this.pause(); } next() { @@ -751,6 +905,9 @@ class EventTimer extends Timer { return; } + // change playstate + this.pause(); + const gotoEvent = this.selectedEventIndex < this.numEvents - 1 ? this.selectedEventIndex + 1 @@ -758,9 +915,6 @@ class EventTimer extends Timer { if (gotoEvent === this.selectedEventIndex) return; this.loadEvent(gotoEvent); - - // change playstate - this.pause(); } unload() { @@ -775,12 +929,10 @@ class EventTimer extends Timer { } reload() { - // reload data - this.loadEvent(this.selectedEventIndex); - // reset playstate this.pause(); + + // reload data + this.loadEvent(this.selectedEventIndex); } } - -module.exports = EventTimer; diff --git a/server/classes/Timer.js b/server/src/classes/Timer.js similarity index 93% rename from server/classes/Timer.js rename to server/src/classes/Timer.js index 6fd83e5e3..29a5ca793 100644 --- a/server/classes/Timer.js +++ b/server/src/classes/Timer.js @@ -4,10 +4,11 @@ * */ -class Timer { +export class Timer { clock = null; duration = null; current = null; + secondaryTimer = null; _finishAt = null; _finishedAt = null; _startedAt = null; @@ -50,6 +51,9 @@ class Timer { // check playstate switch (this.state) { case 'start': + // ensure we have a start time + if (this._startedAt == null) this._startedAt = now; + // update current timer this.current = 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.secondaryTimer = null; this._finishAt = null; this._finishedAt = null; this._startedAt = null; @@ -132,7 +138,7 @@ class Timer { return { clock: this.clock, running: Timer.toSeconds(this.current), - currentSeconds: Timer.toSeconds(Math.max(this.current, 0)), + secondary: Timer.toSeconds(this.secondaryTimer), durationSeconds: Timer.toSeconds(this.duration), expectedFinish: this._getExpectedFinish(), startedAt: this._startedAt, @@ -210,6 +216,4 @@ class Timer { this._finishedAt = null; } } -} - -module.exports = Timer; +} \ No newline at end of file diff --git a/server/src/config/config.js b/server/src/config/config.js new file mode 100644 index 000000000..64ef4ef2b --- /dev/null +++ b/server/src/config/config.js @@ -0,0 +1,15 @@ +export const config = { + timer: { + refresh: 1000, + }, + server: { + port: 4001, + }, + database: { + filename: 'db.json', + tablename: 'events', + }, + osc: { + port: 8888, + }, +}; diff --git a/server/controllers/OscController.js b/server/src/controllers/OscController.js similarity index 71% rename from server/controllers/OscController.js rename to server/src/controllers/OscController.js index 5c46993d6..939ee829b 100644 --- a/server/controllers/OscController.js +++ b/server/src/controllers/OscController.js @@ -1,10 +1,13 @@ -const osc = require('node-osc'); +import { Server } from 'node-osc'; -const initiateOSC = (config) => { - const oscServer = new osc.Server(config.port, '0.0.0.0', () => { +export const initiateOSC = (config) => { + const oscServer = new Server(config.port, '0.0.0.0', () => { console.log(`OSC Server is listening on port ${config.port}`); }); + // error + oscServer.on('error', console.error); + oscServer.on('message', function (msg) { // message should look like /ontime/{path}/{args} where // ontime: fixed message for app @@ -20,7 +23,7 @@ const initiateOSC = (config) => { if (address !== 'ontime') return; // get second part (command) - switch (path) { + switch (path.toLocaleLowerCase()) { case 'start': case 'play': console.log('calling play'); @@ -61,7 +64,19 @@ const initiateOSC = (config) => { case 'goto': console.log('calling goto with', args); 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) { console.log('error calling goto: ', error); } @@ -73,5 +88,3 @@ const initiateOSC = (config) => { } }); }; - -module.exports = { initiateOSC }; diff --git a/server/src/controllers/eventController.js b/server/src/controllers/eventController.js new file mode 100644 index 000000000..938e412b7 --- /dev/null +++ b/server/src/controllers/eventController.js @@ -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); + } +}; diff --git a/server/controllers/eventsController.js b/server/src/controllers/eventsController.js similarity index 69% rename from server/controllers/eventsController.js rename to server/src/controllers/eventsController.js index 9c1306e15..92fe2bf71 100644 --- a/server/controllers/eventsController.js +++ b/server/src/controllers/eventsController.js @@ -1,22 +1,26 @@ // get database -const db = require('../app.js').db; +import { db, data } from '../app.js'; // utils -const customAlphabet = require('nanoid').customAlphabet; +import { customAlphabet } from 'nanoid'; 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() { - return db.get('events').size().value(); + return Array.from(data.events).length; } 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 - let events = db.get('events').value(); + let events = data.events; let count = events.length; let order = entry.order; @@ -39,15 +43,18 @@ function _insertAt(entry, index) { } // save events - db.set('events', events).write(); + data.events = events; + await db.write(); } -function _removeById(eventId) { - return db.get('events').remove({ id: eventId }).write(); +async function _removeById(eventId) { + data.events = Array.from(data.events).filter((e) => e.id != eventId); + await db.write(); } 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 @@ -68,21 +75,21 @@ function _deleteTimerId(entryId) { // Create controller for GET request to '/events' // Returns - -exports.eventsGetAll = async (req, res) => { - const results = db.get('events').value(); - res.json(results); +export const eventsGetAll = async (req, res) => { + res.json(data.events); }; // Create controller for GET request to '/events/:eventId' // Returns - -exports.eventsGetById = async (req, res) => { - const e = db.get('events').find({ id: req.params.eventId }).value(); +export const eventsGetById = async (req, res) => { + const e = data.events.find({ id: req.params.eventId }).value(); + console.log('event by id', e); res.json(e); }; // Create controller for POST request to '/events/' // Returns - -exports.eventsPost = async (req, res) => { +export const eventsPost = async (req, res) => { // TODO: Validate event if (!req.body) { res.status(400).send(`No object found in request`); @@ -95,13 +102,13 @@ exports.eventsPost = async (req, res) => { switch (req.body.type) { case 'event': - newEvent = { ...eventDefs.event, ...req.body }; + newEvent = { ...eventDef, ...req.body }; break; case 'delay': - newEvent = { ...eventDefs.delay, ...req.body }; + newEvent = { ...delayDef, ...req.body }; break; case 'block': - newEvent = { ...eventDefs.block, ...req.body }; + newEvent = { ...blockDef, ...req.body }; break; default: @@ -131,7 +138,7 @@ exports.eventsPost = async (req, res) => { // Create controller for PUT request to '/events/' // Returns - -exports.eventsPut = async (req, res) => { +export const eventsPut = async (req, res) => { // no valid params if (!req.body) { res.status(400).send(`No object found`); @@ -145,29 +152,35 @@ exports.eventsPut = async (req, res) => { } try { - db.get('events') - .find({ id: req.body.id }) - .assign({ ...req.body }) - .update('revision', (n) => n + 1) - .write(); + const eventIndex = data.events.findIndex((e) => e.id === eventId); + if (eventIndex === -1) { + res.status(400).send(`No Id found found`); + return; + } + + const e = data.events[eventIndex]; + data.events[eventIndex] = { ...e, ...req.body }; + data.events[eventIndex].revision++; + db.write(); // update timer - _updateTimersSingle(req.body.id, req.body); + _updateTimersSingle(eventId, req.body); res.sendStatus(200); } catch (error) { + console.log(error); res.status(400).send(error); } }; // Create controller for PATCH request to '/events/' // Returns - -exports.eventsPatch = async (req, res) => { +export const eventsPatch = async (req, res) => { // 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 if (!req.body) { res.status(400).send(`No object found in request`); @@ -177,7 +190,7 @@ exports.eventsReorder = async (req, res) => { const { index, from, to } = req.body; // get events - let events = db.get('events').value(); + let events = data.events; let idx = events.findIndex((e) => e.id === index, from); // Check if item is at given index @@ -194,7 +207,8 @@ exports.eventsReorder = async (req, res) => { events.splice(to, 0, reorderedItem); // save events - db.set('events', events).write(); + data.events = events; + db.write(); // TODO: would it be more efficient to reorder at timer? // update timer @@ -209,7 +223,7 @@ exports.eventsReorder = async (req, res) => { // Create controller for PATCH request to '/events/applydelay/:eventId' // Returns - -exports.eventsApplyDelay = async (req, res) => { +export const eventsApplyDelay = async (req, res) => { // no valid params if (!req.params.eventId) { res.status(400).send(`No id found in request`); @@ -218,7 +232,7 @@ exports.eventsApplyDelay = async (req, res) => { try { // get events - let events = db.get('events').value(); + let events = data.events; // AUX let delayIndex = null; @@ -259,7 +273,8 @@ exports.eventsApplyDelay = async (req, res) => { if (blockIndex) events.splice(blockIndex - 1, 1); // update events - db.set('events', events).write(); + data.events = events; + db.write(); // update timer _updateTimers(); @@ -274,7 +289,7 @@ exports.eventsApplyDelay = async (req, res) => { // Create controller for DELETE request to '/events/:eventId' // Returns - -exports.eventsDelete = async (req, res) => { +export const eventsDelete = async (req, res) => { // no valid params if (!req.params.eventId) { res.status(400).send(`No id found in request`); @@ -295,3 +310,20 @@ exports.eventsDelete = async (req, res) => { 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); + } +}; diff --git a/server/src/controllers/ontimeController.js b/server/src/controllers/ontimeController.js new file mode 100644 index 000000000..2d74bb73f --- /dev/null +++ b/server/src/controllers/ontimeController.js @@ -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 }); + } +}; diff --git a/server/controllers/playbackController.js b/server/src/controllers/playbackController.js similarity index 71% rename from server/controllers/playbackController.js rename to server/src/controllers/playbackController.js index 4dcc45cb3..b8a72f5a5 100644 --- a/server/controllers/playbackController.js +++ b/server/src/controllers/playbackController.js @@ -1,64 +1,64 @@ // Create controller for GET request to '/playback' // Returns ACK message -exports.pbGet = async (req, res) => { +export const pbGet = async (req, res) => { res.send(global.timer.playState); }; // Create controller for GET request to '/playback/start' // Starts timer object -exports.pbStart = async (req, res) => { +export const pbStart = async (req, res) => { global.timer.start(); res.sendStatus(200); }; // Create controller for GET request to '/playback/pause' // Pauses timer object -exports.pbPause = async (req, res) => { +export const pbPause = async (req, res) => { global.timer.pause(); res.sendStatus(200); }; // Create controller for GET request to '/playback/stop' // Stops timer object -exports.pbStop = async (req, res) => { +export const pbStop = async (req, res) => { global.timer.stop(); res.sendStatus(200); }; // Create controller for GET request to '/playback/roll' // Sets timer object to roll mode -exports.pbRoll = async (req, res) => { +export const pbRoll = async (req, res) => { global.timer.roll(); res.sendStatus(501); }; // Create controller for GET request to '/playback/previous' // Sets timer object to roll mode -exports.pbPrevious = async (req, res) => { +export const pbPrevious = async (req, res) => { global.timer.previous(); res.sendStatus(200); }; // Create controller for GET request to '/playback/next' // Sets timer object to roll mode -exports.pbNext = async (req, res) => { +export const pbNext = async (req, res) => { global.timer.next(); res.sendStatus(200); }; // Create controller for GET request to '/playback/unload' // Unloads any events -exports.pbUnload = async (req, res) => { +export const pbUnload = async (req, res) => { global.timer.unload(); - console.log('debug: unload called') + console.log('debug: unload called'); res.sendStatus(200); }; // Create controller for GET request to '/playback/reload' // Reloads current event -exports.pbReload = async (req, res) => { +export const pbReload = async (req, res) => { global.timer.reload(); - console.log('debug: reload called') + console.log('debug: reload called'); res.sendStatus(200); }; diff --git a/server/data/dataModel.js b/server/src/data/dataModel.js similarity index 73% rename from server/data/dataModel.js rename to server/src/data/dataModel.js index cc6f67470..39e781b87 100644 --- a/server/data/dataModel.js +++ b/server/src/data/dataModel.js @@ -1,10 +1,10 @@ -const dbModel = { +export const dbModel = { events: [], event: { title: '', url: '', publicInfo: '', - backStageInfo: '', + backstageInfo: '', }, settings: { app: 'ontime', @@ -14,5 +14,3 @@ const dbModel = { lock: false, }, }; - -module.exports = { dbModel }; diff --git a/server/data/eventsDefinition.js b/server/src/data/eventsDefinition.js similarity index 68% rename from server/data/eventsDefinition.js rename to server/src/data/eventsDefinition.js index 0765f5b60..f1f326ee8 100644 --- a/server/data/eventsDefinition.js +++ b/server/src/data/eventsDefinition.js @@ -1,4 +1,4 @@ -const event = { +export const event = { title: '', subtitle: '', presenter: '', @@ -10,13 +10,12 @@ const event = { revision: 0, }; -const delay = { +export const delay = { duration: 0, type: 'delay', + revision: 0, }; -const block = { +export const block = { type: 'block', }; - -module.exports = { event, delay, block }; diff --git a/server/src/routes/eventRouter.js b/server/src/routes/eventRouter.js new file mode 100644 index 000000000..9b929f95d --- /dev/null +++ b/server/src/routes/eventRouter.js @@ -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); diff --git a/server/src/routes/eventsRouter.js b/server/src/routes/eventsRouter.js new file mode 100644 index 000000000..9ea280c45 --- /dev/null +++ b/server/src/routes/eventsRouter.js @@ -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); diff --git a/server/src/routes/ontimeRouter.js b/server/src/routes/ontimeRouter.js new file mode 100644 index 000000000..e5b5e5670 --- /dev/null +++ b/server/src/routes/ontimeRouter.js @@ -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); diff --git a/server/routes/playbackRouter.js b/server/src/routes/playbackRouter.js similarity index 92% rename from server/routes/playbackRouter.js rename to server/src/routes/playbackRouter.js index e767a3156..3b71b9c86 100644 --- a/server/routes/playbackRouter.js +++ b/server/src/routes/playbackRouter.js @@ -1,5 +1,5 @@ -const express = require('express'); -const router = express.Router(); +import express from 'express'; +export const router = express.Router(); // import event controller const playbackController = require('../controllers/playbackController'); @@ -30,5 +30,3 @@ router.get('/unload', playbackController.pbUnload); // create route between controller and '/playback/reload' endpoint router.get('/reload', playbackController.pbReload); - -module.exports = router; diff --git a/server/src/utils/upload.js b/server/src/utils/upload.js new file mode 100644 index 000000000..e489ac26f --- /dev/null +++ b/server/src/utils/upload.js @@ -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');