mirror of
https://github.com/cpvalente/ontime.git
synced 2026-08-08 08:53:51 +00:00
feat: edit mode (#344)
* style: add mode selection to menu * style: rename rundown functions * feat: edit mode
This commit is contained in:
@@ -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);
|
||||
});
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user