import { useCallback, useEffect, useRef, useState } from 'react'; import { useSearchParams } from 'react-router-dom'; import { CustomField, CustomFields, isOntimeEvent, OntimeEvent, SupportedEvent } from 'ontime-types'; import { getFirstEventNormal, getLastEventNormal } from 'ontime-utils'; import NavigationMenu from '../../common/components/navigation-menu/NavigationMenu'; 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 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 { debounce } from '../../common/utils/debounce'; 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 style from './Operator.module.scss'; const selectedOffset = 50; type TitleFields = Pick; export type EditEvent = Pick & { fieldLabel?: string; fieldValue: string }; export type PartialEdit = EditEvent & { field: keyof CustomFields; }; 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, }); // Set window title useEffect(() => { document.title = 'ontime - 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 debouncedHandleScroll = debounce(handleUserScroll, 1000); const handleScroll = () => { if (timeoutId.current) { clearTimeout(timeoutId.current); } timeoutId.current = setTimeout(() => { setShowEditPrompt(false); }, 700); setShowEditPrompt(true); debouncedHandleScroll(); }; const handleEdit = useCallback( (event: EditEvent) => { const field = searchParams.get('subscribe') as keyof CustomField | null; if (field) { setEditEvent({ ...event, field }); } }, [searchParams], ); 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'); const subscribe = searchParams.get('subscribe') as keyof CustomFields; const canEdit = shouldEdit && subscribe; 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.rundown, data.order); const { lastEvent } = getLastEventNormal(data.rundown, data.order); return (
{editEvent && setEditEvent(null)} />} {canEdit && (
Press and hold to edit user field
)}
{data.order.map((eventId) => { const entry = data.rundown[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 ? entry?.[main] || entry.title : entry.title; const secondaryField = getPropertyValue(entry, secondary) ?? ''; const subscribedData = entry.custom[subscribe]?.value; return ( undefined} /> ); } if (entry.type === SupportedEvent.Block) { return ; } return null; })}
); }