mirror of
https://github.com/cpvalente/ontime.git
synced 2026-08-20 06:29:07 +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';
|
import { FiChevronsUp } from 'react-icons/fi';
|
||||||
|
|
||||||
export default function CollapseBtn(props) {
|
export default function CollapseBtn(props) {
|
||||||
const { clickhandler } = props;
|
const { clickhandler } = props;
|
||||||
return (
|
return (
|
||||||
<Button
|
<Tooltip label='Collapse all'>
|
||||||
size={props.size || 'xs'}
|
<IconButton
|
||||||
leftIcon={<FiChevronsUp />}
|
size={props.size || 'xs'}
|
||||||
colorScheme='white'
|
icon={<FiChevronsUp />}
|
||||||
variant='outline'
|
colorScheme='white'
|
||||||
onClick={clickhandler}
|
variant='outline'
|
||||||
_focus={{ boxShadow: 'none' }}
|
background='#fff1'
|
||||||
>
|
onClick={clickhandler}
|
||||||
Collapse All
|
_focus={{ boxShadow: 'none' }}
|
||||||
</Button>
|
/>
|
||||||
|
</Tooltip>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,13 +2,13 @@ import { Button } from '@chakra-ui/button';
|
|||||||
import { FiTarget } from 'react-icons/fi';
|
import { FiTarget } from 'react-icons/fi';
|
||||||
|
|
||||||
export default function CurrentBtn(props) {
|
export default function CurrentBtn(props) {
|
||||||
const { clickhandler } = props;
|
const { clickhandler, active } = props;
|
||||||
return (
|
return (
|
||||||
<Button
|
<Button
|
||||||
size={props.size || 'xs'}
|
size={props.size || 'xs'}
|
||||||
leftIcon={<FiTarget />}
|
leftIcon={<FiTarget />}
|
||||||
colorScheme='whiteAlpha'
|
colorScheme='whiteAlpha'
|
||||||
variant='outline'
|
variant={active ? 'solid' : 'outline'}
|
||||||
onClick={clickhandler}
|
onClick={clickhandler}
|
||||||
_focus={{ boxShadow: 'none' }}
|
_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';
|
import { FiChevronsDown } from 'react-icons/fi';
|
||||||
|
|
||||||
export default function ExpandBtn(props) {
|
export default function ExpandBtn(props) {
|
||||||
const { clickhandler } = props;
|
const { clickhandler } = props;
|
||||||
return (
|
return (
|
||||||
<Button
|
<Tooltip label='Expand all'>
|
||||||
size={props.size || 'xs'}
|
<IconButton
|
||||||
leftIcon={<FiChevronsDown />}
|
size={props.size || 'xs'}
|
||||||
colorScheme='white'
|
icon={<FiChevronsDown />}
|
||||||
variant='outline'
|
colorScheme='white'
|
||||||
onClick={clickhandler}
|
variant='outline'
|
||||||
_focus={{ boxShadow: 'none' }}
|
background='#fff1'
|
||||||
>
|
onClick={clickhandler}
|
||||||
Expand All
|
_focus={{ boxShadow: 'none' }}
|
||||||
</Button>
|
/>
|
||||||
|
</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 style from './List.module.css';
|
||||||
import { Fragment, useEffect, useState } from 'react';
|
import { createRef, useEffect, useMemo, useState } from 'react';
|
||||||
import { useSocket } from 'app/context/socketContext';
|
import { useSocket } from 'app/context/socketContext';
|
||||||
import tinykeys from 'tinykeys';
|
import tinykeys from 'tinykeys';
|
||||||
import Empty from 'common/state/Empty';
|
import Empty from 'common/state/Empty';
|
||||||
import EventListItem from './EventListItem';
|
import EventListItem from './EventListItem';
|
||||||
import { AnimatePresence, motion } from 'framer-motion';
|
|
||||||
import { DragDropContext, Droppable } from 'react-beautiful-dnd';
|
import { DragDropContext, Droppable } from 'react-beautiful-dnd';
|
||||||
|
import { useAtom } from 'jotai';
|
||||||
|
import { SelectSetting } from 'app/context/settingsAtom';
|
||||||
|
|
||||||
export default function EventList(props) {
|
export default function EventList(props) {
|
||||||
const { events, eventsHandler } = props;
|
const { events, eventsHandler } = props;
|
||||||
const socket = useSocket();
|
const socket = useSocket();
|
||||||
const [selected, setSelected] = useState(null);
|
const [selected, setSelected] = useState(null);
|
||||||
const [next, setNext] = 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
|
// Handle keyboard shortcuts
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -52,12 +56,13 @@ export default function EventList(props) {
|
|||||||
if (socket == null) return;
|
if (socket == null) return;
|
||||||
|
|
||||||
// ask for playstate
|
// ask for playstate
|
||||||
socket.emit('get-selected-id');
|
socket.emit('get-selected');
|
||||||
socket.emit('get-next-id');
|
socket.emit('get-next-id');
|
||||||
|
|
||||||
// Handle playstate
|
// Handle playstate
|
||||||
socket.on('selected-id', (data) => {
|
socket.on('selected', (data) => {
|
||||||
setSelected(data);
|
setSelected(data);
|
||||||
|
console.log('debug cursor setted', data);
|
||||||
});
|
});
|
||||||
socket.on('next-id', (data) => {
|
socket.on('next-id', (data) => {
|
||||||
setNext(data);
|
setNext(data);
|
||||||
@@ -65,31 +70,35 @@ export default function EventList(props) {
|
|||||||
|
|
||||||
// Clear listener
|
// Clear listener
|
||||||
return () => {
|
return () => {
|
||||||
socket.off('selected-id');
|
socket.off('selected');
|
||||||
socket.off('next-id');
|
socket.off('next-id');
|
||||||
};
|
};
|
||||||
}, [socket]);
|
}, [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) {
|
if (events.length < 1) {
|
||||||
return <Empty text='No Events' />;
|
return <Empty text='No Events' />;
|
||||||
}
|
}
|
||||||
|
|
||||||
// motion
|
|
||||||
const cursorVariants = {
|
|
||||||
hidden: {
|
|
||||||
scale: 0,
|
|
||||||
},
|
|
||||||
visible: {
|
|
||||||
scale: 1,
|
|
||||||
transition: {
|
|
||||||
duration: 0.3,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
exit: {
|
|
||||||
scale: 0,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
// DND
|
// DND
|
||||||
const handleOnDragEnd = (result) => {
|
const handleOnDragEnd = (result) => {
|
||||||
// drop outside of area
|
// drop outside of area
|
||||||
@@ -109,19 +118,10 @@ export default function EventList(props) {
|
|||||||
console.log('EventList: events in event list', events);
|
console.log('EventList: events in event list', events);
|
||||||
let cumulativeDelay = 0;
|
let cumulativeDelay = 0;
|
||||||
|
|
||||||
|
console.log('debug selected', selected);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={style.eventContainer}>
|
<div className={style.eventContainer}>
|
||||||
<AnimatePresence>
|
|
||||||
{cursor === -1 && (
|
|
||||||
<motion.div
|
|
||||||
className={style.cursor}
|
|
||||||
variants={cursorVariants}
|
|
||||||
initial='hidden'
|
|
||||||
animate='visible'
|
|
||||||
exit='exit'
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</AnimatePresence>
|
|
||||||
<DragDropContext onDragEnd={handleOnDragEnd}>
|
<DragDropContext onDragEnd={handleOnDragEnd}>
|
||||||
<Droppable droppableId='eventlist'>
|
<Droppable droppableId='eventlist'>
|
||||||
{(provided) => (
|
{(provided) => (
|
||||||
@@ -131,33 +131,27 @@ export default function EventList(props) {
|
|||||||
ref={provided.innerRef}
|
ref={provided.innerRef}
|
||||||
>
|
>
|
||||||
{events.map((e, index) => {
|
{events.map((e, index) => {
|
||||||
|
let isCursor = cursor === index;
|
||||||
if (index === 0) cumulativeDelay = 0;
|
if (index === 0) cumulativeDelay = 0;
|
||||||
if (e.type === 'delay' && e.duration != null) {
|
if (e.type === 'delay' && e.duration != null) {
|
||||||
cumulativeDelay += e.duration;
|
cumulativeDelay += e.duration;
|
||||||
} else if (e.type === 'block') cumulativeDelay = 0;
|
} else if (e.type === 'block') cumulativeDelay = 0;
|
||||||
return (
|
return (
|
||||||
<Fragment key={e.id}>
|
<div
|
||||||
|
ref={isCursor ? cursorRef : undefined}
|
||||||
|
key={e.id}
|
||||||
|
className={isCursor ? style.cursor : undefined}
|
||||||
|
>
|
||||||
<EventListItem
|
<EventListItem
|
||||||
type={e.type}
|
type={e.type}
|
||||||
index={index}
|
index={index}
|
||||||
data={e}
|
data={e}
|
||||||
selected={selected === e.id}
|
selected={selected?.id === e.id}
|
||||||
next={next === e.id}
|
next={next === e.id}
|
||||||
eventsHandler={eventsHandler}
|
eventsHandler={eventsHandler}
|
||||||
delay={cumulativeDelay}
|
delay={cumulativeDelay}
|
||||||
/>
|
/>
|
||||||
<AnimatePresence>
|
</div>
|
||||||
{cursor === index && (
|
|
||||||
<motion.div
|
|
||||||
className={style.cursor}
|
|
||||||
variants={cursorVariants}
|
|
||||||
initial='hidden'
|
|
||||||
animate='visible'
|
|
||||||
exit='exit'
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</AnimatePresence>
|
|
||||||
</Fragment>
|
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
{provided.placeholder}
|
{provided.placeholder}
|
||||||
|
|||||||
@@ -21,11 +21,14 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.cursor {
|
.cursor {
|
||||||
width: 90%;
|
width: 100%;
|
||||||
min-height: 2px;
|
background: linear-gradient(
|
||||||
background-color: #ff7597;
|
180deg,
|
||||||
border-radius: 2px;
|
#ff7597 2%,
|
||||||
margin: 0 auto;
|
#0001 3%,
|
||||||
/* transition: 1s;
|
#0001 97%,
|
||||||
transition-property: all; */
|
#ff7597 98%
|
||||||
|
);
|
||||||
|
|
||||||
|
border-radius: 14px;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,34 +1,16 @@
|
|||||||
import { memo } from 'react';
|
import { memo, useMemo } from 'react';
|
||||||
import { FiChevronDown } from 'react-icons/fi';
|
import { Divider } from '@chakra-ui/react';
|
||||||
import {
|
|
||||||
Button,
|
|
||||||
ButtonGroup,
|
|
||||||
Menu,
|
|
||||||
MenuButton,
|
|
||||||
MenuItem,
|
|
||||||
MenuList,
|
|
||||||
} from '@chakra-ui/react';
|
|
||||||
import style from './EventListMenu.module.css';
|
import style from './EventListMenu.module.css';
|
||||||
import MenuActionButtons from '../editors/list/MenuActionButtons';
|
import MenuActionButtons from './MenuActionButtons';
|
||||||
import CollapseBtn from 'common/components/buttons/CollapseBtn';
|
import CollapseBtn from 'common/components/buttons/CollapseBtn';
|
||||||
import ExpandBtn from 'common/components/buttons/ExpandBtn';
|
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 EventListMenu = ({ eventsHandler }) => {
|
||||||
const buttonProps = {
|
const [cursorSettings] = useAtom(useMemo(() => SelectSetting('cursor'), []));
|
||||||
size: 'sm',
|
const [, SetOption] = useAtom(HandleOptions);
|
||||||
variant: 'outline',
|
|
||||||
colorScheme: 'whiteAlpha',
|
|
||||||
backgroundColor: '#ffffff05',
|
|
||||||
_hover: { bg: 'blue.800' },
|
|
||||||
_expanded: { bg: 'blue.400' },
|
|
||||||
_focus: { boxShadow: 'none' },
|
|
||||||
};
|
|
||||||
|
|
||||||
const menuStyle = {
|
|
||||||
color: 'initial',
|
|
||||||
backgroundColor: 'rgba(255,255,255,0.67)',
|
|
||||||
};
|
|
||||||
|
|
||||||
const actionHandler = (action) => {
|
const actionHandler = (action) => {
|
||||||
switch (action) {
|
switch (action) {
|
||||||
@@ -41,6 +23,13 @@ const EventListMenu = ({ eventsHandler }) => {
|
|||||||
case 'block':
|
case 'block':
|
||||||
eventsHandler('add', { type: action, order: 0 });
|
eventsHandler('add', { type: action, order: 0 });
|
||||||
break;
|
break;
|
||||||
|
case 'togglelock':
|
||||||
|
let newSet = 'locked';
|
||||||
|
if (cursorSettings === 'locked') {
|
||||||
|
newSet = 'unlocked';
|
||||||
|
}
|
||||||
|
SetOption({ cursor: newSet });
|
||||||
|
break;
|
||||||
default:
|
default:
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@@ -48,36 +37,18 @@ const EventListMenu = ({ eventsHandler }) => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={style.headerButtons}>
|
<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
|
<CollapseBtn
|
||||||
size='sm'
|
size='sm'
|
||||||
clickhandler={() => eventsHandler('collapseall')}
|
clickhandler={() => eventsHandler('collapseall')}
|
||||||
/>
|
/>
|
||||||
<ExpandBtn size='sm' clickhandler={() => eventsHandler('expandall')} />
|
<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' />
|
<MenuActionButtons actionHandler={actionHandler} size='sm' />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -90,6 +90,10 @@ class EventTimer extends Timer {
|
|||||||
broadcastState() {
|
broadcastState() {
|
||||||
this.io.emit('timer', this.getObject());
|
this.io.emit('timer', this.getObject());
|
||||||
this.io.emit('playstate', this.state);
|
this.io.emit('playstate', this.state);
|
||||||
|
this.io.emit('selected', {
|
||||||
|
id: this.selectedEventId,
|
||||||
|
index: this.selectedEventIndex,
|
||||||
|
});
|
||||||
this.io.emit('selected-id', this.selectedEventId);
|
this.io.emit('selected-id', this.selectedEventId);
|
||||||
this.io.emit('next-id', this.nextEventId);
|
this.io.emit('next-id', this.nextEventId);
|
||||||
this.io.emit('publicselected-id', this.selectedPublicEventId);
|
this.io.emit('publicselected-id', this.selectedPublicEventId);
|
||||||
@@ -259,6 +263,13 @@ class EventTimer extends Timer {
|
|||||||
|
|
||||||
/*******************************************/
|
/*******************************************/
|
||||||
// selection data
|
// selection data
|
||||||
|
socket.on('get-selected', () => {
|
||||||
|
socket.emit('selected', {
|
||||||
|
id: this.selectedEventId,
|
||||||
|
index: this.selectedEventIndex,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
socket.on('get-selected-id', () => {
|
socket.on('get-selected-id', () => {
|
||||||
socket.emit('selected-id', this.selectedEventId);
|
socket.emit('selected-id', this.selectedEventId);
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user