mirror of
https://github.com/cpvalente/ontime.git
synced 2026-08-16 12:53:32 +00:00
feat: operator view
This commit is contained in:
@@ -18,6 +18,10 @@
|
||||
gap: 2px;
|
||||
}
|
||||
|
||||
.spacer {
|
||||
min-height: 80vh;
|
||||
}
|
||||
|
||||
@mixin block() {
|
||||
width: 100%;
|
||||
padding: 0.25rem 0.5rem;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { UIEvent, useEffect, useState } from 'react';
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
import { useSearchParams } from 'react-router-dom';
|
||||
import { SupportedEvent, UserFields } from 'ontime-types';
|
||||
import { getLastEvent } from 'ontime-utils';
|
||||
@@ -7,6 +7,7 @@ import NavigationMenu from '../../common/components/navigation-menu/NavigationMe
|
||||
import Empty from '../../common/components/state/Empty';
|
||||
import { getOperatorOptions } from '../../common/components/view-params-editor/constants';
|
||||
import ViewParamsEditor from '../../common/components/view-params-editor/ViewParamsEditor';
|
||||
import useFollowComponent from '../../common/hooks/useFollowComponent';
|
||||
import { useOperator } from '../../common/hooks/useSocket';
|
||||
import useRundown from '../../common/hooks-query/useRundown';
|
||||
import useUserFields from '../../common/hooks-query/useUserFields';
|
||||
@@ -19,23 +20,53 @@ import StatusBar from './status-bar/StatusBar';
|
||||
|
||||
import style from './Operator.module.scss';
|
||||
|
||||
const selectedOffset = 50;
|
||||
|
||||
export default function Operator() {
|
||||
const { data, status } = useRundown();
|
||||
const { data: userFields, status: userFieldsStatus } = useUserFields();
|
||||
const featureData = useOperator();
|
||||
const [showChild, setShowChild] = useState(false);
|
||||
const [searchParams] = useSearchParams();
|
||||
|
||||
const [lockAutoScroll, setLockAutoScroll] = useState(false);
|
||||
const selectedRef = useRef<HTMLDivElement | null>(null);
|
||||
const scrollRef = useRef<HTMLDivElement | null>(null);
|
||||
const scrollToComponent = useFollowComponent({
|
||||
followRef: selectedRef,
|
||||
scrollRef: scrollRef,
|
||||
doFollow: !lockAutoScroll,
|
||||
topOffset: selectedOffset,
|
||||
});
|
||||
|
||||
// Set window title
|
||||
useEffect(() => {
|
||||
document.title = 'ontime - Operator';
|
||||
}, []);
|
||||
|
||||
const handleScroll = (event: UIEvent<HTMLElement>) => {
|
||||
const scrollThreshold = 50;
|
||||
const scrollPosition = (event.target as HTMLElement).scrollTop;
|
||||
// reset scroll if nothing is selected
|
||||
useEffect(() => {
|
||||
if (!featureData?.selectedEventId) {
|
||||
if (!lockAutoScroll) {
|
||||
scrollRef.current?.scrollTo(0, 0);
|
||||
}
|
||||
}
|
||||
}, [featureData?.selectedEventId, lockAutoScroll, scrollRef]);
|
||||
|
||||
setShowChild(scrollPosition > scrollThreshold);
|
||||
const handleOffset = () => {
|
||||
if (featureData.selectedEventId) {
|
||||
scrollToComponent();
|
||||
}
|
||||
setLockAutoScroll(false);
|
||||
};
|
||||
|
||||
const handleScroll = () => {
|
||||
if (selectedRef && scrollRef) {
|
||||
const selectedRect = selectedRef.current?.getBoundingClientRect();
|
||||
if (selectedRect) {
|
||||
const hasScrolledOutOfThreshold = selectedRect.top < 0 || selectedRect.top > selectedOffset;
|
||||
setLockAutoScroll(hasScrolledOutOfThreshold);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
if (!data || status === 'loading' || !userFields || userFieldsStatus === 'loading') {
|
||||
@@ -49,24 +80,38 @@ export default function Operator() {
|
||||
const lastEvent = getLastEvent(data);
|
||||
|
||||
const operatorOptions = getOperatorOptions(userFields);
|
||||
let isPast = Boolean(featureData.selectedEventId);
|
||||
const hidePast = isStringBoolean(searchParams.get('hidepast'));
|
||||
|
||||
return (
|
||||
<div className={style.operatorContainer}>
|
||||
<NavigationMenu />
|
||||
<ViewParamsEditor paramFields={operatorOptions} />
|
||||
|
||||
<div className={style.operatorEvents} onScroll={handleScroll}>
|
||||
<div className={style.operatorEvents} onScroll={handleScroll} ref={scrollRef}>
|
||||
{data.map((entry) => {
|
||||
if (entry.type === SupportedEvent.Event) {
|
||||
const isSelected = featureData.selectedEventId === entry.id;
|
||||
if (isSelected) {
|
||||
isPast = false;
|
||||
}
|
||||
|
||||
// hide past events (if setting) and skipped events
|
||||
if ((hidePast && isPast) || entry.skip) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<OperatorEvent
|
||||
key={entry.id}
|
||||
cue={entry.cue}
|
||||
data={entry}
|
||||
isSelected={featureData.selectedEventId === entry.id}
|
||||
isSelected={isSelected}
|
||||
subscribed={subscribe}
|
||||
subscribedAlias={subscribedAlias}
|
||||
showSeconds={showSeconds}
|
||||
isPast={isPast}
|
||||
selectedRef={isSelected ? selectedRef : undefined}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -76,8 +121,9 @@ export default function Operator() {
|
||||
}
|
||||
return null;
|
||||
})}
|
||||
<div className={style.spacer} />
|
||||
</div>
|
||||
<FollowButton isVisible={showChild} onClickHandler={() => undefined} />
|
||||
<FollowButton isVisible={lockAutoScroll} onClickHandler={handleOffset} />
|
||||
<StatusBar playback={featureData.playback} lastEvent={lastEvent} selectedEventId={featureData.selectedEventId} />
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
.event {
|
||||
@include block();
|
||||
background-color: $gray-1300;
|
||||
opacity: 1;
|
||||
|
||||
&.subscribed {
|
||||
background-color: $gray-1250;
|
||||
@@ -13,6 +14,10 @@
|
||||
&.running {
|
||||
background-color: $red-700;
|
||||
}
|
||||
|
||||
&.past {
|
||||
opacity: 0.2;
|
||||
}
|
||||
}
|
||||
|
||||
.titles {
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { RefObject } from 'react';
|
||||
import { OntimeEvent, UserFields } from 'ontime-types';
|
||||
|
||||
import DelayIndicator from '../../../common/components/delay-indicator/DelayIndicator';
|
||||
@@ -14,6 +15,8 @@ interface OperatorEventProps {
|
||||
subscribed: keyof UserFields | null;
|
||||
subscribedAlias: string;
|
||||
showSeconds: boolean;
|
||||
isPast: boolean;
|
||||
selectedRef?: RefObject<HTMLDivElement>;
|
||||
}
|
||||
|
||||
// extract this to contain re-renders
|
||||
@@ -23,7 +26,7 @@ function RollingTime() {
|
||||
}
|
||||
|
||||
export default function OperatorEvent(props: OperatorEventProps) {
|
||||
const { data, cue, isSelected, subscribed, subscribedAlias, showSeconds } = props;
|
||||
const { data, cue, isSelected, subscribed, subscribedAlias, showSeconds, isPast, selectedRef } = props;
|
||||
|
||||
const start = formatTime(data.timeStart, { showSeconds });
|
||||
const end = formatTime(data.timeEnd, { showSeconds });
|
||||
@@ -35,10 +38,11 @@ export default function OperatorEvent(props: OperatorEventProps) {
|
||||
style.event,
|
||||
isSelected ? style.running : null,
|
||||
subscribedData ? style.subscribed : null,
|
||||
isPast ? style.past : null,
|
||||
]);
|
||||
|
||||
return (
|
||||
<div className={operatorClasses}>
|
||||
<div className={operatorClasses} ref={selectedRef}>
|
||||
<div className={style.titles}>
|
||||
<span className={style.title}>
|
||||
{data.title} - {data.subtitle}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { useMemo } from 'react';
|
||||
import { OntimeEvent, Playback } from 'ontime-types';
|
||||
import { millisToString } from 'ontime-utils';
|
||||
|
||||
@@ -37,9 +38,13 @@ export default function StatusBar({
|
||||
const runningTime = millisToString(timer.current);
|
||||
const elapsedTime = millisToString(timer.elapsed);
|
||||
|
||||
const PlaybackIconComponent = useMemo(() => {
|
||||
return <PlaybackIcon state={playback} skipTooltip className={styles.playbackIcon} />;
|
||||
}, [playback]);
|
||||
|
||||
return (
|
||||
<div className={styles.statusBar}>
|
||||
<PlaybackIcon state={playback} className={styles.playbackIcon} />
|
||||
{PlaybackIconComponent}
|
||||
<div className={styles.clock}>
|
||||
<div className={styles.column}>
|
||||
<span className={styles.label}>Time now</span>
|
||||
|
||||
Reference in New Issue
Block a user