import { useCallback, useEffect, useRef, useState } from 'react'; import { useSearchParams } from 'react-router-dom'; import { isOntimeEvent, OntimeEvent, SupportedEntry } from 'ontime-types'; import { getFirstEventNormal, getLastEventNormal } from 'ontime-utils'; import EmptyPage from '../../common/components/state/EmptyPage'; import ViewParamsEditor from '../../common/components/view-params-editor/ViewParamsEditor'; import useFollowComponent from '../../common/hooks/useFollowComponent'; import { useOperator } from '../../common/hooks/useSocket'; import { useWindowTitle } from '../../common/hooks/useWindowTitle'; import useCustomFields from '../../common/hooks-query/useCustomFields'; import useProjectData from '../../common/hooks-query/useProjectData'; import useRundown from '../../common/hooks-query/useRundown'; import useSettings from '../../common/hooks-query/useSettings'; import { throttle } from '../../common/utils/throttle'; import { getDefaultFormat } from '../../common/utils/time'; import { getPropertyValue, isStringBoolean } from '../viewers/common/viewUtils'; import EditModal from './edit-modal/EditModal'; import FollowButton from './follow-button/FollowButton'; import OperatorBlock from './operator-block/OperatorBlock'; import OperatorEvent from './operator-event/OperatorEvent'; import StatusBar from './status-bar/StatusBar'; import { getOperatorOptions } from './operator.options'; import style from './Operator.module.scss'; const selectedOffset = 50; export type Subscribed = { id: string; label: string; colour: string; value: string }[]; type TitleFields = Pick; export type EditEvent = Pick & { subscriptions: Subscribed }; export default function Operator() { const { data, status } = useRundown(); const { data: customFields, status: customFieldStatus } = useCustomFields(); const { data: projectData, status: projectDataStatus } = useProjectData(); const timeoutId = useRef(null); const featureData = useOperator(); const [searchParams] = useSearchParams(); const { data: settings } = useSettings(); const [showEditPrompt, setShowEditPrompt] = useState(false); const [editEvent, setEditEvent] = useState(null); const [lockAutoScroll, setLockAutoScroll] = useState(false); const selectedRef = useRef(null); const scrollRef = useRef(null); const scrollToComponent = useFollowComponent({ followRef: selectedRef, scrollRef, doFollow: !lockAutoScroll, topOffset: selectedOffset, }); useWindowTitle('Operator'); // reset scroll if nothing is selected useEffect(() => { if (!featureData?.selectedEventId) { if (!lockAutoScroll) { scrollRef.current?.scrollTo(0, 0); } } }, [featureData?.selectedEventId, lockAutoScroll, scrollRef]); const handleOffset = () => { if (featureData.selectedEventId) { scrollToComponent(); } setLockAutoScroll(false); }; // prevent considering automated scrolls as user scrolls const handleUserScroll = () => { if (selectedRef?.current && scrollRef?.current) { const selectedRect = selectedRef.current.getBoundingClientRect(); const scrollerRect = scrollRef.current.getBoundingClientRect(); if (selectedRect && scrollerRect) { const distanceFromTop = selectedRect.top - scrollerRect.top; const hasScrolledOutOfThreshold = distanceFromTop < -8 || distanceFromTop > selectedOffset; setLockAutoScroll(hasScrolledOutOfThreshold); } } }; const throttledHandleScroll = throttle(handleUserScroll, 1000); const handleScroll = () => { if (timeoutId.current) { clearTimeout(timeoutId.current); } timeoutId.current = setTimeout(() => { setShowEditPrompt(false); }, 700); setShowEditPrompt(true); throttledHandleScroll(); }; const handleEdit = useCallback((event: EditEvent) => { setEditEvent({ ...event }); }, []); const missingData = !data || !customFields || !projectData; const isLoading = status === 'pending' || customFieldStatus === 'pending' || projectDataStatus === 'pending'; if (missingData || isLoading) { return ; } // get fields which the user subscribed to const shouldEdit = searchParams.get('shouldEdit'); // subscriptions is a MultiSelect and may have multiple values const subscriptions = searchParams.getAll('subscribe').filter((value) => Object.hasOwn(customFields, value)); const canEdit = shouldEdit && subscriptions.length; const main = searchParams.get('main') as keyof TitleFields | null; const secondary = searchParams.get('secondary'); const defaultFormat = getDefaultFormat(settings?.timeFormat); const operatorOptions = getOperatorOptions(customFields, defaultFormat); let isPast = Boolean(featureData.selectedEventId); const hidePast = isStringBoolean(searchParams.get('hidepast')); const { firstEvent } = getFirstEventNormal(data.entries, data.order); const { lastEvent } = getLastEventNormal(data.entries, data.order); return (
{editEvent && setEditEvent(null)} />} {canEdit && (
Press and hold to edit user field
)}
{data.order.map((eventId) => { const entry = data.entries[eventId]; if (isOntimeEvent(entry)) { 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; } const mainField = main ? getPropertyValue(entry, main) ?? '' : entry.title; const secondaryField = getPropertyValue(entry, secondary) ?? ''; const subscribedData = subscriptions ? subscriptions.flatMap((id) => { if (!customFields[id]) { return []; } const { label, colour } = customFields[id]; return [{ id, label, colour, value: entry.custom[id] }]; }) : null; return ( undefined} /> ); } if (entry.type === SupportedEntry.Block) { return ; } return null; })}
); }