feat: edit mode (#344)

* style: add mode selection to menu

* style: rename rundown functions

* feat: edit mode
This commit is contained in:
Carlos Valente
2023-04-21 20:06:31 +02:00
committed by GitHub
parent da889a4825
commit 9c4c84905e
16 changed files with 272 additions and 218 deletions
+1
View File
@@ -27,6 +27,7 @@
"prettier"
],
"rules": {
"@typescript-eslint/no-non-null-assertion": "warn",
"prettier/prettier": [
"error",
{
@@ -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<AppModeStore>()((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,
};
}),
}));
@@ -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<CursorStore>()((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 })),
}));
@@ -1,13 +0,0 @@
import { create } from 'zustand';
type EventEditorStore = {
openId: string | null;
setOpenEvent: (eventId: string) => void;
removeOpenEvent: () => void;
};
export const useEventEditorStore = create<EventEditorStore>()((set) => ({
openId: null,
setOpenEvent: (eventId: string | null) => set({ openId: eventId }),
removeOpenEvent: () => set({ openId: null }),
}));
@@ -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);
});
+56 -17
View File
@@ -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 <limit> 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;
}
}
@@ -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<OntimeEvent | null>(null);
const [delay, setDelay] = useState(0);
@@ -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 (
<Box className={`${style.eventEditor} ${!openId ? style.noEvent : ''}`}>
<Box className={editorStyle}>
<ErrorBoundary>
<div className={style.eventEditorLayout}>
<EventEditor />
<div className={style.header}>
<IconButton aria-label='Close Menu' icon={<FiX />} onClick={removeOpenEvent} {...closeBtnStyle} />
<IconButton
aria-label='Close Menu'
icon={<FiX />}
onClick={removeOpenEvent}
isDisabled={!canRemoveOpenId}
{...closeBtnStyle}
/>
</div>
</div>
</ErrorBoundary>
+31 -27
View File
@@ -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 (
<VStack>
<QuitIconBtn clickHandler={() => actionHandler('shutdown')} />
<TooltipActionBtn
{...buttonStyle}
icon={<IoScan />}
clickHandler={() => actionHandler('max')}
tooltip='Show full window'
aria-label='Show full window'
isDisabled={!isElectron}
/>
<TooltipActionBtn
{...buttonStyle}
icon={<FiMinimize />}
clickHandler={() => actionHandler('min')}
tooltip='Minimise to tray'
aria-label='Minimise to tray'
isDisabled={!isElectron}
/>
<div className={style.gap} />
<TooltipActionBtn
{...buttonStyle}
@@ -145,6 +130,25 @@ export default function MenuBar(props: MenuBarProps) {
tooltip='Export showfile'
aria-label='Export showfile'
/>
<div className={style.gap} />
<TooltipActionBtn
{...buttonStyle}
icon={<IoPlay />}
className={appMode === AppMode.Run ? style.open : ''}
clickHandler={setRunMode}
tooltip='Run mode'
aria-label='Run mode'
/>
<TooltipActionBtn
{...buttonStyle}
icon={<IoOptions />}
className={appMode === AppMode.Edit ? style.open : ''}
clickHandler={setEditMode}
tooltip='Edit mode'
aria-label='Edit mode'
/>
<div className={style.gap} />
<TooltipActionBtn
{...buttonStyle}
@@ -165,7 +169,7 @@ export default function MenuBar(props: MenuBarProps) {
<div className={style.gap} />
<TooltipActionBtn
{...buttonStyle}
icon={<FiHelpCircle />}
icon={<IoHelpCircleOutline />}
clickHandler={() => actionHandler('help')}
tooltip='Help'
aria-label='Help'
@@ -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;
}
+9 -18
View File
@@ -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 (
<HStack className={style.headerButtons}>
<label className={style.labelledSwitch}>
<Switch
defaultChecked={isCursorLocked}
onChange={(event) => toggleCursorLocked(event.target.checked)}
variant='ontime'
/>
Follow loaded event
</label>
<div className={style.headerButtons}>
<Menu isLazy lazyBehavior='unmount' variant='ontime-on-dark'>
<MenuButton as={Button} leftIcon={<IoAdd />} size='sm' variant='ontime-subtle'>
Event...
@@ -66,7 +57,7 @@ const RundownMenu = () => {
</MenuItem>
</MenuList>
</Menu>
</HStack>
</div>
);
};
+58 -63
View File
@@ -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<HTMLDivElement>();
// 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 <RundownEmpty handleAddNew={() => insertAtCursor(SupportedEvent.Event, -1)} />;
return <RundownEmpty handleAddNew={() => 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 (
<div key={entry.id} ref={cursor === index ? cursorRef : undefined}>
<div key={entry.id} ref={hasCursor ? cursorRef : undefined}>
<RundownEntry
type={entry.type}
index={index}
eventIndex={eventIndex}
data={entry}
selected={isSelected}
hasCursor={cursor === index}
hasCursor={hasCursor}
next={isNext}
delay={cumulativeDelay}
previousEnd={previousEnd}
@@ -244,13 +239,13 @@ export default function Rundown(props: RundownProps) {
playback={isSelected ? featureData.playback : undefined}
isRolling={featureData.playback === Playback.Roll}
/>
{((showQuickEntry && index === cursor) || isLast) && (
{((showQuickEntry && hasCursor) || isLast) && (
<QuickAddBlock
showKbd={index === cursor}
showKbd={hasCursor}
eventId={entry.id}
previousEventId={previousEventId}
disableAddDelay={entry.type === 'delay'}
disableAddBlock={entry.type === 'block'}
disableAddDelay={entry.type === SupportedEvent.Delay}
disableAddBlock={entry.type === SupportedEvent.Block}
/>
)}
</div>
@@ -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}
@@ -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 | HTMLSpanElement>(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 (
<div className={blockClasses} ref={setNodeRef} style={dragStyle}>
<div className={style.binder} style={{ ...binderColours }} tabIndex={-1} onClick={() => moveCursorTo(index)}>
<div
className={style.binder}
style={{ ...binderColours }}
tabIndex={-1}
onClick={() => moveCursorTo(eventId, true)}
>
<span className={style.drag} ref={handleRef} {...dragAttributes} {...dragListeners}>
<IoReorderTwo />
</span>
@@ -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;
@@ -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,
});