mirror of
https://github.com/cpvalente/ontime.git
synced 2026-08-05 15:33:59 +00:00
@@ -0,0 +1,34 @@
|
||||
const { atom } = require('jotai');
|
||||
|
||||
const PATH = 'option-gen';
|
||||
const initialValue = {
|
||||
cursor: 'locked',
|
||||
};
|
||||
|
||||
export const settingsAtom = atom(
|
||||
(get) => {
|
||||
const storedOptions = localStorage.getItem(PATH);
|
||||
if (storedOptions == null) return initialValue;
|
||||
|
||||
return JSON.parse(storedOptions);
|
||||
},
|
||||
(get, set, newValues) => {
|
||||
set(settingsAtom, newValues);
|
||||
localStorage.setItem(PATH, JSON.stringify(newValues));
|
||||
}
|
||||
);
|
||||
|
||||
// get a single option, if it exists
|
||||
export const SelectSetting = (setting) => {
|
||||
return atom((get) => get(settingsAtom)[setting]);
|
||||
};
|
||||
|
||||
// change a single item in object
|
||||
export const HandleOptions = atom(null, (get, set, payload) => {
|
||||
const updatedVal = {
|
||||
...get(settingsAtom),
|
||||
...payload,
|
||||
};
|
||||
set(settingsAtom, updatedVal);
|
||||
localStorage.setItem(PATH, JSON.stringify(updatedVal));
|
||||
});
|
||||
@@ -1,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 (
|
||||
<Button
|
||||
size={props.size || 'xs'}
|
||||
leftIcon={<FiChevronsUp />}
|
||||
colorScheme='white'
|
||||
variant='outline'
|
||||
onClick={clickhandler}
|
||||
_focus={{ boxShadow: 'none' }}
|
||||
>
|
||||
Collapse All
|
||||
</Button>
|
||||
<Tooltip label='Collapse all'>
|
||||
<IconButton
|
||||
size={props.size || 'xs'}
|
||||
icon={<FiChevronsUp />}
|
||||
colorScheme='white'
|
||||
variant='outline'
|
||||
background='#fff1'
|
||||
onClick={clickhandler}
|
||||
_focus={{ boxShadow: 'none' }}
|
||||
/>
|
||||
</Tooltip>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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 (
|
||||
<Button
|
||||
size={props.size || 'xs'}
|
||||
leftIcon={<FiTarget />}
|
||||
colorScheme='whiteAlpha'
|
||||
variant='outline'
|
||||
variant={active ? 'solid' : 'outline'}
|
||||
onClick={clickhandler}
|
||||
_focus={{ boxShadow: 'none' }}
|
||||
>
|
||||
|
||||
@@ -1,18 +1,20 @@
|
||||
import { Button } from '@chakra-ui/button';
|
||||
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 (
|
||||
<Button
|
||||
size={props.size || 'xs'}
|
||||
leftIcon={<FiChevronsDown />}
|
||||
colorScheme='white'
|
||||
variant='outline'
|
||||
onClick={clickhandler}
|
||||
_focus={{ boxShadow: 'none' }}
|
||||
>
|
||||
Expand All
|
||||
</Button>
|
||||
<Tooltip label='Expand all'>
|
||||
<IconButton
|
||||
size={props.size || 'xs'}
|
||||
icon={<FiChevronsDown />}
|
||||
colorScheme='white'
|
||||
variant='outline'
|
||||
background='#fff1'
|
||||
onClick={clickhandler}
|
||||
_focus={{ boxShadow: 'none' }}
|
||||
/>
|
||||
</Tooltip>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,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 (
|
||||
<Tooltip label='Lock cursor to current'>
|
||||
<IconButton
|
||||
ref={ref}
|
||||
size={props.size || 'xs'}
|
||||
icon={<FiTarget />}
|
||||
colorScheme='pink'
|
||||
color={'pink.300'}
|
||||
variant={active ? 'solid' : 'outline'}
|
||||
onClick={clickhandler}
|
||||
_focus={{ boxShadow: 'none' }}
|
||||
/>
|
||||
</Tooltip>
|
||||
);
|
||||
}
|
||||
@@ -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 <Empty text='No Events' />;
|
||||
}
|
||||
|
||||
// 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 (
|
||||
<div className={style.eventContainer}>
|
||||
<AnimatePresence>
|
||||
{cursor === -1 && (
|
||||
<motion.div
|
||||
className={style.cursor}
|
||||
variants={cursorVariants}
|
||||
initial='hidden'
|
||||
animate='visible'
|
||||
exit='exit'
|
||||
/>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
<DragDropContext onDragEnd={handleOnDragEnd}>
|
||||
<Droppable droppableId='eventlist'>
|
||||
{(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 (
|
||||
<Fragment key={e.id}>
|
||||
<div
|
||||
ref={isCursor ? cursorRef : undefined}
|
||||
key={e.id}
|
||||
className={isCursor ? style.cursor : undefined}
|
||||
>
|
||||
<EventListItem
|
||||
type={e.type}
|
||||
index={index}
|
||||
data={e}
|
||||
selected={selected === e.id}
|
||||
selected={selected?.id === e.id}
|
||||
next={next === e.id}
|
||||
eventsHandler={eventsHandler}
|
||||
delay={cumulativeDelay}
|
||||
/>
|
||||
<AnimatePresence>
|
||||
{cursor === index && (
|
||||
<motion.div
|
||||
className={style.cursor}
|
||||
variants={cursorVariants}
|
||||
initial='hidden'
|
||||
animate='visible'
|
||||
exit='exit'
|
||||
/>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
</Fragment>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
{provided.placeholder}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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 (
|
||||
<div className={style.headerButtons}>
|
||||
<Menu className={style.menu} isLazy>
|
||||
<ButtonGroup isAttached>
|
||||
<Button {...buttonProps}>Upload</Button>
|
||||
<MenuButton as={Button} {...buttonProps}>
|
||||
<FiChevronDown />
|
||||
</MenuButton>
|
||||
</ButtonGroup>
|
||||
<MenuList style={menuStyle}>
|
||||
<MenuItem>Upload Excel</MenuItem>
|
||||
<MenuItem>Upload CSV</MenuItem>
|
||||
</MenuList>
|
||||
</Menu>
|
||||
<Menu>
|
||||
<ButtonGroup isAttached>
|
||||
<Button {...buttonProps}>Save</Button>
|
||||
<MenuButton as={Button} {...buttonProps}>
|
||||
<FiChevronDown />
|
||||
</MenuButton>
|
||||
</ButtonGroup>
|
||||
<MenuList style={menuStyle}>
|
||||
<MenuItem>Download Excel</MenuItem>
|
||||
<MenuItem>Download CSV</MenuItem>
|
||||
</MenuList>
|
||||
</Menu>
|
||||
<CurrentBtn size='sm' />
|
||||
<CollapseBtn
|
||||
size='sm'
|
||||
clickhandler={() => eventsHandler('collapseall')}
|
||||
/>
|
||||
<ExpandBtn size='sm' clickhandler={() => eventsHandler('expandall')} />
|
||||
<Divider orientation='vertical' />
|
||||
<LockIconBtn
|
||||
size='sm'
|
||||
clickhandler={() => actionHandler('togglelock')}
|
||||
active={cursorSettings === 'locked'}
|
||||
/>
|
||||
<Divider orientation='vertical' />
|
||||
<MenuActionButtons actionHandler={actionHandler} size='sm' />
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user