diff --git a/apps/client/.eslintrc b/apps/client/.eslintrc index 726c4bf48..402e99e49 100644 --- a/apps/client/.eslintrc +++ b/apps/client/.eslintrc @@ -27,6 +27,7 @@ "prettier" ], "rules": { + "@typescript-eslint/no-non-null-assertion": "warn", "prettier/prettier": [ "error", { diff --git a/apps/client/src/common/stores/appModeStore.ts b/apps/client/src/common/stores/appModeStore.ts new file mode 100644 index 000000000..5e031d190 --- /dev/null +++ b/apps/client/src/common/stores/appModeStore.ts @@ -0,0 +1,59 @@ +import { create } from 'zustand'; + +export enum AppMode { + Run = 'run', + Edit = 'edit', +} + +type AppModeStore = { + mode: AppMode; + cursor: string | null; + editId: string | null; + setMode: (mode: AppMode) => void; + setCursor: (id: string | null, isEditable?: boolean) => void; + setEditId: (id: string | null) => void; +}; + +export const useAppMode = create()((set) => ({ + mode: AppMode.Edit, + cursor: null, + editId: null, + setMode: (mode: AppMode) => + set((state) => { + return mode === AppMode.Edit + ? { + editId: state.cursor, + mode: mode, + } + : { + editId: null, + mode: mode, + }; + }), + setCursor: (id: string | null, isEditable?: boolean) => + set((state) => { + if (isEditable) { + return state.mode === AppMode.Edit + ? { + cursor: id, + editId: id, + } + : { + cursor: id, + }; + } else { + return { cursor: id, editId: null }; + } + }), + setEditId: (id: string | null) => + set((state) => { + return state.mode === AppMode.Edit + ? { + cursor: id, + editId: id, + } + : { + editId: id, + }; + }), +})); diff --git a/apps/client/src/common/stores/cursorStore.ts b/apps/client/src/common/stores/cursorStore.ts deleted file mode 100644 index 6c9679299..000000000 --- a/apps/client/src/common/stores/cursorStore.ts +++ /dev/null @@ -1,24 +0,0 @@ -import { create } from 'zustand'; - -import { booleanFromLocalStorage } from '../utils/localStorage'; - -type CursorStore = { - cursor: number; - isCursorLocked: boolean; - toggleCursorLocked: (newValue?: boolean) => void; - moveCursorTo: (index: number) => void; -}; - -const cursorLockedKey = 'ontime-cursor-islocked'; - -export const useCursor = create()((set) => ({ - cursor: 0, - isCursorLocked: booleanFromLocalStorage(cursorLockedKey, false), - toggleCursorLocked: (newValue?: boolean) => - set((state) => { - const val = typeof newValue === 'undefined' ? !state.isCursorLocked : newValue; - localStorage.setItem(cursorLockedKey, String(val)); - return { isCursorLocked: val }; - }), - moveCursorTo: (index: number) => set(() => ({ cursor: index })), -})); diff --git a/apps/client/src/common/stores/eventEditor.ts b/apps/client/src/common/stores/eventEditor.ts deleted file mode 100644 index 7640bde28..000000000 --- a/apps/client/src/common/stores/eventEditor.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { create } from 'zustand'; - -type EventEditorStore = { - openId: string | null; - setOpenEvent: (eventId: string) => void; - removeOpenEvent: () => void; -}; - -export const useEventEditorStore = create()((set) => ({ - openId: null, - setOpenEvent: (eventId: string | null) => set({ openId: eventId }), - removeOpenEvent: () => set({ openId: null }), -})); diff --git a/apps/client/src/common/utils/__tests__/eventsManager.test.js b/apps/client/src/common/utils/__tests__/eventsManager.test.js index 5b24034e8..a65f9853b 100644 --- a/apps/client/src/common/utils/__tests__/eventsManager.test.js +++ b/apps/client/src/common/utils/__tests__/eventsManager.test.js @@ -1,4 +1,4 @@ -import { formatEventList, getEventsWithDelay, trimEventlist } from '../eventsManager'; +import { formatEventList, getEventsWithDelay, trimRundown } from '../eventsManager'; describe('getEventsWithDelay function', () => { test('with positive delays', () => { @@ -365,7 +365,7 @@ describe('test trimEventlist function', () => { { id: '8' }, ]; - const l = trimEventlist(testData, selectedId, limit); + const l = trimRundown(testData, selectedId, limit); expect(l.length).toBe(limit); expect(l).toStrictEqual(expected); }); @@ -383,7 +383,7 @@ describe('test trimEventlist function', () => { { id: '8' }, ]; - const l = trimEventlist(testData, selectedId, limit); + const l = trimRundown(testData, selectedId, limit); expect(l.length).toBe(limit); expect(l).toStrictEqual(expected); }); @@ -401,7 +401,7 @@ describe('test trimEventlist function', () => { { id: '9' }, ]; - const l = trimEventlist(testData, selectedId, limit); + const l = trimRundown(testData, selectedId, limit); expect(l.length).toBe(limit); expect(l).toStrictEqual(expected); }); @@ -419,7 +419,7 @@ describe('test trimEventlist function', () => { { id: '8' }, ]; - const l = trimEventlist(testData, selectedId, limit); + const l = trimRundown(testData, selectedId, limit); expect(l.length).toBe(limit); expect(l).toStrictEqual(expected); }); diff --git a/apps/client/src/common/utils/eventsManager.ts b/apps/client/src/common/utils/eventsManager.ts index 9f07ba48d..9f8fbbc27 100644 --- a/apps/client/src/common/utils/eventsManager.ts +++ b/apps/client/src/common/utils/eventsManager.ts @@ -4,18 +4,18 @@ import { formatTime } from './time'; /** * @description From a list of events, returns only events of type event with calculated delays - * @param {Object[]} events - given events + * @param {Object[]} rundown - given rundown * @returns {Object[]} Filtered events with calculated delays */ -export const getEventsWithDelay = (events: OntimeRundownEntry[]): OntimeEvent[] => { - if (events == null) return []; +export const getEventsWithDelay = (rundown: OntimeRundownEntry[]): OntimeEvent[] => { + if (rundown == null) return []; const delayedEvents: OntimeEvent[] = []; // Add running delay let delay = 0; - for (const event of events) { + for (const event of rundown) { if (event.type === SupportedEvent.Block) delay = 0; else if (event.type === SupportedEvent.Delay) { if (typeof event.duration === 'number') { @@ -36,29 +36,29 @@ export const getEventsWithDelay = (events: OntimeRundownEntry[]): OntimeEvent[] /** * @description Returns trimmed event list array - * @param {Object[]} events - given events + * @param {Object[]} rundown - given rundown * @param {string} selectedId - id of currently selected event * @param {number} limit - max number of events to return * @returns {Object[]} Event list with maximum objects */ -export const trimEventlist = (events: OntimeRundownEntry[], selectedId: string, limit: number) => { - if (events == null) return []; +export const trimRundown = (rundown: OntimeRundownEntry[], selectedId: string, limit: number) => { + if (rundown == null) return []; const BEFORE = 2; - const trimmedEvents = [...events]; + const trimmedRundown = [...rundown]; // limit events length if necessary if (limit != null) { - while (trimmedEvents.length > limit) { - const idx = trimmedEvents.findIndex((e) => e.id === selectedId); + while (trimmedRundown.length > limit) { + const idx = trimmedRundown.findIndex((e) => e.id === selectedId); if (idx <= BEFORE) { - trimmedEvents.pop(); + trimmedRundown.pop(); } else { - trimmedEvents.shift(); + trimmedRundown.shift(); } } } - return trimmedEvents; + return trimmedRundown; }; type FormatEventListOptionsProp = { @@ -66,7 +66,7 @@ type FormatEventListOptionsProp = { }; /** * @description Returns list of events formatted to be displayed - * @param {Object[]} events - given events + * @param {Object[]} rundown - given rundown * @param {string} selectedId - id of currently selected event * @param {string} nextId - id of next event * @param {object} [options] @@ -74,15 +74,15 @@ type FormatEventListOptionsProp = { * @returns {Object[]} Formatted list of events [{time: -, title: -, isNow, isNext}] */ export const formatEventList = ( - events: OntimeEvent[], + rundown: OntimeEvent[], selectedId: string, nextId: string, options: FormatEventListOptionsProp, ) => { - if (events == null) return []; + if (rundown == null) return []; const { showEnd = false } = options; - const givenEvents = [...events]; + const givenEvents = [...rundown]; // format list const formattedEvents = []; @@ -124,3 +124,42 @@ export const cloneEvent = (event: OntimeEvent, after?: string): ClonedEvent => { after: after, }; }; + +/** + * Gets first event in rundown, if it exists + * @param {OntimeRundownEntry[]} rundown + * @return {OntimeEvent | null} + */ +export function getFirstEvent(rundown: OntimeRundownEntry[]) { + return rundown.length ? rundown[0] : null; +} + +/** + * Gets next event in rundown, if it exists + * @param {OntimeRundownEntry[]} rundown + * @param {string} currentId + * @return {OntimeEvent | null} + */ +export function getNextEvent(rundown: OntimeRundownEntry[], currentId: string) { + const index = rundown.findIndex((event) => event.id === currentId); + if (index !== -1 && index + 1 < rundown.length) { + return rundown[index + 1]; + } else { + return null; + } +} + +/** + * Gets previous event in rundown, if it exists + * @param {OntimeRundownEntry[]} rundown + * @param {string} currentId + * @return {OntimeEvent | null} + */ +export function getPreviousEvent(rundown: OntimeRundownEntry[], currentId: string) { + const index = rundown.findIndex((event) => event.id === currentId); + if (index !== -1 && index - 1 >= 0) { + return rundown[index - 1]; + } else { + return null; + } +} diff --git a/apps/client/src/features/event-editor/EventEditor.tsx b/apps/client/src/features/event-editor/EventEditor.tsx index d0ac458b6..bb7a0b3d6 100644 --- a/apps/client/src/features/event-editor/EventEditor.tsx +++ b/apps/client/src/features/event-editor/EventEditor.tsx @@ -3,7 +3,7 @@ import { OntimeEvent } from 'ontime-types'; import CopyTag from '../../common/components/copy-tag/CopyTag'; import useRundown from '../../common/hooks-query/useRundown'; -import { useEventEditorStore } from '../../common/stores/eventEditor'; +import { useAppMode } from '../../common/stores/appModeStore'; import getDelayTo from '../../common/utils/getDelayTo'; import EventEditorTimes from './composite/EventEditorTimes'; @@ -14,7 +14,7 @@ import style from './EventEditor.module.scss'; export type EventEditorSubmitActions = keyof OntimeEvent; export default function EventEditor() { - const { openId } = useEventEditorStore(); + const openId = useAppMode((state) => state.editId); const { data } = useRundown(); const [event, setEvent] = useState(null); const [delay, setDelay] = useState(0); diff --git a/apps/client/src/features/event-editor/EventEditorExport.tsx b/apps/client/src/features/event-editor/EventEditorExport.tsx index 304315872..587a2cd0d 100644 --- a/apps/client/src/features/event-editor/EventEditorExport.tsx +++ b/apps/client/src/features/event-editor/EventEditorExport.tsx @@ -3,7 +3,8 @@ import { Box, IconButton } from '@chakra-ui/react'; import { FiX } from '@react-icons/all-files/fi/FiX'; import ErrorBoundary from '../../common/components/error-boundary/ErrorBoundary'; -import { useEventEditorStore } from '../../common/stores/eventEditor'; +import { AppMode, useAppMode } from '../../common/stores/appModeStore'; +import { cx } from '../../common/utils/styleUtils'; import EventEditor from './EventEditor'; @@ -18,15 +19,27 @@ const closeBtnStyle = { }; const EventEditorExport = () => { - const { openId, removeOpenEvent } = useEventEditorStore(); + const appMode = useAppMode((state) => state.mode); + const editId = useAppMode((state) => state.editId); + const setEditId = useAppMode((state) => state.setEditId); + + const editorStyle = cx([style.eventEditor, !editId ? style.noEvent : null]); + const removeOpenEvent = () => setEditId(null); + const canRemoveOpenId = appMode === AppMode.Run; return ( - +
- } onClick={removeOpenEvent} {...closeBtnStyle} /> + } + onClick={removeOpenEvent} + isDisabled={!canRemoveOpenId} + {...closeBtnStyle} + />
diff --git a/apps/client/src/features/menu/MenuBar.tsx b/apps/client/src/features/menu/MenuBar.tsx index 8355aa027..a64fda3e3 100644 --- a/apps/client/src/features/menu/MenuBar.tsx +++ b/apps/client/src/features/menu/MenuBar.tsx @@ -1,18 +1,19 @@ import { useCallback, useEffect } from 'react'; import { VStack } from '@chakra-ui/react'; -import { FiHelpCircle } from '@react-icons/all-files/fi/FiHelpCircle'; -import { FiMinimize } from '@react-icons/all-files/fi/FiMinimize'; import { FiSave } from '@react-icons/all-files/fi/FiSave'; import { FiUpload } from '@react-icons/all-files/fi/FiUpload'; import { IoExtensionPuzzle } from '@react-icons/all-files/io5/IoExtensionPuzzle'; import { IoExtensionPuzzleOutline } from '@react-icons/all-files/io5/IoExtensionPuzzleOutline'; -import { IoScan } from '@react-icons/all-files/io5/IoScan'; +import { IoHelpCircleOutline } from '@react-icons/all-files/io5/IoHelpCircleOutline'; +import { IoOptions } from '@react-icons/all-files/io5/IoOptions'; +import { IoPlay } from '@react-icons/all-files/io5/IoPlay'; import { IoSettingsOutline } from '@react-icons/all-files/io5/IoSettingsOutline'; import { downloadRundown } from '../../common/api/ontimeApi'; import QuitIconBtn from '../../common/components/buttons/QuitIconBtn'; import TooltipActionBtn from '../../common/components/buttons/TooltipActionBtn'; import useElectronEvent from '../../common/hooks/useElectronEvent'; +import { AppMode, useAppMode } from '../../common/stores/appModeStore'; import style from './MenuBar.module.scss'; @@ -52,21 +53,20 @@ export default function MenuBar(props: MenuBarProps) { } = props; const { isElectron, sendToElectron } = useElectronEvent(); + const appMode = useAppMode((state) => state.mode); + const setAppMode = useAppMode((state) => state.setMode); + + const setRunMode = () => setAppMode(AppMode.Run); + const setEditMode = () => setAppMode(AppMode.Edit); + const actionHandler = useCallback( (action: Actions) => { - // Stop crashes when testing locally if (!isElectron) { if (action === 'help') { window.open('https://cpvalente.gitbook.io/ontime/'); } } else { switch (action) { - case 'min': - sendToElectron('set-window', 'to-tray'); - break; - case 'max': - sendToElectron('set-window', 'to-max'); - break; case 'shutdown': sendToElectron('shutdown', 'now'); break; @@ -113,22 +113,7 @@ export default function MenuBar(props: MenuBarProps) { return ( actionHandler('shutdown')} /> - } - clickHandler={() => actionHandler('max')} - tooltip='Show full window' - aria-label='Show full window' - isDisabled={!isElectron} - /> - } - clickHandler={() => actionHandler('min')} - tooltip='Minimise to tray' - aria-label='Minimise to tray' - isDisabled={!isElectron} - /> +
+ +
+ } + className={appMode === AppMode.Run ? style.open : ''} + clickHandler={setRunMode} + tooltip='Run mode' + aria-label='Run mode' + /> + } + className={appMode === AppMode.Edit ? style.open : ''} + clickHandler={setEditMode} + tooltip='Edit mode' + aria-label='Edit mode' + /> +
} + icon={} clickHandler={() => actionHandler('help')} tooltip='Help' aria-label='Help' diff --git a/apps/client/src/features/menu/RundownMenu.module.scss b/apps/client/src/features/menu/RundownMenu.module.scss index 85bf21eec..f5803c125 100644 --- a/apps/client/src/features/menu/RundownMenu.module.scss +++ b/apps/client/src/features/menu/RundownMenu.module.scss @@ -1,15 +1,4 @@ -@use '../../theme/v2Styles' as *; -@use '../../theme/ontimeColours' as *; - .headerButtons { - align-content: center; - justify-content: space-between; + text-align: right; padding-top: 24px; } - -.labelledSwitch { - display: flex; - align-items: center; - gap: $element-spacing; - color: $gray-100; -} diff --git a/apps/client/src/features/menu/RundownMenu.tsx b/apps/client/src/features/menu/RundownMenu.tsx index b0696a587..01ea78e5f 100644 --- a/apps/client/src/features/menu/RundownMenu.tsx +++ b/apps/client/src/features/menu/RundownMenu.tsx @@ -1,5 +1,5 @@ import { memo, useCallback } from 'react'; -import { Button, HStack, Menu, MenuButton, MenuDivider, MenuItem, MenuList, Switch } from '@chakra-ui/react'; +import { Button, Menu, MenuButton, MenuDivider, MenuItem, MenuList } from '@chakra-ui/react'; import { FiMinusCircle } from '@react-icons/all-files/fi/FiMinusCircle'; import { FiTrash2 } from '@react-icons/all-files/fi/FiTrash2'; import { IoAdd } from '@react-icons/all-files/io5/IoAdd'; @@ -7,15 +7,13 @@ import { IoTimerOutline } from '@react-icons/all-files/io5/IoTimerOutline'; import { SupportedEvent } from 'ontime-types'; import { useEventAction } from '../../common/hooks/useEventAction'; -import { useCursor } from '../../common/stores/cursorStore'; -import { useEventEditorStore } from '../../common/stores/eventEditor'; +import { useAppMode } from '../../common/stores/appModeStore'; import style from './RundownMenu.module.scss'; const RundownMenu = () => { - const isCursorLocked = useCursor((state) => state.isCursorLocked); - const toggleCursorLocked = useCursor((state) => state.toggleCursorLocked); - const removeOpenEvent = useEventEditorStore((state) => state.removeOpenEvent); + const setEditId = useAppMode((state) => state.setEditId); + const setCursor = useAppMode((state) => state.setCursor); const { addEvent, deleteAllEvents } = useEventAction(); @@ -33,19 +31,12 @@ const RundownMenu = () => { const deleteAll = useCallback(() => { deleteAllEvents(); - removeOpenEvent(); - }, [deleteAllEvents, removeOpenEvent]); + setEditId(null); + setCursor(null); + }, [deleteAllEvents, setCursor, setEditId]); return ( - - +
} size='sm' variant='ontime-subtle'> Event... @@ -66,7 +57,7 @@ const RundownMenu = () => { - +
); }; diff --git a/apps/client/src/features/rundown/Rundown.tsx b/apps/client/src/features/rundown/Rundown.tsx index 0cb83bd53..f42827a12 100644 --- a/apps/client/src/features/rundown/Rundown.tsx +++ b/apps/client/src/features/rundown/Rundown.tsx @@ -5,9 +5,9 @@ import { OntimeRundown, Playback, SupportedEvent } from 'ontime-types'; import { useEventAction } from '../../common/hooks/useEventAction'; import { useRundownEditor } from '../../common/hooks/useSocket'; -import { useCursor } from '../../common/stores/cursorStore'; +import { AppMode, useAppMode } from '../../common/stores/appModeStore'; import { useLocalEvent } from '../../common/stores/localEvent'; -import { cloneEvent } from '../../common/utils/eventsManager'; +import { cloneEvent, getFirstEvent, getNextEvent, getPreviousEvent } from '../../common/utils/eventsManager'; import QuickAddBlock from './quick-add-block/QuickAddBlock'; import RundownEmpty from './RundownEmpty'; @@ -31,46 +31,47 @@ export default function Rundown(props: RundownProps) { const showQuickEntry = eventSettings.showQuickEntry; // cursor - const cursor = useCursor((state) => state.cursor); - const isCursorLocked = useCursor((state) => state.isCursorLocked); - const moveCursorTo = useCursor((state) => state.moveCursorTo); + const cursor = useAppMode((state) => state.cursor); + const appMode = useAppMode((state) => state.mode); + const viewFollowsCursor = appMode === AppMode.Run; + const moveCursorTo = useAppMode((state) => state.setCursor); const cursorRef = useRef(); // DND KIT const sensors = useSensors(useSensor(PointerSensor)); const insertAtCursor = useCallback( - (type: SupportedEvent | 'clone', cursor: number) => { - if (cursor === -1) { + (type: SupportedEvent | 'clone', cursor: string | null) => { + if (cursor === null) { + // we cant clone without selection if (type === 'clone') { return; } + // the only thing to do is adding an event at top addEvent({ type }); - } else { - const previousEvent = entries?.[cursor]; - const nextEvent = entries?.[cursor + 1]; + return; + } - // prevent adding two non-event blocks consecutively - const isPreviousDifferent = previousEvent?.type !== type; - const isNextDifferent = nextEvent?.type !== type; - if (type === 'clone' && previousEvent?.type === SupportedEvent.Event) { - const newEvent = cloneEvent(previousEvent); - newEvent.after = previousEvent.id; + if (type === 'clone') { + const cursorEvent = entries.find((event) => event.id === cursor); + if (cursorEvent?.type === SupportedEvent.Event) { + const newEvent = cloneEvent(cursorEvent); + newEvent.after = cursorEvent.id; addEvent(newEvent); - } else if (type === SupportedEvent.Event) { - const newEvent = { - type: SupportedEvent.Event, - }; - const options = { - defaultPublic: defaultPublic, - startTimeIsLastEnd: startTimeIsLastEnd, - lastEventId: previousEvent.id, - after: previousEvent.id, - }; - addEvent(newEvent, options); - } else if (isPreviousDifferent && isNextDifferent && type !== 'clone') { - addEvent({ type }, { after: previousEvent.id }); } + } else if (type === SupportedEvent.Event) { + const newEvent = { + type: SupportedEvent.Event, + }; + const options = { + defaultPublic: defaultPublic, + startTimeIsLastEnd: startTimeIsLastEnd, + lastEventId: cursor, + after: cursor, + }; + addEvent(newEvent, options); + } else { + addEvent({ type }, { after: cursor }); } }, [addEvent, defaultPublic, entries, startTimeIsLastEnd], @@ -85,41 +86,50 @@ export default function Rundown(props: RundownProps) { if (event.altKey && (!event.ctrlKey || !event.shiftKey)) { switch (event.code) { case 'ArrowDown': { - if (cursor < entries.length - 1) moveCursorTo(cursor + 1); + if (entries.length < 1) { + return; + } + const nextEvent = cursor == null ? getFirstEvent(entries) : getNextEvent(entries, cursor); + if (nextEvent) { + moveCursorTo(nextEvent.id, nextEvent.type === SupportedEvent.Event); + } break; } case 'ArrowUp': { - if (cursor > 0) moveCursorTo(cursor - 1); + if (entries.length < 1) { + return; + } + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion -- we check for this before + const previousEvent = cursor == null ? getFirstEvent(entries) : getPreviousEvent(entries, cursor); + if (previousEvent) { + moveCursorTo(previousEvent.id, previousEvent.type === SupportedEvent.Event); + } break; } case 'KeyE': { event.preventDefault(); - if (cursor === -1) return; insertAtCursor(SupportedEvent.Event, cursor); break; } case 'KeyD': { event.preventDefault(); - if (cursor < 0) return; insertAtCursor(SupportedEvent.Delay, cursor); break; } case 'KeyB': { event.preventDefault(); - if (cursor < 0) return; insertAtCursor(SupportedEvent.Block, cursor); break; } case 'KeyC': { event.preventDefault(); - if (cursor < 0) return; insertAtCursor('clone', cursor); break; } } } }, - [cursor, entries.length, insertAtCursor, moveCursorTo], + [cursor, entries, insertAtCursor, moveCursorTo], ); // we copy the state from the store here @@ -155,28 +165,13 @@ export default function Rundown(props: RundownProps) { }); }, [cursorRef]); - // if selected event - // or cursor settings changed useEffect(() => { - // and if we are locked - if (!isCursorLocked || !featureData?.selectedEventId) { + // in run mode, we follow selection + if (!viewFollowsCursor || !featureData?.selectedEventId) { return; } - - // move cursor - let gotoIndex = -1; - let found = false; - for (const entry of entries) { - gotoIndex++; - if (entry.id === featureData.selectedEventId) { - found = true; - break; - } - } - if (found) { - moveCursorTo(gotoIndex); - } - }, [featureData?.selectedEventId, entries, isCursorLocked, moveCursorTo]); + moveCursorTo(featureData.selectedEventId); + }, [featureData?.selectedEventId, viewFollowsCursor, moveCursorTo]); const handleOnDragEnd = (event: DragEndEvent) => { const { active, over } = event; @@ -195,7 +190,7 @@ export default function Rundown(props: RundownProps) { }; if (statefulEntries?.length < 1) { - return insertAtCursor(SupportedEvent.Event, -1)} />; + return insertAtCursor(SupportedEvent.Event, null)} />; } let cumulativeDelay = 0; @@ -227,16 +222,16 @@ export default function Rundown(props: RundownProps) { const isLast = index === entries.length - 1; const isSelected = featureData?.selectedEventId === entry.id; const isNext = featureData?.nextEventId === entry.id; + const hasCursor = entry.id === cursor; return ( -
+
- {((showQuickEntry && index === cursor) || isLast) && ( + {((showQuickEntry && hasCursor) || isLast) && ( )}
diff --git a/apps/client/src/features/rundown/RundownEntry.tsx b/apps/client/src/features/rundown/RundownEntry.tsx index 84ac993be..ab0ec5275 100644 --- a/apps/client/src/features/rundown/RundownEntry.tsx +++ b/apps/client/src/features/rundown/RundownEntry.tsx @@ -2,7 +2,7 @@ import { useCallback } from 'react'; import { OntimeEvent, OntimeRundownEntry, Playback, SupportedEvent } from 'ontime-types'; import { useEventAction } from '../../common/hooks/useEventAction'; -import { useEventEditorStore } from '../../common/stores/eventEditor'; +import { useAppMode } from '../../common/stores/appModeStore'; import { useLocalEvent } from '../../common/stores/localEvent'; import { useEmitLog } from '../../common/stores/logger'; import { cloneEvent } from '../../common/utils/eventsManager'; @@ -16,7 +16,6 @@ export type EventItemActions = 'set-cursor' | 'event' | 'delay' | 'block' | 'del interface RundownEntryProps { type: SupportedEvent; - index: number; eventIndex: number; data: OntimeRundownEntry; selected: boolean; @@ -30,24 +29,25 @@ interface RundownEntryProps { } export default function RundownEntry(props: RundownEntryProps) { - const { - index, - eventIndex, - data, - selected, - hasCursor, - next, - delay, - previousEnd, - previousEventId, - playback, - isRolling, - } = props; + const { eventIndex, data, selected, hasCursor, next, delay, previousEnd, previousEventId, playback, isRolling } = + props; const { emitError } = useEmitLog(); const { addEvent, updateEvent, deleteEvent } = useEventAction(); - const openId = useEventEditorStore((state) => state.openId); - const removeOpenEvent = useEventEditorStore((state) => state.removeOpenEvent); + const cursor = useAppMode((state) => state.cursor); + const setCursor = useAppMode((state) => state.setCursor); + const openId = useAppMode((state) => state.editId); + const setEditId = useAppMode((state) => state.setEditId); + + const removeOpenEvent = useCallback(() => { + if (openId === data.id) { + setEditId(null); + } + + if (cursor === data.id) { + setCursor(null); + } + }, [cursor, data.id, openId, setCursor, setEditId]); const eventSettings = useLocalEvent((state) => state.eventSettings); const defaultPublic = eventSettings.defaultPublic; @@ -146,7 +146,6 @@ export default function RundownEntry(props: RundownEntryProps) { timeStart={data.timeStart} timeEnd={data.timeEnd} duration={data.duration} - index={index} eventIndex={eventIndex + 1} eventId={data.id} isPublic={data.isPublic} diff --git a/apps/client/src/features/rundown/event-block/EventBlock.tsx b/apps/client/src/features/rundown/event-block/EventBlock.tsx index 2dd3b608c..0b05c65a7 100644 --- a/apps/client/src/features/rundown/event-block/EventBlock.tsx +++ b/apps/client/src/features/rundown/event-block/EventBlock.tsx @@ -4,8 +4,7 @@ import { CSS } from '@dnd-kit/utilities'; import { IoReorderTwo } from '@react-icons/all-files/io5/IoReorderTwo'; import { EndAction, OntimeEvent, Playback, TimerType } from 'ontime-types'; -import { useCursor } from '../../../common/stores/cursorStore'; -import { useEventEditorStore } from '../../../common/stores/eventEditor'; +import { useAppMode } from '../../../common/stores/appModeStore'; import { cx, getAccessibleColour } from '../../../common/utils/styleUtils'; import { EventItemActions } from '../RundownEntry'; @@ -17,7 +16,6 @@ interface EventBlockProps { timeStart: number; timeEnd: number; duration: number; - index: number; eventIndex: number; eventId: string; isPublic: boolean; @@ -50,7 +48,6 @@ export default function EventBlock(props: EventBlockProps) { timeStart, timeEnd, duration, - index, eventIndex, eventId, isPublic = true, @@ -70,10 +67,10 @@ export default function EventBlock(props: EventBlockProps) { actionHandler, } = props; - const moveCursorTo = useCursor((state) => state.moveCursorTo); + const moveCursorTo = useAppMode((state) => state.setCursor); const handleRef = useRef(null); const [isVisible, setIsVisible] = useState(false); - const openId = useEventEditorStore((state) => state.openId); + const openId = useAppMode((state) => state.editId); const { isDragging, @@ -136,7 +133,12 @@ export default function EventBlock(props: EventBlockProps) { return (
-
moveCursorTo(index)}> +
moveCursorTo(eventId, true)} + > diff --git a/apps/client/src/features/rundown/event-block/EventBlockInner.tsx b/apps/client/src/features/rundown/event-block/EventBlockInner.tsx index 7e3902f7a..0c593f4f1 100644 --- a/apps/client/src/features/rundown/event-block/EventBlockInner.tsx +++ b/apps/client/src/features/rundown/event-block/EventBlockInner.tsx @@ -12,7 +12,7 @@ import { IoTime } from '@react-icons/all-files/io5/IoTime'; import { EndAction, Playback, TimerType } from 'ontime-types'; import TooltipActionBtn from '../../../common/components/buttons/TooltipActionBtn'; -import { useEventEditorStore } from '../../../common/stores/eventEditor'; +import { useAppMode } from '../../../common/stores/appModeStore'; import { tooltipDelayMid } from '../../../ontimeConfig'; import EditableBlockTitle from '../common/EditableBlockTitle'; import { EventItemActions } from '../RundownEntry'; @@ -76,8 +76,7 @@ const EventBlockInner = (props: EventBlockInnerProps) => { } = props; const [renderInner, setRenderInner] = useState(false); - const setOpenEvent = useEventEditorStore((state) => state.setOpenEvent); - const removeOpenEvent = useEventEditorStore((state) => state.removeOpenEvent); + const setEditId = useAppMode((state) => state.setEditId); useEffect(() => { setRenderInner(true); @@ -85,11 +84,11 @@ const EventBlockInner = (props: EventBlockInnerProps) => { const toggleOpenEvent = useCallback(() => { if (isOpen) { - removeOpenEvent(); + setEditId(null); } else { - setOpenEvent(eventId); + setEditId(eventId); } - }, [eventId, isOpen, removeOpenEvent, setOpenEvent]); + }, [eventId, isOpen, setEditId]); const eventIsPlaying = playback === Playback.Play; const eventIsPaused = playback === Playback.Pause; diff --git a/apps/client/src/features/viewers/studio/StudioClock.jsx b/apps/client/src/features/viewers/studio/StudioClock.jsx index 456141f2d..575b7f218 100644 --- a/apps/client/src/features/viewers/studio/StudioClock.jsx +++ b/apps/client/src/features/viewers/studio/StudioClock.jsx @@ -8,7 +8,7 @@ import NavigationMenu from '../../../common/components/navigation-menu/Navigatio import useFitText from '../../../common/hooks/useFitText'; import { useRuntimeStylesheet } from '../../../common/hooks/useRuntimeStylesheet'; import { formatDisplay } from '../../../common/utils/dateConfig'; -import { formatEventList, getEventsWithDelay, trimEventlist } from '../../../common/utils/eventsManager'; +import { formatEventList, getEventsWithDelay, trimRundown } from '../../../common/utils/eventsManager'; import { formatTime } from '../../../common/utils/time'; import './StudioClock.scss'; @@ -59,7 +59,7 @@ export default function StudioClock(props) { const delayed = getEventsWithDelay(backstageEvents); const events = delayed.filter((e) => e.type === 'event'); - const trimmed = trimEventlist(events, selectedId, MAX_TITLES); + const trimmed = trimRundown(events, selectedId, MAX_TITLES); const formatted = formatEventList(trimmed, selectedId, nextId, { showEnd: false, });