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/common/components/buttons/CollapseBtn.jsx b/client/src/common/components/buttons/CollapseBtn.jsx index 490af34bb..77891b087 100644 --- a/client/src/common/components/buttons/CollapseBtn.jsx +++ b/client/src/common/components/buttons/CollapseBtn.jsx @@ -1,18 +1,20 @@ -import { Button } from '@chakra-ui/button'; +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/CurrentBtn.jsx b/client/src/common/components/buttons/CurrentBtn.jsx index 2bea19028..a15c62aa0 100644 --- a/client/src/common/components/buttons/CurrentBtn.jsx +++ b/client/src/common/components/buttons/CurrentBtn.jsx @@ -2,13 +2,13 @@ import { Button } from '@chakra-ui/button'; import { FiTarget } from 'react-icons/fi'; export default function CurrentBtn(props) { - const { clickhandler } = props; + const { clickhandler, active } = 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..d52030fd1 --- /dev/null +++ b/client/src/common/components/buttons/LockIconBtn.jsx @@ -0,0 +1,21 @@ +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 ( + + } + colorScheme='pink' + color={'pink.300'} + variant={active ? 'solid' : 'outline'} + onClick={clickhandler} + _focus={{ boxShadow: 'none' }} + /> + + ); +} diff --git a/client/src/features/editors/list/EventList.jsx b/client/src/features/editors/list/EventList.jsx index 51c582c90..dc6c0b913 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 [cursor, setCursor] = useState(0); + const [cursorSettings] = useAtom(useMemo(() => SelectSetting('cursor'), [])); + + const cursorRef = createRef(); // Handle keyboard shortcuts useEffect(() => { @@ -52,12 +56,13 @@ export default function EventList(props) { 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) => { + socket.on('selected', (data) => { setSelected(data); + console.log('debug cursor setted', data); }); socket.on('next-id', (data) => { setNext(data); @@ -65,31 +70,35 @@ export default function EventList(props) { // Clear listener return () => { - socket.off('selected-id'); + socket.off('selected'); socket.off('next-id'); }; }, [socket]); + // attach cursor to selected + useEffect(() => { + if (cursorSettings !== 'locked' || selected == null) return; + if (selected.index == null) return; + + setCursor(selected.index); + }, [selected, cursorSettings]); + + // attach scroll to cursor + useEffect(() => { + if (cursor == null || cursorRef.current == null) return; + console.log('debug cursor scrolling'); + + cursorRef.current.scrollIntoView({ + behavior: 'smooth', + block: 'nearest', + inline: 'start', + }); + }, [cursor, cursorRef]); + 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 @@ -109,19 +118,10 @@ export default function EventList(props) { console.log('EventList: events in event list', events); let cumulativeDelay = 0; + console.log('debug selected', selected); + return (
- - {cursor === -1 && ( - - )} - {(provided) => ( @@ -131,33 +131,27 @@ export default function EventList(props) { ref={provided.innerRef} > {events.map((e, index) => { + let isCursor = cursor === index; if (index === 0) cumulativeDelay = 0; if (e.type === 'delay' && e.duration != null) { cumulativeDelay += e.duration; } else if (e.type === 'block') cumulativeDelay = 0; return ( - +
- - {cursor === index && ( - - )} - - +
); })} {provided.placeholder} 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 aba254a80..912c47b95 100644 --- a/client/src/features/menu/EventListMenu.jsx +++ b/client/src/features/menu/EventListMenu.jsx @@ -1,34 +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 CurrentBtn from 'common/components/buttons/CurrentBtn'; +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) { @@ -41,6 +23,13 @@ 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; default: break; } @@ -48,36 +37,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/server/classes/EventTimer.js b/server/classes/EventTimer.js index 960b21af5..6059a7a18 100644 --- a/server/classes/EventTimer.js +++ b/server/classes/EventTimer.js @@ -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); @@ -259,6 +263,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); });