mirror of
https://github.com/cpvalente/ontime.git
synced 2026-08-22 07:29:08 +00:00
@@ -0,0 +1,211 @@
|
||||
@use '../../../theme/main' as *;
|
||||
@use '../blockMixins' as *;
|
||||
|
||||
.eventBlock {
|
||||
// layout
|
||||
display: grid;
|
||||
grid-template-areas:
|
||||
"binder pb-actions times actions"
|
||||
"binder pb-actions title title"
|
||||
"binder pb-actions notes status"
|
||||
"binder progb progb progb";
|
||||
grid-template-columns: $binder-width auto 1fr auto;
|
||||
grid-template-rows: 36px 36px 36px $element-spacing;
|
||||
align-items: center;
|
||||
margin:4px 2px;
|
||||
padding-right: $clearance;
|
||||
|
||||
// style - general
|
||||
border-radius: $block-border-radius;
|
||||
gap: 2px;
|
||||
|
||||
// style - colour
|
||||
background-color: $bg-container-l2;
|
||||
border: $border-l3;
|
||||
|
||||
// variant
|
||||
&.selected {
|
||||
background-color: $bg-container-l3;
|
||||
}
|
||||
|
||||
&.skip {
|
||||
background-color: $bg-gray-1000;
|
||||
border: 1px solid transparent;
|
||||
|
||||
.eventTimers > .delayNote,
|
||||
.eventTitle,
|
||||
.eventNote {
|
||||
color: $text-gray-disabled;
|
||||
}
|
||||
|
||||
.binder,
|
||||
.eventTimers,
|
||||
.eventStatus {
|
||||
opacity: $opacity-disabled;
|
||||
}
|
||||
}
|
||||
|
||||
.binder {
|
||||
// layout
|
||||
grid-area: binder;
|
||||
height: 100%;
|
||||
display: grid;
|
||||
place-content: center;
|
||||
position: relative;
|
||||
cursor: pointer;
|
||||
|
||||
// style - general
|
||||
border-radius: 3px 0 0 3px;
|
||||
border-right: 1px solid $bg-gray-900;
|
||||
|
||||
// style - colour
|
||||
background-color: $bg-container-l3; // override inline
|
||||
color: $text-white;
|
||||
|
||||
.drag {
|
||||
@include drag-style;
|
||||
position: absolute;
|
||||
}
|
||||
}
|
||||
|
||||
.playbackActions {
|
||||
// layout
|
||||
grid-area: pb-actions;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
margin: $element-spacing $clearance $element-spacing $element-spacing;
|
||||
// style - general
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.eventTimers {
|
||||
grid-area: times;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: $clearance;
|
||||
height: 100%;
|
||||
|
||||
.delayNote {
|
||||
font-size: 11px;
|
||||
color: $text-delay;
|
||||
}
|
||||
}
|
||||
|
||||
.eventTitle {
|
||||
grid-area: title;
|
||||
display: block;
|
||||
font-size: 18px;
|
||||
|
||||
.eventTitle__preview {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
&.noTitle {
|
||||
span {
|
||||
opacity: $opacity-disabled;
|
||||
}
|
||||
}
|
||||
|
||||
input {
|
||||
border-radius: 2px;
|
||||
border: 1px;
|
||||
}
|
||||
}
|
||||
|
||||
.eventNote {
|
||||
grid-area: notes;
|
||||
display: block;
|
||||
font-size: 14px;
|
||||
color: $notes-color;
|
||||
line-height: 15px;
|
||||
}
|
||||
|
||||
.eventActions {
|
||||
grid-area: actions;
|
||||
|
||||
display: flex;
|
||||
gap: $element-spacing;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.eventStatus {
|
||||
grid-area: status;
|
||||
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: $element-spacing;
|
||||
|
||||
.statusIcon {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
border-radius: 12px;
|
||||
padding: 4px;
|
||||
color: $text-gray-disabled;
|
||||
}
|
||||
|
||||
.statusIcon.statusNext {
|
||||
border: 1px solid transparent;
|
||||
|
||||
&.enabled {
|
||||
color: $text-white;
|
||||
background-color: $ontime-accent;
|
||||
}
|
||||
}
|
||||
|
||||
.statusIcon.statusDelay {
|
||||
&.enabled {
|
||||
color: $text-white;
|
||||
background-color: $ontime-delay;
|
||||
}
|
||||
}
|
||||
|
||||
.statusIcon.statusPublic {
|
||||
&.enabled {
|
||||
color: $text-white;
|
||||
background-color: $ontime-roll;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.eventOptions {
|
||||
margin: $element-spacing 16px $element-spacing 0;
|
||||
}
|
||||
}
|
||||
|
||||
.progressBg {
|
||||
grid-area: progb;
|
||||
border-radius: 1px;
|
||||
background-color: $bg-container-l1;
|
||||
margin-bottom: 4px;
|
||||
margin-left: 4px;
|
||||
|
||||
// layout
|
||||
height: 100%;
|
||||
|
||||
.progressBar {
|
||||
// layout
|
||||
height: 100%;
|
||||
width: 0;
|
||||
border-radius: 1px 0 0 1px;
|
||||
|
||||
// animations
|
||||
transition: 1s linear;
|
||||
transition-property: width;
|
||||
|
||||
&.start {
|
||||
background-color: $ontime-accent;
|
||||
}
|
||||
|
||||
&.pause {
|
||||
background-color: $ontime-paused;
|
||||
}
|
||||
|
||||
&.roll {
|
||||
background-color: $ontime-roll;
|
||||
}
|
||||
|
||||
&.overtime {
|
||||
background-color: red;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,250 @@
|
||||
import { useCallback, useState } from 'react';
|
||||
import { Draggable } from 'react-beautiful-dnd';
|
||||
import { Editable, EditableInput, EditablePreview, Tooltip } from '@chakra-ui/react';
|
||||
import { FiUsers } from '@react-icons/all-files/fi/FiUsers';
|
||||
import { IoOptions } from '@react-icons/all-files/io5/IoOptions';
|
||||
import { IoPlay } from '@react-icons/all-files/io5/IoPlay';
|
||||
import { IoPlayBackOutline } from '@react-icons/all-files/io5/IoPlayBackOutline';
|
||||
import { IoPlayOutline } from '@react-icons/all-files/io5/IoPlayOutline';
|
||||
import { IoReload } from '@react-icons/all-files/io5/IoReload';
|
||||
import { IoRemoveCircle } from '@react-icons/all-files/io5/IoRemoveCircle';
|
||||
import { IoRemoveCircleOutline } from '@react-icons/all-files/io5/IoRemoveCircleOutline';
|
||||
import { IoReorderTwo } from '@react-icons/all-files/io5/IoReorderTwo';
|
||||
import { IoReturnDownForward } from '@react-icons/all-files/io5/IoReturnDownForward';
|
||||
import { IoTimerOutline } from '@react-icons/all-files/io5/IoTimerOutline';
|
||||
import { editorEventId } from 'common/atoms/LocalEventSettings';
|
||||
import TooltipActionBtn from 'common/components/buttons/TooltipActionBtn';
|
||||
import { getAccessibleColour } from 'common/utils/styleUtils';
|
||||
import { useAtom } from 'jotai';
|
||||
|
||||
import { useEventProvider } from '../../../common/hooks/useSocketProvider';
|
||||
import { Playstate } from '../../../common/models/OntimeTypes';
|
||||
import { tooltipDelayMid } from '../../../ontimeConfig';
|
||||
import { EventItemActions } from '../RundownEntry';
|
||||
|
||||
import EventBlockActionMenu from './composite/EventBlockActionMenu';
|
||||
import EventBlockTimers from './composite/EventBlockTimers';
|
||||
|
||||
import style from './EventBlock.module.scss';
|
||||
|
||||
const blockBtnStyle = {
|
||||
size: 'sm',
|
||||
colorScheme: 'white',
|
||||
variant: 'ghost',
|
||||
fontSize: '20px',
|
||||
};
|
||||
|
||||
const tooltipProps = {
|
||||
openDelay: tooltipDelayMid,
|
||||
};
|
||||
|
||||
interface EventBlockProps {
|
||||
timeStart: number;
|
||||
timeEnd: number;
|
||||
duration: number;
|
||||
index: number;
|
||||
eventIndex: number;
|
||||
eventId: string;
|
||||
isPublic: boolean;
|
||||
title: string;
|
||||
note: string;
|
||||
delay: number;
|
||||
previousEnd: number;
|
||||
colour: string;
|
||||
next: boolean;
|
||||
skip: boolean;
|
||||
selected: boolean;
|
||||
playback?: Playstate;
|
||||
actionHandler: (action: EventItemActions, payload: any) => void;
|
||||
}
|
||||
|
||||
export default function EventBlock(props: EventBlockProps) {
|
||||
const {
|
||||
timeStart,
|
||||
timeEnd,
|
||||
duration,
|
||||
index,
|
||||
eventIndex,
|
||||
eventId,
|
||||
isPublic = true,
|
||||
title,
|
||||
note,
|
||||
delay,
|
||||
previousEnd,
|
||||
colour,
|
||||
next,
|
||||
skip = false,
|
||||
selected,
|
||||
playback,
|
||||
actionHandler,
|
||||
} = props;
|
||||
|
||||
const [openId, setOpenId] = useAtom(editorEventId);
|
||||
const { setPlayback } = useEventProvider(eventId);
|
||||
const [blockTitle, setBlockTitle] = useState<string>(title || '');
|
||||
|
||||
const binderColours = colour && getAccessibleColour(colour);
|
||||
const hasDelay = delay !== 0 && delay !== null;
|
||||
|
||||
const handleTitle = useCallback(
|
||||
(text: string) => {
|
||||
if (text === title) {
|
||||
return;
|
||||
}
|
||||
|
||||
const cleanVal = text.trim();
|
||||
setBlockTitle(cleanVal);
|
||||
|
||||
// Todo: no need for action handler
|
||||
actionHandler('update', { field: 'title', value: cleanVal });
|
||||
},
|
||||
[actionHandler, title],
|
||||
);
|
||||
|
||||
// Todo: data should come from socket
|
||||
const progress = `${Math.random() * 100}%`;
|
||||
const eventIsPlaying = selected && playback === 'start';
|
||||
const playBtnStyles = { _hover: {} };
|
||||
if (!skip && eventIsPlaying) {
|
||||
playBtnStyles._hover = { bg: '#c05621' };
|
||||
} else if (!skip && !eventIsPlaying) {
|
||||
playBtnStyles._hover = { };
|
||||
}
|
||||
|
||||
return (
|
||||
<Draggable key={eventId} draggableId={eventId} index={index}>
|
||||
{(provided) => (
|
||||
<div
|
||||
className={`${style.eventBlock} ${skip ? style.skip : ''} ${
|
||||
selected ? style.selected : ''
|
||||
}`}
|
||||
{...provided.draggableProps}
|
||||
ref={provided.innerRef}
|
||||
>
|
||||
<div
|
||||
className={style.binder}
|
||||
style={{ ...binderColours }}
|
||||
tabIndex={-1}
|
||||
onClick={() => actionHandler('set-cursor', index)}
|
||||
>
|
||||
<span className={style.drag} {...provided.dragHandleProps}>
|
||||
<IoReorderTwo />
|
||||
</span>
|
||||
{eventIndex}
|
||||
</div>
|
||||
<div className={selected ? style.progressBg : ''}>
|
||||
<div
|
||||
className={`${style.progressBar} ${playback ? style[playback] : ''}`}
|
||||
style={{ width: progress }}
|
||||
/>
|
||||
</div>
|
||||
<div className={style.playbackActions}>
|
||||
<TooltipActionBtn
|
||||
aria-label='Skip event'
|
||||
tooltip='Skip event'
|
||||
openDelay={tooltipDelayMid}
|
||||
icon={skip ? <IoRemoveCircle /> : <IoRemoveCircleOutline />}
|
||||
{...blockBtnStyle}
|
||||
variant={skip ? 'solid' : 'ghost'}
|
||||
clickHandler={() => actionHandler('update', { field: 'skip', value: !skip })}
|
||||
tabIndex={-1}
|
||||
disabled={selected}
|
||||
/>
|
||||
<TooltipActionBtn
|
||||
aria-label='Load event'
|
||||
tooltip='Load event'
|
||||
openDelay={tooltipDelayMid}
|
||||
icon={selected ? <IoPlayBackOutline /> : <IoReload />}
|
||||
disabled={skip}
|
||||
{...blockBtnStyle}
|
||||
clickHandler={() => setPlayback.loadEvent()}
|
||||
tabIndex={-1}
|
||||
/>
|
||||
<TooltipActionBtn
|
||||
aria-label='Start event'
|
||||
tooltip='Start event'
|
||||
openDelay={tooltipDelayMid}
|
||||
icon={eventIsPlaying ? <IoPlay /> : <IoPlayOutline />}
|
||||
disabled={skip}
|
||||
{...blockBtnStyle}
|
||||
variant={eventIsPlaying ? 'solid' : 'ghost'}
|
||||
clickHandler={() => setPlayback.startEvent()}
|
||||
backgroundColor={eventIsPlaying ? '#58A151' : undefined}
|
||||
tabIndex={-1}
|
||||
/>
|
||||
</div>
|
||||
<EventBlockTimers
|
||||
timeStart={timeStart}
|
||||
timeEnd={timeEnd}
|
||||
duration={duration}
|
||||
delay={delay}
|
||||
actionHandler={actionHandler}
|
||||
previousEnd={previousEnd}
|
||||
/>
|
||||
<Editable
|
||||
value={blockTitle}
|
||||
className={`${style.eventTitle} ${!title || title === '' ? style.noTitle : ''}`}
|
||||
placeholder='Event title'
|
||||
onChange={(value) => setBlockTitle(value)}
|
||||
onSubmit={(value) => handleTitle(value)}
|
||||
>
|
||||
<EditablePreview className={style.eventTitle__preview} />
|
||||
<EditableInput />
|
||||
</Editable>
|
||||
<span className={style.eventNote}>{note}</span>
|
||||
<div className={style.eventActions}>
|
||||
<TooltipActionBtn
|
||||
{...blockBtnStyle}
|
||||
size='sm'
|
||||
icon={<IoOptions />}
|
||||
clickHandler={() => setOpenId((prev) => prev === eventId ? null : eventId)}
|
||||
tooltip='Event options'
|
||||
aria-label='Event options'
|
||||
tabIndex={-1}
|
||||
backgroundColor={openId === eventId ? '#ebedf0' : 'transparent'}
|
||||
color={openId === eventId ? '#333' : '#ebedf0'}
|
||||
_hover={{ bg: '#ebedf0', color: '#333' }}
|
||||
/>
|
||||
<EventBlockActionMenu
|
||||
showAdd
|
||||
showDelay
|
||||
showBlock
|
||||
showClone
|
||||
actionHandler={actionHandler}
|
||||
/>
|
||||
</div>
|
||||
<div className={style.eventStatus}>
|
||||
<Tooltip label='Next event' isDisabled={!next} {...tooltipProps}>
|
||||
<span
|
||||
className={`${style.statusIcon} ${style.statusNext} ${next ? style.enabled : ''}`}
|
||||
>
|
||||
<IoReturnDownForward />
|
||||
</span>
|
||||
</Tooltip>
|
||||
<Tooltip label='Event has delay' isDisabled={!hasDelay} {...tooltipProps}>
|
||||
<span
|
||||
className={`${style.statusIcon} ${style.statusDelay} ${
|
||||
hasDelay ? style.enabled : ''
|
||||
}`}
|
||||
>
|
||||
<IoTimerOutline />
|
||||
</span>
|
||||
</Tooltip>
|
||||
<Tooltip
|
||||
label={`${isPublic ? 'Event is public' : 'Event is private'}`}
|
||||
{...tooltipProps}
|
||||
>
|
||||
<span
|
||||
className={`${style.statusIcon} ${style.statusPublic} ${
|
||||
isPublic ? style.enabled : ''
|
||||
}`}
|
||||
>
|
||||
<FiUsers />
|
||||
</span>
|
||||
</Tooltip>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</Draggable>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
import {
|
||||
Divider,
|
||||
IconButton,
|
||||
Menu,
|
||||
MenuButton,
|
||||
MenuItem,
|
||||
MenuList,
|
||||
Tooltip,
|
||||
} from '@chakra-ui/react';
|
||||
import { FiMinusCircle } from '@react-icons/all-files/fi/FiMinusCircle';
|
||||
import { FiPlus } from '@react-icons/all-files/fi/FiPlus';
|
||||
import { FiTrash2 } from '@react-icons/all-files/fi/FiTrash2';
|
||||
import { IoDuplicateOutline } from '@react-icons/all-files/io5/IoDuplicateOutline';
|
||||
import { IoTimerOutline } from '@react-icons/all-files/io5/IoTimerOutline';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import { tooltipDelayMid } from '../../../../ontimeConfig';
|
||||
|
||||
|
||||
export default function EventBlockActionMenu(props) {
|
||||
const { showAdd, showDelay, showBlock, showClone, actionHandler } = props;
|
||||
|
||||
const menuStyle = {
|
||||
color: '#000000',
|
||||
backgroundColor: 'rgba(255,255,255,1)',
|
||||
};
|
||||
|
||||
const blockBtnStyle = {
|
||||
size: 'sm',
|
||||
variant: 'outline',
|
||||
colorScheme: 'whiteAlpha',
|
||||
};
|
||||
|
||||
return (
|
||||
<Menu isLazy lazyBehavior='unmount'>
|
||||
<Tooltip label='Add ...' delay={tooltipDelayMid}>
|
||||
<MenuButton as={IconButton} aria-label='Options' icon={<FiPlus />}
|
||||
tabIndex={-1} {...blockBtnStyle} />
|
||||
</Tooltip>
|
||||
<MenuList style={menuStyle}>
|
||||
<MenuItem icon={<FiPlus />} onClick={() => actionHandler('event')} isDisabled={!showAdd}>
|
||||
Add Event after
|
||||
</MenuItem>
|
||||
<MenuItem
|
||||
icon={<IoTimerOutline />}
|
||||
onClick={() => actionHandler('delay')}
|
||||
isDisabled={!showDelay}
|
||||
>
|
||||
Add Delay after
|
||||
</MenuItem>
|
||||
<MenuItem
|
||||
icon={<FiMinusCircle />}
|
||||
onClick={() => actionHandler('block')}
|
||||
isDisabled={!showBlock}
|
||||
>
|
||||
Add Block after
|
||||
</MenuItem>
|
||||
{showClone && (
|
||||
<MenuItem
|
||||
icon={<IoDuplicateOutline />}
|
||||
onClick={() => actionHandler('clone')}
|
||||
isDisabled={!showBlock}
|
||||
>
|
||||
Clone event
|
||||
</MenuItem>
|
||||
)}
|
||||
<Divider />
|
||||
<MenuItem
|
||||
icon={<FiTrash2 />}
|
||||
onClick={() => actionHandler('delete')}
|
||||
isDisabled={!showBlock}
|
||||
color='red.500'
|
||||
>
|
||||
Delete event
|
||||
</MenuItem>
|
||||
</MenuList>
|
||||
</Menu>
|
||||
);
|
||||
}
|
||||
|
||||
EventBlockActionMenu.propTypes = {
|
||||
showAdd: PropTypes.bool,
|
||||
showDelay: PropTypes.bool,
|
||||
showBlock: PropTypes.bool,
|
||||
showClone: PropTypes.bool,
|
||||
actionHandler: PropTypes.func,
|
||||
};
|
||||
@@ -0,0 +1,89 @@
|
||||
import { useCallback, useContext } from 'react';
|
||||
import TimeInput from 'common/components/input/TimeInput';
|
||||
import { LoggingContext } from 'common/context/LoggingContext';
|
||||
import { millisToMinutes } from 'common/utils/dateConfig';
|
||||
import { stringFromMillis } from 'common/utils/time';
|
||||
import { validateEntry } from 'common/utils/timesManager';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import style from '../EventBlock.module.scss';
|
||||
|
||||
export default function EventBlockTimers(props) {
|
||||
const { timeStart, timeEnd, duration, delay, actionHandler, previousEnd } = props;
|
||||
const { emitWarning } = useContext(LoggingContext);
|
||||
|
||||
const delayTime = `${delay >= 0 ? '+' : '-'} ${millisToMinutes(Math.abs(delay))}`;
|
||||
const newTime = stringFromMillis(timeStart + delay);
|
||||
|
||||
/**
|
||||
* @description Validates a time input against its pair
|
||||
* @param {string} entry - field to validate: timeStart, timeEnd, durationOverride
|
||||
* @param {number} val - field value
|
||||
* @return {boolean}
|
||||
*/
|
||||
const handleValidation = useCallback(
|
||||
(field, value) => {
|
||||
const valid = validateEntry(field, value, timeStart, timeEnd);
|
||||
if (!valid.value) {
|
||||
emitWarning(`Time Input Warning: ${valid.catch}`);
|
||||
}
|
||||
return valid.value;
|
||||
},
|
||||
[emitWarning, timeEnd, timeStart]
|
||||
);
|
||||
|
||||
const handleSubmit = useCallback(
|
||||
(field, value) => {
|
||||
actionHandler('update', { field, value });
|
||||
},
|
||||
[actionHandler]
|
||||
);
|
||||
|
||||
return (
|
||||
<div className={style.eventTimers}>
|
||||
<TimeInput
|
||||
name='timeStart'
|
||||
submitHandler={handleSubmit}
|
||||
validationHandler={handleValidation}
|
||||
time={timeStart}
|
||||
delay={delay}
|
||||
placeholder='Start'
|
||||
previousEnd={previousEnd}
|
||||
/>
|
||||
<TimeInput
|
||||
name='timeEnd'
|
||||
submitHandler={handleSubmit}
|
||||
validationHandler={handleValidation}
|
||||
time={timeEnd}
|
||||
delay={delay}
|
||||
placeholder='End'
|
||||
previousEnd={previousEnd}
|
||||
/>
|
||||
<TimeInput
|
||||
name='duration'
|
||||
submitHandler={handleSubmit}
|
||||
validationHandler={handleValidation}
|
||||
time={duration}
|
||||
delay={delay}
|
||||
placeholder='Duration'
|
||||
previousEnd={previousEnd}
|
||||
/>
|
||||
{delay !== 0 && delay !== null && (
|
||||
<div className={style.delayNote}>
|
||||
{`${delayTime} minutes`}
|
||||
<br />
|
||||
{`New start: ${newTime}`}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
EventBlockTimers.propTypes = {
|
||||
timeStart: PropTypes.number,
|
||||
timeEnd: PropTypes.number,
|
||||
duration: PropTypes.number,
|
||||
delay: PropTypes.number,
|
||||
actionHandler: PropTypes.func,
|
||||
previousEnd: PropTypes.number,
|
||||
};
|
||||
Reference in New Issue
Block a user