import { OntimeView, isOntimeEvent, isOntimeGroup } from 'ontime-types'; import { Fragment, useCallback, useEffect, useMemo, useRef, useState } from 'react'; 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 { cx } from '../../common/utils/styleUtils'; import { throttle } from '../../common/utils/throttle'; import { getDefaultFormat } from '../../common/utils/time'; import { isTouchDevice } from '../../externals'; import Loader from '../../views/common/loader/Loader'; import CustomFieldEditModal from './custom-field-edit-modal/CustomFieldEditModal'; import FollowButton from './follow-button/FollowButton'; import OperatorEvent from './operator-event/OperatorEvent'; import OperatorGroup from './operator-group/OperatorGroup'; import { getOperatorOptions, useOperatorOptions } from './operator.options'; import type { EditEvent } from './operator.types'; import { getEventData } from './operator.utils'; import StatusBar from './status-bar/StatusBar'; import { OperatorData, useOperatorData } from './useOperatorData'; import style from './Operator.module.scss'; const selectedOffset = 50; export default function OperatorLoader() { const { data, status } = useOperatorData(); useWindowTitle('Operator'); if (status === 'pending') { return ; } if (status === 'error') { return ; } return ; } function Operator({ rundown, rundownMetadata, customFields, settings }: OperatorData) { const selectedEventId = useSelectedEventId(); const { subscribe, mainSource, secondarySource, shouldEdit, hidePast, showStart } = useOperatorOptions(); 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, followTrigger: selectedEventId, }); const timeoutId = useRef(null); // 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 }); }, []); // gather option data const defaultFormat = getDefaultFormat(settings?.timeFormat); const operatorOptions = useMemo(() => getOperatorOptions(customFields, defaultFormat), [customFields, defaultFormat]); const canEdit = shouldEdit && subscribe.length; return (
{editEvent && setEditEvent(null)} />} {canEdit && (
{isTouchDevice ? 'Press and hold to edit user field' : 'Right click to edit user field'}
)}
{rundown.order.map((entryId) => { const entry = rundown.entries[entryId]; if (isOntimeEvent(entry)) { const { isPast, isLinkedToLoaded, isLoaded, totalGap } = rundownMetadata[entryId]; // 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)) { const { isPast } = rundownMetadata[entry.id]; const isCurrentParent = selectedEventId ? rundownMetadata[selectedEventId]?.groupId === entry.id : false; if (hidePast && isPast && !isCurrentParent) { return null; } return ( {entry.entries.map((nestedEntryId) => { const nestedEntry = rundown.entries[nestedEntryId]; if (!isOntimeEvent(nestedEntry)) { return null; } const { isPast, isLoaded, isLinkedToLoaded, totalGap } = rundownMetadata[nestedEntryId]; // hide past events (if setting) and skipped events if ((hidePast && isPast) || nestedEntry.skip) { return null; } const { mainField, secondaryField, subscribedData } = getEventData( nestedEntry, mainSource, secondarySource, subscribe, customFields, ); return ( undefined} /> ); })} ); } return null; })}
); }