mirror of
https://github.com/cpvalente/ontime.git
synced 2026-08-30 03:19:11 +00:00
V2 event block (#261)
* feat: implement progress bar on running event * refactor: use event action hook * refactor: align backend data
This commit is contained in:
@@ -131,8 +131,9 @@ export const setEventPlayback = {
|
|||||||
|
|
||||||
const emptyTimer = {
|
const emptyTimer = {
|
||||||
clock: 0,
|
clock: 0,
|
||||||
running: 0,
|
current: 0,
|
||||||
isNegative: false,
|
secondaryTimer: null,
|
||||||
|
duration: null,
|
||||||
startedAt: null,
|
startedAt: null,
|
||||||
expectedFinish: null,
|
expectedFinish: null,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,18 +1,74 @@
|
|||||||
@use '../../../theme/main' as *;
|
@use '../../../theme/main' as *;
|
||||||
@use '../blockMixins' as *;
|
@use '../blockMixins' as *;
|
||||||
|
|
||||||
|
.statusElements {
|
||||||
|
grid-area: estatus;
|
||||||
|
display: grid;
|
||||||
|
grid-template-areas:
|
||||||
|
"notes status"
|
||||||
|
"progb progb";
|
||||||
|
gap: 2px;
|
||||||
|
grid-template-rows: auto 4px;
|
||||||
|
|
||||||
|
.eventNote {
|
||||||
|
grid-area: notes;
|
||||||
|
display: block;
|
||||||
|
font-size: 15px;
|
||||||
|
color: $notes-color;
|
||||||
|
line-height: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.eventBlock {
|
.eventBlock {
|
||||||
// layout
|
// layout
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-areas:
|
grid-template-areas:
|
||||||
"binder pb-actions times actions"
|
"binder pb-actions times actions"
|
||||||
"binder pb-actions title title"
|
"binder pb-actions title title"
|
||||||
"binder pb-actions notes status"
|
"binder pb-actions estatus estatus";
|
||||||
"binder progb progb progb";
|
|
||||||
grid-template-columns: $binder-width auto 1fr auto;
|
grid-template-columns: $binder-width auto 1fr auto;
|
||||||
grid-template-rows: 36px 36px 36px $element-spacing;
|
grid-template-rows: 36px 36px 36px;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
margin:4px 2px;
|
margin: 4px 2px;
|
||||||
padding-right: $clearance;
|
padding-right: $clearance;
|
||||||
|
|
||||||
// style - general
|
// style - general
|
||||||
@@ -23,7 +79,7 @@
|
|||||||
background-color: $bg-container-l2;
|
background-color: $bg-container-l2;
|
||||||
border: $border-l3;
|
border: $border-l3;
|
||||||
|
|
||||||
// variant
|
// variant
|
||||||
&.selected {
|
&.selected {
|
||||||
background-color: $bg-container-l3;
|
background-color: $bg-container-l3;
|
||||||
}
|
}
|
||||||
@@ -112,14 +168,6 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.eventNote {
|
|
||||||
grid-area: notes;
|
|
||||||
display: block;
|
|
||||||
font-size: 14px;
|
|
||||||
color: $notes-color;
|
|
||||||
line-height: 15px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.eventActions {
|
.eventActions {
|
||||||
grid-area: actions;
|
grid-area: actions;
|
||||||
|
|
||||||
@@ -128,44 +176,6 @@
|
|||||||
justify-content: flex-end;
|
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 {
|
.eventOptions {
|
||||||
margin: $element-spacing 16px $element-spacing 0;
|
margin: $element-spacing 16px $element-spacing 0;
|
||||||
@@ -176,36 +186,12 @@
|
|||||||
grid-area: progb;
|
grid-area: progb;
|
||||||
border-radius: 1px;
|
border-radius: 1px;
|
||||||
background-color: $bg-container-l1;
|
background-color: $bg-container-l1;
|
||||||
margin-bottom: 4px;
|
opacity: 1;
|
||||||
margin-left: 4px;
|
|
||||||
|
|
||||||
// layout
|
// layout
|
||||||
height: 100%;
|
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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.progressBg.hidden {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
@@ -17,12 +17,14 @@ import TooltipActionBtn from 'common/components/buttons/TooltipActionBtn';
|
|||||||
import { getAccessibleColour } from 'common/utils/styleUtils';
|
import { getAccessibleColour } from 'common/utils/styleUtils';
|
||||||
import { useAtom } from 'jotai';
|
import { useAtom } from 'jotai';
|
||||||
|
|
||||||
|
import { useEventAction } from '../../../common/hooks/useEventAction';
|
||||||
import { setEventPlayback } from '../../../common/hooks/useSocket';
|
import { setEventPlayback } from '../../../common/hooks/useSocket';
|
||||||
import { Playstate } from '../../../common/models/OntimeTypes';
|
import { Playstate } from '../../../common/models/OntimeTypes';
|
||||||
import { tooltipDelayMid } from '../../../ontimeConfig';
|
import { tooltipDelayMid } from '../../../ontimeConfig';
|
||||||
import { EventItemActions } from '../RundownEntry';
|
import { EventItemActions } from '../RundownEntry';
|
||||||
|
|
||||||
import EventBlockActionMenu from './composite/EventBlockActionMenu';
|
import EventBlockActionMenu from './composite/EventBlockActionMenu';
|
||||||
|
import EventBlockProgressBar from './composite/EventBlockProgressBar';
|
||||||
import EventBlockTimers from './composite/EventBlockTimers';
|
import EventBlockTimers from './composite/EventBlockTimers';
|
||||||
|
|
||||||
import style from './EventBlock.module.scss';
|
import style from './EventBlock.module.scss';
|
||||||
@@ -80,6 +82,7 @@ export default function EventBlock(props: EventBlockProps) {
|
|||||||
} = props;
|
} = props;
|
||||||
|
|
||||||
const [openId, setOpenId] = useAtom(editorEventId);
|
const [openId, setOpenId] = useAtom(editorEventId);
|
||||||
|
const { updateEvent } = useEventAction();
|
||||||
const [blockTitle, setBlockTitle] = useState<string>(title || '');
|
const [blockTitle, setBlockTitle] = useState<string>(title || '');
|
||||||
|
|
||||||
const binderColours = colour && getAccessibleColour(colour);
|
const binderColours = colour && getAccessibleColour(colour);
|
||||||
@@ -99,14 +102,11 @@ export default function EventBlock(props: EventBlockProps) {
|
|||||||
const cleanVal = text.trim();
|
const cleanVal = text.trim();
|
||||||
setBlockTitle(cleanVal);
|
setBlockTitle(cleanVal);
|
||||||
|
|
||||||
// Todo: no need for action handler
|
updateEvent({ id: eventId, title: cleanVal });
|
||||||
actionHandler('update', { field: 'title', value: cleanVal });
|
|
||||||
},
|
},
|
||||||
[actionHandler, title],
|
[updateEvent, title],
|
||||||
);
|
);
|
||||||
|
|
||||||
// Todo: data should come from socket
|
|
||||||
const progress = `${Math.random() * 100}%`;
|
|
||||||
const eventIsPlaying = selected && playback === 'start';
|
const eventIsPlaying = selected && playback === 'start';
|
||||||
const playBtnStyles = { _hover: {} };
|
const playBtnStyles = { _hover: {} };
|
||||||
if (!skip && eventIsPlaying) {
|
if (!skip && eventIsPlaying) {
|
||||||
@@ -136,12 +136,6 @@ export default function EventBlock(props: EventBlockProps) {
|
|||||||
</span>
|
</span>
|
||||||
{eventIndex}
|
{eventIndex}
|
||||||
</div>
|
</div>
|
||||||
<div className={selected ? style.progressBg : ''}>
|
|
||||||
<div
|
|
||||||
className={`${style.progressBar} ${playback ? style[playback] : ''}`}
|
|
||||||
style={{ width: progress }}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className={style.playbackActions}>
|
<div className={style.playbackActions}>
|
||||||
<TooltipActionBtn
|
<TooltipActionBtn
|
||||||
aria-label='Skip event'
|
aria-label='Skip event'
|
||||||
@@ -195,7 +189,42 @@ export default function EventBlock(props: EventBlockProps) {
|
|||||||
<EditablePreview className={style.eventTitle__preview} />
|
<EditablePreview className={style.eventTitle__preview} />
|
||||||
<EditableInput />
|
<EditableInput />
|
||||||
</Editable>
|
</Editable>
|
||||||
<span className={style.eventNote}>{note}</span>
|
<div className={style.statusElements}>
|
||||||
|
<span className={style.eventNote}>{note}</span>
|
||||||
|
<div className={selected ? style.progressBg : `${style.progressBg} ${style.hidden}`}>
|
||||||
|
<EventBlockProgressBar playback={playback} />
|
||||||
|
</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>
|
||||||
<div className={style.eventActions}>
|
<div className={style.eventActions}>
|
||||||
<TooltipActionBtn
|
<TooltipActionBtn
|
||||||
{...blockBtnStyle}
|
{...blockBtnStyle}
|
||||||
@@ -217,36 +246,6 @@ export default function EventBlock(props: EventBlockProps) {
|
|||||||
actionHandler={actionHandler}
|
actionHandler={actionHandler}
|
||||||
/>
|
/>
|
||||||
</div>
|
</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>
|
</div>
|
||||||
)}
|
)}
|
||||||
</Draggable>
|
</Draggable>
|
||||||
|
|||||||
@@ -0,0 +1,29 @@
|
|||||||
|
@use '../../../../theme/main' as *;
|
||||||
|
|
||||||
|
.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: $ontime-pink-variant;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
import { useTimer } from '../../../../common/hooks/useSocket';
|
||||||
|
import { Playstate } from '../../../../common/models/OntimeTypes';
|
||||||
|
import { clamp } from '../../../../common/utils/math';
|
||||||
|
|
||||||
|
import style from './EventBlockProgressBar.module.scss';
|
||||||
|
|
||||||
|
interface EventBlockProgressBarProps {
|
||||||
|
playback?: Playstate;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function EventBlockProgressBar(props: EventBlockProgressBarProps) {
|
||||||
|
const { playback } = props;
|
||||||
|
const { data: timer } = useTimer();
|
||||||
|
const now = Math.floor(Math.max((timer?.current ?? 1) / 1000, 0));
|
||||||
|
const complete = (timer?.duration ?? 1) / 1000;
|
||||||
|
const elapsed = clamp(100 - (now * 100) / complete, 0, 100);
|
||||||
|
const progress = `${elapsed}%`;
|
||||||
|
|
||||||
|
if (timer?.current != null && timer?.current < 0) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={`${style.progressBar} ${style.overtime}`}
|
||||||
|
style={{ width: '100%' }}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={`${style.progressBar} ${playback ? style[playback] : ''}`}
|
||||||
|
style={{ width: progress }}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,3 +1,6 @@
|
|||||||
|
$transition-time-action: 0.1s;
|
||||||
|
$transition-time-feedback: 0.3s;
|
||||||
|
|
||||||
//////////////////////////////////// general app colours
|
//////////////////////////////////// general app colours
|
||||||
|
|
||||||
$bg-black-gradient: #202020;
|
$bg-black-gradient: #202020;
|
||||||
@@ -42,7 +45,7 @@ $ontime-red: #E4281E;
|
|||||||
// outdent in level2 - $bg-gray-950
|
// outdent in level2 - $bg-gray-950
|
||||||
//////////////////////////////////// editor
|
//////////////////////////////////// editor
|
||||||
|
|
||||||
$notes-color: #9fd8ff;
|
$notes-color: #f6f6f6;
|
||||||
|
|
||||||
$text-white: #fffffa;
|
$text-white: #fffffa;
|
||||||
$text-gray-disabled: #505050;
|
$text-gray-disabled: #505050;
|
||||||
|
|||||||
@@ -119,8 +119,8 @@ export class EventTimer extends Timer {
|
|||||||
current: this.current,
|
current: this.current,
|
||||||
secondaryTimer: this.secondaryTimer,
|
secondaryTimer: this.secondaryTimer,
|
||||||
duration: this.duration,
|
duration: this.duration,
|
||||||
expectedFinish: this._getExpectedFinish(),
|
|
||||||
startedAt: this._startedAt,
|
startedAt: this._startedAt,
|
||||||
|
expectedFinish: this._getExpectedFinish(),
|
||||||
};
|
};
|
||||||
this.socket.send('ontime-timer', featureData);
|
this.socket.send('ontime-timer', featureData);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { Server } from 'node-osc';
|
import { Server } from 'node-osc';
|
||||||
import { PlaybackService } from '../services/playbackService';
|
import { PlaybackService } from '../services/playbackService.js';
|
||||||
import { messageManager } from '../classes/message-manager/MessageManager.js';
|
import { messageManager } from '../classes/message-manager/MessageManager.js';
|
||||||
import { socketProvider } from '../classes/socket/SocketController.js';
|
import { socketProvider } from '../classes/socket/SocketController.js';
|
||||||
import { ADDRESS_MESSAGE_CONTROL } from '../classes/socket/socketConfig.js';
|
import { ADDRESS_MESSAGE_CONTROL } from '../classes/socket/socketConfig.js';
|
||||||
@@ -22,7 +22,7 @@ export const initiateOSC = (config) => {
|
|||||||
|
|
||||||
oscServer.on('error', console.error);
|
oscServer.on('error', console.error);
|
||||||
|
|
||||||
oscServer.on('message', function (msg) {
|
oscServer.on('message', function(msg) {
|
||||||
// message should look like /ontime/{path} {args} where
|
// message should look like /ontime/{path} {args} where
|
||||||
// ontime: fixed message for app
|
// ontime: fixed message for app
|
||||||
// path: command to be called
|
// path: command to be called
|
||||||
@@ -125,7 +125,7 @@ export const initiateOSC = (config) => {
|
|||||||
if (isNaN(eventIndex) || eventIndex <= 0) {
|
if (isNaN(eventIndex) || eventIndex <= 0) {
|
||||||
socketProvider.error(
|
socketProvider.error(
|
||||||
'RX',
|
'RX',
|
||||||
`OSC IN: event index not recognised or out of range ${eventIndex}`,
|
`OSC IN: event index not recognised or out of range ${eventIndex}`
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
PlaybackService.loadByIndex(eventIndex - 1);
|
PlaybackService.loadByIndex(eventIndex - 1);
|
||||||
|
|||||||
Reference in New Issue
Block a user