mirror of
https://github.com/cpvalente/ontime.git
synced 2026-09-06 14:59:09 +00:00
feat: operator view
This commit is contained in:
@@ -8,11 +8,12 @@ import { tooltipDelayFast } from '../../../ontimeConfig';
|
|||||||
|
|
||||||
interface PlaybackIconProps {
|
interface PlaybackIconProps {
|
||||||
state: Playback;
|
state: Playback;
|
||||||
|
skipTooltip?: boolean;
|
||||||
className?: string;
|
className?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function PlaybackIcon(props: PlaybackIconProps) {
|
export default function PlaybackIcon(props: PlaybackIconProps) {
|
||||||
const { state, className } = props;
|
const { state, skipTooltip, className } = props;
|
||||||
|
|
||||||
// if timer is Pause or Armed
|
// if timer is Pause or Armed
|
||||||
let label = 'Timer Paused';
|
let label = 'Timer Paused';
|
||||||
@@ -29,6 +30,10 @@ export default function PlaybackIcon(props: PlaybackIconProps) {
|
|||||||
Icon = IoStop;
|
Icon = IoStop;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (skipTooltip) {
|
||||||
|
return <Icon className={className} />;
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Tooltip openDelay={tooltipDelayFast} label={label} shouldWrapChildren>
|
<Tooltip openDelay={tooltipDelayFast} label={label} shouldWrapChildren>
|
||||||
<Icon className={className} />
|
<Icon className={className} />
|
||||||
|
|||||||
@@ -0,0 +1,61 @@
|
|||||||
|
import { MutableRefObject, useCallback, useEffect } from 'react';
|
||||||
|
|
||||||
|
function scrollToComponent<ComponentRef extends HTMLElement, ScrollRef extends HTMLElement>(
|
||||||
|
componentRef: MutableRefObject<ComponentRef>,
|
||||||
|
scrollRef: MutableRefObject<ScrollRef>,
|
||||||
|
topOffset: number,
|
||||||
|
) {
|
||||||
|
if (!componentRef.current || !scrollRef.current) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const componentRect = componentRef.current.getBoundingClientRect();
|
||||||
|
const scrollRect = scrollRef.current.getBoundingClientRect();
|
||||||
|
const top = componentRect.top - scrollRect.top + scrollRef.current.scrollTop - topOffset;
|
||||||
|
|
||||||
|
scrollRef.current.scrollTo({ top, behavior: 'smooth' });
|
||||||
|
}
|
||||||
|
|
||||||
|
interface UseFollowComponentProps {
|
||||||
|
followRef: MutableRefObject<HTMLElement | null>;
|
||||||
|
scrollRef: MutableRefObject<HTMLElement | null>;
|
||||||
|
doFollow: boolean;
|
||||||
|
topOffset?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function useFollowComponent(props: UseFollowComponentProps) {
|
||||||
|
const { followRef, scrollRef, doFollow, topOffset = 100 } = props;
|
||||||
|
|
||||||
|
// when cursor moves, view should follow
|
||||||
|
useEffect(() => {
|
||||||
|
if (!doFollow) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (followRef.current && scrollRef.current) {
|
||||||
|
// Use requestAnimationFrame to ensure the component is fully loaded
|
||||||
|
window.requestAnimationFrame(() => {
|
||||||
|
scrollToComponent(
|
||||||
|
followRef as MutableRefObject<HTMLElement>,
|
||||||
|
scrollRef as MutableRefObject<HTMLElement>,
|
||||||
|
topOffset,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// eslint-disable-next-line -- the prompt seems incorrect
|
||||||
|
}, [followRef?.current, scrollRef?.current]);
|
||||||
|
|
||||||
|
const scrollToRefComponent = useCallback(
|
||||||
|
(componentRef = followRef, containerRef = scrollRef, offset = topOffset) => {
|
||||||
|
if (followRef.current && containerRef.current) {
|
||||||
|
// @ts-expect-error -- we know this are not null
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||||
|
scrollToComponent(componentRef!, scrollRef!, offset);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[followRef, scrollRef, topOffset],
|
||||||
|
);
|
||||||
|
|
||||||
|
return scrollToRefComponent;
|
||||||
|
}
|
||||||
@@ -15,7 +15,6 @@ export const useOperator = () => {
|
|||||||
const featureSelector = (state: RuntimeStore) => ({
|
const featureSelector = (state: RuntimeStore) => ({
|
||||||
playback: state.playback,
|
playback: state.playback,
|
||||||
selectedEventId: state.loaded.selectedEventId,
|
selectedEventId: state.loaded.selectedEventId,
|
||||||
nextEventId: state.loaded.nextEventId,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
return useRuntimeStore(featureSelector, deepCompare);
|
return useRuntimeStore(featureSelector, deepCompare);
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { MutableRefObject, useEffect, useRef } from 'react';
|
import { useRef } from 'react';
|
||||||
import { Tooltip } from '@chakra-ui/react';
|
import { Tooltip } from '@chakra-ui/react';
|
||||||
import {
|
import {
|
||||||
closestCenter,
|
closestCenter,
|
||||||
@@ -14,6 +14,7 @@ import { horizontalListSortingStrategy, SortableContext, sortableKeyboardCoordin
|
|||||||
import { ColumnDef, flexRender, getCoreRowModel, useReactTable } from '@tanstack/react-table';
|
import { ColumnDef, flexRender, getCoreRowModel, useReactTable } from '@tanstack/react-table';
|
||||||
import { isOntimeBlock, isOntimeDelay, isOntimeEvent, OntimeRundown, OntimeRundownEntry } from 'ontime-types';
|
import { isOntimeBlock, isOntimeDelay, isOntimeEvent, OntimeRundown, OntimeRundownEntry } from 'ontime-types';
|
||||||
|
|
||||||
|
import useFollowComponent from '../../common/hooks/useFollowComponent';
|
||||||
import { useLocalStorage } from '../../common/hooks/useLocalStorage';
|
import { useLocalStorage } from '../../common/hooks/useLocalStorage';
|
||||||
import { millisToDelayString } from '../../common/utils/dateConfig';
|
import { millisToDelayString } from '../../common/utils/dateConfig';
|
||||||
import { getAccessibleColour } from '../../common/utils/styleUtils';
|
import { getAccessibleColour } from '../../common/utils/styleUtils';
|
||||||
@@ -46,6 +47,8 @@ export default function Cuesheet({ data, columns, handleUpdate, selectedId }: Cu
|
|||||||
const [columnSizing, setColumnSizing] = useLocalStorage('table-sizes', {});
|
const [columnSizing, setColumnSizing] = useLocalStorage('table-sizes', {});
|
||||||
|
|
||||||
const selectedRef = useRef<HTMLTableRowElement | null>(null);
|
const selectedRef = useRef<HTMLTableRowElement | null>(null);
|
||||||
|
const tableContainerRef = useRef<HTMLDivElement | null>(null);
|
||||||
|
useFollowComponent({ followRef: selectedRef, scrollRef: tableContainerRef, doFollow: followSelected });
|
||||||
|
|
||||||
const table = useReactTable({
|
const table = useReactTable({
|
||||||
data,
|
data,
|
||||||
@@ -63,7 +66,6 @@ export default function Cuesheet({ data, columns, handleUpdate, selectedId }: Cu
|
|||||||
onColumnSizingChange: setColumnSizing,
|
onColumnSizingChange: setColumnSizing,
|
||||||
getCoreRowModel: getCoreRowModel(),
|
getCoreRowModel: getCoreRowModel(),
|
||||||
});
|
});
|
||||||
const tableContainerRef = useRef<HTMLDivElement>(null);
|
|
||||||
|
|
||||||
const sensors = useSensors(
|
const sensors = useSensors(
|
||||||
useSensor(PointerSensor, {
|
useSensor(PointerSensor, {
|
||||||
@@ -83,34 +85,6 @@ export default function Cuesheet({ data, columns, handleUpdate, selectedId }: Cu
|
|||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
// when selection moves, view should follow
|
|
||||||
useEffect(() => {
|
|
||||||
function scrollToComponent(
|
|
||||||
componentRef: MutableRefObject<HTMLTableRowElement>,
|
|
||||||
scrollRef: MutableRefObject<HTMLDivElement>,
|
|
||||||
) {
|
|
||||||
const componentRect = componentRef.current.getBoundingClientRect();
|
|
||||||
const scrollRect = scrollRef.current.getBoundingClientRect();
|
|
||||||
const top = componentRect.top - scrollRect.top + scrollRef.current.scrollTop - 100;
|
|
||||||
scrollRef.current.scrollTo({ top, behavior: 'smooth' });
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!followSelected) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (selectedRef.current && tableContainerRef.current) {
|
|
||||||
// Use requestAnimationFrame to ensure the component is fully loaded
|
|
||||||
window.requestAnimationFrame(() => {
|
|
||||||
scrollToComponent(
|
|
||||||
selectedRef as MutableRefObject<HTMLTableRowElement>,
|
|
||||||
tableContainerRef as MutableRefObject<HTMLDivElement>,
|
|
||||||
);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
// eslint-disable-next-line -- the prompt seems incorrect, we need the refs
|
|
||||||
}, [selectedRef.current, tableContainerRef.current, followSelected]);
|
|
||||||
|
|
||||||
const handleOnDragEnd = (event: DragEndEvent) => {
|
const handleOnDragEnd = (event: DragEndEvent) => {
|
||||||
const { delta, active, over } = event;
|
const { delta, active, over } = event;
|
||||||
|
|
||||||
|
|||||||
@@ -18,6 +18,10 @@
|
|||||||
gap: 2px;
|
gap: 2px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.spacer {
|
||||||
|
min-height: 80vh;
|
||||||
|
}
|
||||||
|
|
||||||
@mixin block() {
|
@mixin block() {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
padding: 0.25rem 0.5rem;
|
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 { useSearchParams } from 'react-router-dom';
|
||||||
import { SupportedEvent, UserFields } from 'ontime-types';
|
import { SupportedEvent, UserFields } from 'ontime-types';
|
||||||
import { getLastEvent } from 'ontime-utils';
|
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 Empty from '../../common/components/state/Empty';
|
||||||
import { getOperatorOptions } from '../../common/components/view-params-editor/constants';
|
import { getOperatorOptions } from '../../common/components/view-params-editor/constants';
|
||||||
import ViewParamsEditor from '../../common/components/view-params-editor/ViewParamsEditor';
|
import ViewParamsEditor from '../../common/components/view-params-editor/ViewParamsEditor';
|
||||||
|
import useFollowComponent from '../../common/hooks/useFollowComponent';
|
||||||
import { useOperator } from '../../common/hooks/useSocket';
|
import { useOperator } from '../../common/hooks/useSocket';
|
||||||
import useRundown from '../../common/hooks-query/useRundown';
|
import useRundown from '../../common/hooks-query/useRundown';
|
||||||
import useUserFields from '../../common/hooks-query/useUserFields';
|
import useUserFields from '../../common/hooks-query/useUserFields';
|
||||||
@@ -19,23 +20,53 @@ import StatusBar from './status-bar/StatusBar';
|
|||||||
|
|
||||||
import style from './Operator.module.scss';
|
import style from './Operator.module.scss';
|
||||||
|
|
||||||
|
const selectedOffset = 50;
|
||||||
|
|
||||||
export default function Operator() {
|
export default function Operator() {
|
||||||
const { data, status } = useRundown();
|
const { data, status } = useRundown();
|
||||||
const { data: userFields, status: userFieldsStatus } = useUserFields();
|
const { data: userFields, status: userFieldsStatus } = useUserFields();
|
||||||
const featureData = useOperator();
|
const featureData = useOperator();
|
||||||
const [showChild, setShowChild] = useState(false);
|
|
||||||
const [searchParams] = useSearchParams();
|
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
|
// Set window title
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
document.title = 'ontime - Operator';
|
document.title = 'ontime - Operator';
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const handleScroll = (event: UIEvent<HTMLElement>) => {
|
// reset scroll if nothing is selected
|
||||||
const scrollThreshold = 50;
|
useEffect(() => {
|
||||||
const scrollPosition = (event.target as HTMLElement).scrollTop;
|
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') {
|
if (!data || status === 'loading' || !userFields || userFieldsStatus === 'loading') {
|
||||||
@@ -49,24 +80,38 @@ export default function Operator() {
|
|||||||
const lastEvent = getLastEvent(data);
|
const lastEvent = getLastEvent(data);
|
||||||
|
|
||||||
const operatorOptions = getOperatorOptions(userFields);
|
const operatorOptions = getOperatorOptions(userFields);
|
||||||
|
let isPast = Boolean(featureData.selectedEventId);
|
||||||
|
const hidePast = isStringBoolean(searchParams.get('hidepast'));
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={style.operatorContainer}>
|
<div className={style.operatorContainer}>
|
||||||
<NavigationMenu />
|
<NavigationMenu />
|
||||||
<ViewParamsEditor paramFields={operatorOptions} />
|
<ViewParamsEditor paramFields={operatorOptions} />
|
||||||
|
|
||||||
<div className={style.operatorEvents} onScroll={handleScroll}>
|
<div className={style.operatorEvents} onScroll={handleScroll} ref={scrollRef}>
|
||||||
{data.map((entry) => {
|
{data.map((entry) => {
|
||||||
if (entry.type === SupportedEvent.Event) {
|
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 (
|
return (
|
||||||
<OperatorEvent
|
<OperatorEvent
|
||||||
key={entry.id}
|
key={entry.id}
|
||||||
cue={entry.cue}
|
cue={entry.cue}
|
||||||
data={entry}
|
data={entry}
|
||||||
isSelected={featureData.selectedEventId === entry.id}
|
isSelected={isSelected}
|
||||||
subscribed={subscribe}
|
subscribed={subscribe}
|
||||||
subscribedAlias={subscribedAlias}
|
subscribedAlias={subscribedAlias}
|
||||||
showSeconds={showSeconds}
|
showSeconds={showSeconds}
|
||||||
|
isPast={isPast}
|
||||||
|
selectedRef={isSelected ? selectedRef : undefined}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -76,8 +121,9 @@ export default function Operator() {
|
|||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
})}
|
})}
|
||||||
|
<div className={style.spacer} />
|
||||||
</div>
|
</div>
|
||||||
<FollowButton isVisible={showChild} onClickHandler={() => undefined} />
|
<FollowButton isVisible={lockAutoScroll} onClickHandler={handleOffset} />
|
||||||
<StatusBar playback={featureData.playback} lastEvent={lastEvent} selectedEventId={featureData.selectedEventId} />
|
<StatusBar playback={featureData.playback} lastEvent={lastEvent} selectedEventId={featureData.selectedEventId} />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -5,6 +5,7 @@
|
|||||||
.event {
|
.event {
|
||||||
@include block();
|
@include block();
|
||||||
background-color: $gray-1300;
|
background-color: $gray-1300;
|
||||||
|
opacity: 1;
|
||||||
|
|
||||||
&.subscribed {
|
&.subscribed {
|
||||||
background-color: $gray-1250;
|
background-color: $gray-1250;
|
||||||
@@ -13,6 +14,10 @@
|
|||||||
&.running {
|
&.running {
|
||||||
background-color: $red-700;
|
background-color: $red-700;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&.past {
|
||||||
|
opacity: 0.2;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.titles {
|
.titles {
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { RefObject } from 'react';
|
||||||
import { OntimeEvent, UserFields } from 'ontime-types';
|
import { OntimeEvent, UserFields } from 'ontime-types';
|
||||||
|
|
||||||
import DelayIndicator from '../../../common/components/delay-indicator/DelayIndicator';
|
import DelayIndicator from '../../../common/components/delay-indicator/DelayIndicator';
|
||||||
@@ -14,6 +15,8 @@ interface OperatorEventProps {
|
|||||||
subscribed: keyof UserFields | null;
|
subscribed: keyof UserFields | null;
|
||||||
subscribedAlias: string;
|
subscribedAlias: string;
|
||||||
showSeconds: boolean;
|
showSeconds: boolean;
|
||||||
|
isPast: boolean;
|
||||||
|
selectedRef?: RefObject<HTMLDivElement>;
|
||||||
}
|
}
|
||||||
|
|
||||||
// extract this to contain re-renders
|
// extract this to contain re-renders
|
||||||
@@ -23,7 +26,7 @@ function RollingTime() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export default function OperatorEvent(props: OperatorEventProps) {
|
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 start = formatTime(data.timeStart, { showSeconds });
|
||||||
const end = formatTime(data.timeEnd, { showSeconds });
|
const end = formatTime(data.timeEnd, { showSeconds });
|
||||||
@@ -35,10 +38,11 @@ export default function OperatorEvent(props: OperatorEventProps) {
|
|||||||
style.event,
|
style.event,
|
||||||
isSelected ? style.running : null,
|
isSelected ? style.running : null,
|
||||||
subscribedData ? style.subscribed : null,
|
subscribedData ? style.subscribed : null,
|
||||||
|
isPast ? style.past : null,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={operatorClasses}>
|
<div className={operatorClasses} ref={selectedRef}>
|
||||||
<div className={style.titles}>
|
<div className={style.titles}>
|
||||||
<span className={style.title}>
|
<span className={style.title}>
|
||||||
{data.title} - {data.subtitle}
|
{data.title} - {data.subtitle}
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { useMemo } from 'react';
|
||||||
import { OntimeEvent, Playback } from 'ontime-types';
|
import { OntimeEvent, Playback } from 'ontime-types';
|
||||||
import { millisToString } from 'ontime-utils';
|
import { millisToString } from 'ontime-utils';
|
||||||
|
|
||||||
@@ -37,9 +38,13 @@ export default function StatusBar({
|
|||||||
const runningTime = millisToString(timer.current);
|
const runningTime = millisToString(timer.current);
|
||||||
const elapsedTime = millisToString(timer.elapsed);
|
const elapsedTime = millisToString(timer.elapsed);
|
||||||
|
|
||||||
|
const PlaybackIconComponent = useMemo(() => {
|
||||||
|
return <PlaybackIcon state={playback} skipTooltip className={styles.playbackIcon} />;
|
||||||
|
}, [playback]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={styles.statusBar}>
|
<div className={styles.statusBar}>
|
||||||
<PlaybackIcon state={playback} className={styles.playbackIcon} />
|
{PlaybackIconComponent}
|
||||||
<div className={styles.clock}>
|
<div className={styles.clock}>
|
||||||
<div className={styles.column}>
|
<div className={styles.column}>
|
||||||
<span className={styles.label}>Time now</span>
|
<span className={styles.label}>Time now</span>
|
||||||
|
|||||||
@@ -1,10 +1,11 @@
|
|||||||
import { Fragment, lazy, MutableRefObject, useCallback, useEffect, useRef, useState } from 'react';
|
import { Fragment, lazy, useCallback, useEffect, useRef, useState } from 'react';
|
||||||
import { closestCenter, DndContext, DragEndEvent, PointerSensor, useSensor, useSensors } from '@dnd-kit/core';
|
import { closestCenter, DndContext, DragEndEvent, PointerSensor, useSensor, useSensors } from '@dnd-kit/core';
|
||||||
import { arrayMove, SortableContext, verticalListSortingStrategy } from '@dnd-kit/sortable';
|
import { arrayMove, SortableContext, verticalListSortingStrategy } from '@dnd-kit/sortable';
|
||||||
import { OntimeRundown, Playback, SupportedEvent } from 'ontime-types';
|
import { OntimeRundown, Playback, SupportedEvent } from 'ontime-types';
|
||||||
import { getFirst, getNext, getPrevious } from 'ontime-utils';
|
import { getFirst, getNext, getPrevious } from 'ontime-utils';
|
||||||
|
|
||||||
import { useEventAction } from '../../common/hooks/useEventAction';
|
import { useEventAction } from '../../common/hooks/useEventAction';
|
||||||
|
import useFollowComponent from '../../common/hooks/useFollowComponent';
|
||||||
import { useRundownEditor } from '../../common/hooks/useSocket';
|
import { useRundownEditor } from '../../common/hooks/useSocket';
|
||||||
import { AppMode, useAppMode } from '../../common/stores/appModeStore';
|
import { AppMode, useAppMode } from '../../common/stores/appModeStore';
|
||||||
import { useEditorSettings } from '../../common/stores/editorSettings';
|
import { useEditorSettings } from '../../common/stores/editorSettings';
|
||||||
@@ -41,6 +42,7 @@ export default function Rundown(props: RundownProps) {
|
|||||||
const moveCursorTo = useAppMode((state) => state.setCursor);
|
const moveCursorTo = useAppMode((state) => state.setCursor);
|
||||||
const cursorRef = useRef<HTMLDivElement | null>(null);
|
const cursorRef = useRef<HTMLDivElement | null>(null);
|
||||||
const scrollRef = useRef<HTMLDivElement | null>(null);
|
const scrollRef = useRef<HTMLDivElement | null>(null);
|
||||||
|
useFollowComponent({ followRef: cursorRef, scrollRef: scrollRef, doFollow: true });
|
||||||
|
|
||||||
// DND KIT
|
// DND KIT
|
||||||
const sensors = useSensors(useSensor(PointerSensor));
|
const sensors = useSensors(useSensor(PointerSensor));
|
||||||
@@ -153,28 +155,6 @@ export default function Rundown(props: RundownProps) {
|
|||||||
};
|
};
|
||||||
}, [handleKeyPress]);
|
}, [handleKeyPress]);
|
||||||
|
|
||||||
// when cursor moves, view should follow
|
|
||||||
useEffect(() => {
|
|
||||||
function scrollToComponent(
|
|
||||||
componentRef: MutableRefObject<HTMLDivElement>,
|
|
||||||
scrollRef: MutableRefObject<HTMLDivElement>,
|
|
||||||
) {
|
|
||||||
const componentRect = componentRef.current.getBoundingClientRect();
|
|
||||||
const scrollRect = scrollRef.current.getBoundingClientRect();
|
|
||||||
const top = componentRect.top - scrollRect.top + scrollRef.current.scrollTop - 100;
|
|
||||||
scrollRef.current.scrollTo({ top, behavior: 'smooth' });
|
|
||||||
}
|
|
||||||
|
|
||||||
if (cursorRef.current && scrollRef.current) {
|
|
||||||
// Use requestAnimationFrame to ensure the component is fully loaded
|
|
||||||
window.requestAnimationFrame(() => {
|
|
||||||
scrollToComponent(cursorRef as MutableRefObject<HTMLDivElement>, scrollRef as MutableRefObject<HTMLDivElement>);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// eslint-disable-next-line -- the prompt seems incorrect
|
|
||||||
}, [cursorRef?.current, scrollRef]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// in run mode, we follow selection
|
// in run mode, we follow selection
|
||||||
if (!viewFollowsCursor || !featureData?.selectedEventId) {
|
if (!viewFollowsCursor || !featureData?.selectedEventId) {
|
||||||
|
|||||||
Reference in New Issue
Block a user