import { Fragment, useCallback, useEffect, useMemo, useRef, useState } from 'react'; import { isOntimeEvent, isOntimeGroup, OntimeView } from 'ontime-types'; import EmptyPage from '../../common/components/state/EmptyPage'; import ViewParamsEditor from '../../common/components/view-params-editor/ViewParamsEditor'; import useFollowComponent from '../../common/hooks/useFollowComponent'; import { useSelectedEventId } 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 { cx } from '../../common/utils/styleUtils'; import { throttle } from '../../common/utils/throttle'; import { getDefaultFormat } from '../../common/utils/time'; import EditModal from './edit-modal/EditModal'; import FollowButton from './follow-button/FollowButton'; import OperatorEvent from './operator-event/OperatorEvent'; import OperatorGroup from './operator-group/OperatorGroup'; import StatusBar from './status-bar/StatusBar'; import { getOperatorOptions, useOperatorOptions } from './operator.options'; import type { EditEvent } from './operator.types'; import { getEventData, makeOperatorMetadata } from './operator.utils'; import style from './Operator.module.scss'; const selectedOffset = 50; 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 { selectedEventId } = useSelectedEventId(); const { subscribe, mainSource, secondarySource, shouldEdit, hidePast, showStart } = useOperatorOptions(); 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 (!selectedEventId) { if (!lockAutoScroll) { scrollRef.current?.scrollTo(0, 0); } } }, [selectedEventId, lockAutoScroll, scrollRef]); const handleOffset = () => { if (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'; // gather option data const defaultFormat = getDefaultFormat(settings?.timeFormat); const operatorOptions = useMemo(() => getOperatorOptions(customFields, defaultFormat), [customFields, defaultFormat]); if (missingData || isLoading) { return ; } const canEdit = shouldEdit && subscribe.length; const { process } = makeOperatorMetadata(selectedEventId); return (
{editEvent && setEditEvent(null)} />} {canEdit && (
Press and hold to edit user field
)}
{data.order.map((entryId) => { const entry = data.entries[entryId]; if (isOntimeEvent(entry)) { const { isPast, isSelected, isLinkedToLoaded, totalGap } = process(entry); // hide past events (if setting) and skipped events if ((hidePast && isPast) || entry.skip) { return null; } const { mainField, secondaryField, subscribedData } = getEventData( entry, mainSource, secondarySource, subscribe, customFields, ); return ( undefined} /> ); } if (isOntimeGroup(entry)) { return ( {entry.entries.map((nestedEntryId) => { const nestedEntry = data.entries[nestedEntryId]; if (!isOntimeEvent(nestedEntry)) { return null; } const { isPast, isSelected, isLinkedToLoaded, totalGap } = process(nestedEntry); // hide past events (if setting) and skipped events if (hidePast && isPast) { return null; } const { mainField, secondaryField, subscribedData } = getEventData( nestedEntry, mainSource, secondarySource, subscribe, customFields, ); return ( undefined} /> ); })} ); } return null; })}
); }