Skip fixes (#805)

* refactor: resolve current in time-to-end

* refactor: recalculate on skip

* Alpha tweaks (#806)

* style: small tweaks to interface

* refactor: simplify quick add
This commit is contained in:
Carlos Valente
2024-03-08 19:54:18 +01:00
committed by GitHub
parent 6dc2bebeee
commit 10d7273ec0
29 changed files with 221 additions and 165 deletions
@@ -7,4 +7,5 @@
gap: 0.25rem; gap: 0.25rem;
overflow: hidden; overflow: hidden;
background-color: black;
} }
@@ -5,6 +5,7 @@ import { useKeyDown } from '../../common/hooks/useKeyDown';
import AboutPanel from './panel/about-panel/AboutPanel'; import AboutPanel from './panel/about-panel/AboutPanel';
import GeneralPanel from './panel/general-panel/GeneralPanel'; import GeneralPanel from './panel/general-panel/GeneralPanel';
import IntegrationsPanel from './panel/integrations-panel/IntegrationsPanel'; import IntegrationsPanel from './panel/integrations-panel/IntegrationsPanel';
import InterfacePanel from './panel/interface-panel/InterfacePanel';
import LogPanel from './panel/log-panel/LogPanel'; import LogPanel from './panel/log-panel/LogPanel';
import ProjectPanel from './panel/project-panel/ProjectPanel'; import ProjectPanel from './panel/project-panel/ProjectPanel';
import ProjectSettingsPanel from './panel/project-settings-panel/ProjectSettingsPanel'; import ProjectSettingsPanel from './panel/project-settings-panel/ProjectSettingsPanel';
@@ -32,6 +33,7 @@ export default function AppSettings() {
{selectedPanel === 'project' && <ProjectPanel />} {selectedPanel === 'project' && <ProjectPanel />}
{selectedPanel === 'general' && <GeneralPanel />} {selectedPanel === 'general' && <GeneralPanel />}
{selectedPanel === 'sources' && <SourcesPanel />} {selectedPanel === 'sources' && <SourcesPanel />}
{selectedPanel === 'interface' && <InterfacePanel />}
{selectedPanel === 'integrations' && <IntegrationsPanel />} {selectedPanel === 'integrations' && <IntegrationsPanel />}
{selectedPanel === 'project_settings' && <ProjectSettingsPanel />} {selectedPanel === 'project_settings' && <ProjectSettingsPanel />}
{selectedPanel === 'about' && <AboutPanel />} {selectedPanel === 'about' && <AboutPanel />}
@@ -1,5 +1,5 @@
import { PropsWithChildren } from 'react'; import { PropsWithChildren } from 'react';
import { IconButton } from '@chakra-ui/react'; import { Button } from '@chakra-ui/react';
import { IoClose } from '@react-icons/all-files/io5/IoClose'; import { IoClose } from '@react-icons/all-files/io5/IoClose';
import style from './PanelContent.module.scss'; import style from './PanelContent.module.scss';
@@ -14,7 +14,9 @@ export default function PanelContent(props: PropsWithChildren<PanelContentProps>
return ( return (
<div className={style.contentWrapper}> <div className={style.contentWrapper}>
<div className={style.corner}> <div className={style.corner}>
<IconButton onClick={onClose} aria-label='close' icon={<IoClose />} variant='ontime-ghosted-white' /> <Button onClick={onClose} aria-label='close' rightIcon={<IoClose />} variant='ontime-subtle'>
Close settings
</Button>
</div> </div>
<div className={style.content}>{children}</div> <div className={style.content}>{children}</div>
</div> </div>
@@ -55,7 +55,7 @@ $inner-padding: 1rem;
} }
.pad { .pad {
padding: 0 1rem; margin: 0 2rem;
max-height: 550px; max-height: 550px;
overflow-y: scroll; overflow-y: scroll;
} }
@@ -58,6 +58,7 @@ export default function CheckUpdatesButton(props: CheckUpdatesButtonProps) {
isLoading={isFetching} isLoading={isFetching}
isDisabled={disableButton} isDisabled={disableButton}
size='sm' size='sm'
maxWidth='max-content'
> >
Check for updates Check for updates
</Button> </Button>
@@ -1,6 +1,5 @@
import * as Panel from '../PanelUtils'; import * as Panel from '../PanelUtils';
import EditorSettingsForm from './EditorSettingsForm';
import GeneralPanelForm from './GeneralPanelForm'; import GeneralPanelForm from './GeneralPanelForm';
import ViewSettingsForm from './ViewSettingsForm'; import ViewSettingsForm from './ViewSettingsForm';
@@ -9,7 +8,6 @@ export default function GeneralPanel() {
<> <>
<Panel.Header>Settings</Panel.Header> <Panel.Header>Settings</Panel.Header>
<GeneralPanelForm /> <GeneralPanelForm />
<EditorSettingsForm />
<ViewSettingsForm /> <ViewSettingsForm />
</> </>
); );
@@ -91,7 +91,7 @@ export default function HttpIntegrations() {
</Button> </Button>
</div> </div>
</Panel.SubHeader> </Panel.SubHeader>
<Panel.Divider />
<Panel.Section as='form' id='http-form' onSubmit={handleSubmit(onSubmit)} onKeyDown={preventEscape}> <Panel.Section as='form' id='http-form' onSubmit={handleSubmit(onSubmit)} onKeyDown={preventEscape}>
{errors?.root && <Panel.Error>{errors.root.message}</Panel.Error>} {errors?.root && <Panel.Error>{errors.root.message}</Panel.Error>}
<Panel.ListGroup> <Panel.ListGroup>
@@ -0,0 +1,27 @@
import { Alert, AlertDescription, AlertIcon } from '@chakra-ui/react';
import * as Panel from '../PanelUtils';
import EditorSettingsForm from './EditorSettingsForm';
export default function InterfacePanel() {
return (
<>
<Panel.Header>Interface</Panel.Header>
<Panel.Section>
<Alert status='info' variant='ontime-on-dark-info'>
<AlertIcon />
<AlertDescription>
Interface settings
<br />
<br />
These concern settings that are applied to this user in this browser.
<br />
It will not affect other users or other browsers.
</AlertDescription>
</Alert>
</Panel.Section>
<EditorSettingsForm />
</>
);
}
@@ -81,7 +81,7 @@ export default function ManageProjects() {
isDisabled={Boolean(loading) || isCreatingProject} isDisabled={Boolean(loading) || isCreatingProject}
rightIcon={<IoAdd />} rightIcon={<IoAdd />}
> >
Add New
</Button> </Button>
</div> </div>
</Panel.SubHeader> </Panel.SubHeader>
@@ -77,7 +77,7 @@ export default function ProjectCreateForm(props: ProjectCreateFromProps) {
</div> </div>
</Panel.Title> </Panel.Title>
{error && <Panel.Error>{error}</Panel.Error>} {error && <Panel.Error>{error}</Panel.Error>}
<div className={style.createFormInputField}> <div className={style.innerColumn}>
<label> <label>
Project title Project title
<Input <Input
@@ -89,8 +89,6 @@ export default function ProjectCreateForm(props: ProjectCreateFromProps) {
{...register('title')} {...register('title')}
/> />
</label> </label>
</div>
<div className={style.createFormInputField}>
<label> <label>
Project description Project description
<Input <Input
@@ -102,8 +100,6 @@ export default function ProjectCreateForm(props: ProjectCreateFromProps) {
{...register('description')} {...register('description')}
/> />
</label> </label>
</div>
<div className={style.createFormInputField}>
<label> <label>
Public info Public info
<Textarea <Textarea
@@ -112,11 +108,10 @@ export default function ProjectCreateForm(props: ProjectCreateFromProps) {
maxLength={150} maxLength={150}
placeholder='Shows always start ontime' placeholder='Shows always start ontime'
autoComplete='off' autoComplete='off'
resize='none'
{...register('publicInfo')} {...register('publicInfo')}
/> />
</label> </label>
</div>
<div className={style.createFormInputField}>
<label> <label>
Public QR code Url Public QR code Url
<Input <Input
@@ -127,8 +122,6 @@ export default function ProjectCreateForm(props: ProjectCreateFromProps) {
{...register('publicUrl')} {...register('publicUrl')}
/> />
</label> </label>
</div>
<div className={style.createFormInputField}>
<label> <label>
Backstage info Backstage info
<Textarea <Textarea
@@ -137,11 +130,10 @@ export default function ProjectCreateForm(props: ProjectCreateFromProps) {
maxLength={150} maxLength={150}
placeholder='Wi-Fi password: 1234' placeholder='Wi-Fi password: 1234'
autoComplete='off' autoComplete='off'
resize='none'
{...register('backstageInfo')} {...register('backstageInfo')}
/> />
</label> </label>
</div>
<div className={style.createFormInputField}>
<label> <label>
Backstage QR code Url Backstage QR code Url
<Input <Input
@@ -70,84 +70,72 @@ export default function ProjectData() {
</Panel.SubHeader> </Panel.SubHeader>
<Panel.Divider /> <Panel.Divider />
<Panel.Section> <Panel.Section>
<div className={style.createFormInputField}> <label>
<label> Project title
Project title <Input
<Input variant='ontime-filled'
variant='ontime-filled' size='sm'
size='sm' maxLength={50}
maxLength={50} placeholder='Your project name'
placeholder='Your project name' autoComplete='off'
autoComplete='off' {...register('title')}
{...register('title')} />
/> </label>
</label> <label>
</div> Project description
<div className={style.createFormInputField}> <Input
<label> variant='ontime-filled'
Project description size='sm'
<Input maxLength={100}
variant='ontime-filled' placeholder='Euro Love, Malmö 2024'
size='sm' autoComplete='off'
maxLength={100} {...register('description')}
placeholder='Euro Love, Malmö 2024' />
autoComplete='off' </label>
{...register('description')} <label>
/> Public info
</label> <Textarea
</div> variant='ontime-filled'
<div className={style.createFormInputField}> size='sm'
<label> maxLength={150}
Public info placeholder='Shows always start ontime'
<Textarea autoComplete='off'
variant='ontime-filled' resize='none'
size='sm' {...register('publicInfo')}
maxLength={150} />
placeholder='Shows always start ontime' </label>
autoComplete='off' <label>
resize='none' Public QR code URL
{...register('publicInfo')} <Input
/> variant='ontime-filled'
</label> size='sm'
</div> placeholder='www.getontime.no'
<div className={style.createFormInputField}> autoComplete='off'
<label> {...register('publicUrl')}
Public QR code URL />
<Input </label>
variant='ontime-filled' <label>
size='sm' Backstage info
placeholder='www.getontime.no' <Textarea
autoComplete='off' variant='ontime-filled'
{...register('publicUrl')} size='sm'
/> maxLength={150}
</label> placeholder='Wi-Fi password: 1234'
</div> autoComplete='off'
<div className={style.createFormInputField}> resize='none'
<label> {...register('backstageInfo')}
Backstage info />
<Textarea </label>
variant='ontime-filled' <label>
size='sm' Backstage QR code URL
maxLength={150} <Input
placeholder='Wi-Fi password: 1234' variant='ontime-filled'
autoComplete='off' size='sm'
resize='none' placeholder='http://docs.getontime.no'
{...register('backstageInfo')} autoComplete='off'
/> {...register('backstageUrl')}
</label> />
</div> </label>
<div className={style.createFormInputField}>
<label>
Backstage QR code URL
<Input
variant='ontime-filled'
size='sm'
placeholder='http://docs.getontime.no'
autoComplete='off'
{...register('backstageUrl')}
/>
</label>
</div>
</Panel.Section> </Panel.Section>
</Panel.Card> </Panel.Card>
</Panel.Section> </Panel.Section>
@@ -18,10 +18,6 @@
flex: 2; flex: 2;
} }
.createFormInputField {
padding-bottom: 0.5rem;
}
.headerButtons, .headerButtons,
.actionButtons, .actionButtons,
.createActionButtons { .createActionButtons {
@@ -39,10 +35,6 @@
justify-content: flex-end; justify-content: flex-end;
} }
.createContainer {
padding: 0.5rem 0;
}
.saveButton { .saveButton {
text-transform: capitalize; text-transform: capitalize;
} }
@@ -50,3 +42,11 @@
.containCell { .containCell {
max-width: 400px; max-width: 400px;
} }
.innerColumn {
margin: 0 2rem;
margin-bottom: 2rem;
display: flex;
flex-direction: column;
gap: 1em;
}
@@ -56,6 +56,7 @@ export default function ProjectSettingsPanel() {
New New
</Button> </Button>
</Panel.SubHeader> </Panel.SubHeader>
<Panel.Divider />
<Panel.Section> <Panel.Section>
<Alert status='info' variant='ontime-on-dark-info'> <Alert status='info' variant='ontime-on-dark-info'>
<AlertIcon /> <AlertIcon />
@@ -13,14 +13,21 @@ export const settingPanels: Readonly<SettingsOption[]> = [
label: 'Project', label: 'Project',
secondary: [{ id: 'project__manage', label: 'Manage project files' }], secondary: [{ id: 'project__manage', label: 'Manage project files' }],
}, },
{ id: 'general', label: 'General', secondary: [{ id: 'general__manage', label: 'Manage app settings' }] }, {
id: 'general',
label: 'General',
secondary: [
{ id: 'general__manage', label: 'Manage Ontime settings' },
{ id: 'general__view', label: 'View settings' },
{ id: 'general__urlPresets', label: 'URL presets' },
],
},
{ {
id: 'project_settings', id: 'project_settings',
label: 'Project Settings', label: 'Project Settings',
secondary: [{ id: 'project_settings__custom', label: 'Custom fields' }], secondary: [{ id: 'project_settings__custom', label: 'Custom fields' }],
}, },
{ id: 'interface', label: 'Interface' }, { id: 'interface', label: 'Interface', secondary: [{ id: 'general__editor', label: 'Editor settings' }] },
{ id: 'views', label: 'Views' },
{ {
id: 'sources', id: 'sources',
label: 'Data Sources', label: 'Data Sources',
+21 -11
View File
@@ -1,7 +1,15 @@
import { Fragment, lazy, useCallback, useEffect, useRef, useState } from 'react'; import { Fragment, lazy, useCallback, useEffect, useRef, useState } from 'react';
import { closestCenter, DndContext, DragEndEvent, PointerSensor, useSensor, useSensors } from '@dnd-kit/core'; import { closestCenter, DndContext, DragEndEvent, PointerSensor, useSensor, useSensors } from '@dnd-kit/core';
import { arrayMove, SortableContext, verticalListSortingStrategy } from '@dnd-kit/sortable'; import { arrayMove, SortableContext, verticalListSortingStrategy } from '@dnd-kit/sortable';
import { isOntimeBlock, isOntimeDelay, isOntimeEvent, Playback, RundownCached, SupportedEvent } from 'ontime-types'; import {
isOntimeBlock,
isOntimeDelay,
isOntimeEvent,
MaybeNumber,
Playback,
RundownCached,
SupportedEvent,
} from 'ontime-types';
import { getFirstNormal, getNextNormal, getPreviousNormal } from 'ontime-utils'; import { getFirstNormal, getNextNormal, getPreviousNormal } from 'ontime-utils';
import { useEventAction } from '../../common/hooks/useEventAction'; import { useEventAction } from '../../common/hooks/useEventAction';
@@ -201,10 +209,13 @@ export default function Rundown({ data }: RundownProps) {
return <RundownEmpty handleAddNew={() => insertAtCursor(SupportedEvent.Event, null)} />; return <RundownEmpty handleAddNew={() => insertAtCursor(SupportedEvent.Event, null)} />;
} }
let previousEnd: null | number = null; let previousEnd: MaybeNumber = null;
let thisEnd = 0;
let previousEventId: string | undefined; let previousEventId: string | undefined;
let thisEnd: MaybeNumber = previousEnd;
let thisId = previousEventId;
let eventIndex = 0; let eventIndex = 0;
// all events before the current selected are in the past
let isPast = Boolean(featureData?.selectedEventId); let isPast = Boolean(featureData?.selectedEventId);
return ( return (
@@ -223,16 +234,16 @@ export default function Rundown({ data }: RundownProps) {
if (index === 0) { if (index === 0) {
eventIndex = 0; eventIndex = 0;
} }
let isFirstEvent = false;
if (isOntimeEvent(event)) { if (isOntimeEvent(event)) {
isFirstEvent = eventIndex === 0;
// event indexes are 1 based in frontend // event indexes are 1 based in frontend
eventIndex++; eventIndex++;
if (!isFirstEvent) { previousEnd = thisEnd;
previousEnd = thisEnd; previousEventId = thisId;
if (!event.skip) {
thisEnd = event.timeEnd;
thisId = eventId;
} }
thisEnd = event.timeEnd;
previousEventId = event.id;
} }
const isLast = index === order.length - 1; const isLast = index === order.length - 1;
const isLoaded = featureData?.selectedEventId === event.id; const isLoaded = featureData?.selectedEventId === event.id;
@@ -265,8 +276,7 @@ export default function Rundown({ data }: RundownProps) {
{((showQuickEntry && hasCursor) || isLast) && ( {((showQuickEntry && hasCursor) || isLast) && (
<QuickAddBlock <QuickAddBlock
showKbd={hasCursor} showKbd={hasCursor}
eventId={event.id} previousEventId={event.id}
previousEventId={previousEventId}
disableAddDelay={isOntimeDelay(event)} disableAddDelay={isOntimeDelay(event)}
disableAddBlock={isOntimeBlock(event)} disableAddBlock={isOntimeBlock(event)}
/> />
@@ -1,6 +1,6 @@
@use '../blockMixins' as *; @use '../blockMixins' as *;
$skip-opacity: 0.1; $skip-opacity: 0.2;
.eventBlock { .eventBlock {
@include block-styling; @include block-styling;
@@ -173,7 +173,7 @@ function TimerIcon(props: { type: TimerType; className: string }) {
return <IoTime className={className} />; return <IoTime className={className} />;
} }
if (type === TimerType.TimeToEnd) { if (type === TimerType.TimeToEnd) {
return <IoFlag className={className} /> return <IoFlag className={className} />;
} }
return <IoArrowDown className={className} />; return <IoArrowDown className={className} />;
} }
@@ -10,14 +10,14 @@ $gap-left: calc(2rem + 0.25rem + 3rem);
display: flex; display: flex;
gap: 0.5rem; gap: 0.5rem;
font-weight: 700; font-weight: 600;
} }
@mixin indicator($bg-colour) { @mixin indicator($bg-colour) {
padding: 0 0.5rem; padding: 0 0.5rem;
border-radius: 2px; border-radius: 2px;
background-color: $bg-colour; background-color: $bg-colour;
color: $ui-black; color: $ui-white;
} }
.delay { .delay {
@@ -94,8 +94,8 @@ const EventBlockPlayback = (props: EventBlockPlaybackProps) => {
aria-label='Skip event' aria-label='Skip event'
tooltip='Skip event' tooltip='Skip event'
icon={skip ? <IoRemoveCircle /> : <IoRemoveCircleOutline />} icon={skip ? <IoRemoveCircle /> : <IoRemoveCircleOutline />}
backgroundColor={skip ? '#FA5656' : undefined} backgroundColor={skip ? '#B20000' : undefined}
_hover={{ backgroundColor: skip ? '#FF7878' : '#404040' }} _hover={{ backgroundColor: '#FF7878' }}
{...tooltipProps} {...tooltipProps}
{...blockBtnStyle} {...blockBtnStyle}
clickHandler={toggleSkip} clickHandler={toggleSkip}
@@ -7,7 +7,6 @@
display: flex; display: flex;
flex-direction: column; flex-direction: column;
overflow-x: auto; overflow-x: auto;
} }
.content { .content {
@@ -49,6 +48,14 @@
text-transform: capitalize; text-transform: capitalize;
} }
.decorated {
color: var(--decorator-color, $ui-white);
background-color: var(--decorator-bg, $gray-1100);
width: fit-content;
padding: 0 0.5rem;
border-radius: 2px;
}
.delayLabel { .delayLabel {
font-size: calc(1rem - 3px); font-size: calc(1rem - 3px);
color: $ontime-delay-text; color: $ontime-delay-text;
@@ -81,17 +88,3 @@
column-gap: 1.5rem; column-gap: 1.5rem;
row-gap: 1rem; row-gap: 1rem;
} }
.decorated {
&::before{
content: '';
display: inline-block;
vertical-align: baseline;
margin-right: 0.25rem;
width: 1em;
height: 1em;
aspect-ratio: 1;
border-radius: 99px;
background-color: var(--decorator-color, $gray-1100);
}
}
@@ -6,6 +6,7 @@ import CopyTag from '../../../common/components/copy-tag/CopyTag';
import { useEventAction } from '../../../common/hooks/useEventAction'; import { useEventAction } from '../../../common/hooks/useEventAction';
import useCustomFields from '../../../common/hooks-query/useCustomFields'; import useCustomFields from '../../../common/hooks-query/useCustomFields';
import useRundown from '../../../common/hooks-query/useRundown'; import useRundown from '../../../common/hooks-query/useRundown';
import { getAccessibleColour } from '../../../common/utils/styleUtils';
import { useEventSelection } from '../useEventSelection'; import { useEventSelection } from '../useEventSelection';
import EventEditorTimes from './composite/EventEditorTimes'; import EventEditorTimes from './composite/EventEditorTimes';
@@ -105,6 +106,7 @@ export default function EventEditor() {
const key = `${event.id}-${label}`; const key = `${event.id}-${label}`;
const fieldName = `custom-${label}`; const fieldName = `custom-${label}`;
const initialValue = event.custom[label]?.value ?? ''; const initialValue = event.custom[label]?.value ?? '';
const { backgroundColor, color } = getAccessibleColour(customFields[label].colour);
return ( return (
<EventTextArea <EventTextArea
@@ -114,7 +116,7 @@ export default function EventEditor() {
initialValue={initialValue} initialValue={initialValue}
submitHandler={handleSubmit} submitHandler={handleSubmit}
className={style.decorated} className={style.decorated}
style={{ '--decorator-color': customFields[label].colour } as CSSProperties} style={{ '--decorator-bg': backgroundColor, '--decorator-color': color } as CSSProperties}
/> />
); );
})} })}
@@ -13,14 +13,13 @@ import style from './QuickAddBlock.module.scss';
interface QuickAddBlockProps { interface QuickAddBlockProps {
showKbd: boolean; showKbd: boolean;
eventId: string; previousEventId: string;
previousEventId?: string;
disableAddDelay?: boolean; disableAddDelay?: boolean;
disableAddBlock: boolean; disableAddBlock: boolean;
} }
const QuickAddBlock = (props: QuickAddBlockProps) => { const QuickAddBlock = (props: QuickAddBlockProps) => {
const { showKbd, eventId, previousEventId, disableAddDelay = true, disableAddBlock } = props; const { showKbd, previousEventId, disableAddDelay = true, disableAddBlock } = props;
const { addEvent } = useEventAction(); const { addEvent } = useEventAction();
const { emitError } = useEmitLog(); const { emitError } = useEmitLog();
@@ -43,7 +42,7 @@ const QuickAddBlock = (props: QuickAddBlockProps) => {
defaultPublic: isPublicOption, defaultPublic: isPublicOption,
startTimeIsLastEnd: startTimeIsLastEndOption, startTimeIsLastEnd: startTimeIsLastEndOption,
lastEventId: previousEventId, lastEventId: previousEventId,
after: eventId, after: previousEventId,
}; };
addEvent(newEvent, options); addEvent(newEvent, options);
break; break;
@@ -51,7 +50,7 @@ const QuickAddBlock = (props: QuickAddBlockProps) => {
case 'delay': { case 'delay': {
const options = { const options = {
lastEventId: previousEventId, lastEventId: previousEventId,
after: eventId, after: previousEventId,
}; };
addEvent({ type: SupportedEvent.Delay }, options); addEvent({ type: SupportedEvent.Delay }, options);
break; break;
@@ -59,7 +58,7 @@ const QuickAddBlock = (props: QuickAddBlockProps) => {
case 'block': { case 'block': {
const options = { const options = {
lastEventId: previousEventId, lastEventId: previousEventId,
after: eventId, after: previousEventId,
}; };
addEvent({ type: SupportedEvent.Block }, options); addEvent({ type: SupportedEvent.Block }, options);
break; break;
@@ -70,7 +69,7 @@ const QuickAddBlock = (props: QuickAddBlockProps) => {
} }
} }
}, },
[previousEventId, eventId, addEvent, emitError], [previousEventId, addEvent, emitError],
); );
return ( return (
+1 -1
View File
@@ -277,7 +277,7 @@ export async function previewSpreadsheet(req: Request, res: Response) {
} }
const options = JSON.parse(req.body.options); const options = JSON.parse(req.body.options);
const data = handleMaybeExcel(filePath, options); const { data } = handleMaybeExcel(filePath, options);
res.status(200).send(data); res.status(200).send(data);
} catch (error) { } catch (error) {
res.status(500).send({ message: String(error) }); res.status(500).send({ message: String(error) });
@@ -68,7 +68,7 @@ export async function addEvent(
notifyChanges({ timer: [newEvent.id], external: true }); notifyChanges({ timer: [newEvent.id], external: true });
// notify runtime that rundown size has changed // notify runtime that rundown has changed
updateRuntimeOnChange(); updateRuntimeOnChange();
return newEvent; return newEvent;
@@ -84,7 +84,7 @@ export async function deleteEvent(eventId: string) {
notifyChanges({ timer: [eventId], external: true }); notifyChanges({ timer: [eventId], external: true });
// notify event loader that rundown size has changed // notify event loader that rundown has changed
updateRuntimeOnChange(); updateRuntimeOnChange();
} }
@@ -114,6 +114,9 @@ export async function editEvent(patch: Partial<OntimeEvent> | Partial<OntimeBloc
notifyChanges({ timer: [patch.id], external: true }); notifyChanges({ timer: [patch.id], external: true });
// notify event loader that rundown has changed
updateRuntimeOnChange();
return newEvent; return newEvent;
} }
@@ -127,6 +130,9 @@ export async function batchEditEvents(ids: string[], data: Partial<OntimeEvent>)
await scopedMutation({ patch: data, eventIds: ids }); await scopedMutation({ patch: data, eventIds: ids });
notifyChanges({ timer: ids, external: true }); notifyChanges({ timer: ids, external: true });
// notify event loader that rundown has changed
updateRuntimeOnChange();
} }
/** /**
@@ -141,6 +147,9 @@ export async function reorderEvent(eventId: string, from: number, to: number) {
notifyChanges({ timer: true, external: true }); notifyChanges({ timer: true, external: true });
// notify event loader that rundown has changed
updateRuntimeOnChange();
return reorderedItem; return reorderedItem;
} }
@@ -162,6 +171,9 @@ export async function swapEvents(from: string, to: string) {
await scopedMutation({ fromId: from, toId: to }); await scopedMutation({ fromId: from, toId: to });
notifyChanges({ timer: true, external: true }); notifyChanges({ timer: true, external: true });
// notify event loader that rundown has changed
updateRuntimeOnChange();
} }
/** /**
@@ -169,7 +181,8 @@ export async function swapEvents(from: string, to: string) {
* Called when we make changes to the rundown object * Called when we make changes to the rundown object
*/ */
function updateRuntimeOnChange() { function updateRuntimeOnChange() {
updateRundownData(getPlayableEvents()); // schedule an update for the end of the event loop
setImmediate(() => updateRundownData(getPlayableEvents()));
} }
/** /**
@@ -180,10 +193,8 @@ export function notifyChanges(options: { timer?: boolean | string[]; external?:
const playableEvents = getPlayableEvents(); const playableEvents = getPlayableEvents();
// notify timer service of changed events // notify timer service of changed events
// timer can be true or an array of changed IDs // timer can be true or an array of changed IDs
if (Array.isArray(options.timer)) { const affected = Array.isArray(options.timer) ? options.timer : undefined;
runtimeService.maybeUpdate(playableEvents, options.timer); runtimeService.maybeUpdate(playableEvents, affected);
}
runtimeService.maybeUpdate(playableEvents);
} }
if (options.external) { if (options.external) {
@@ -8,7 +8,7 @@ export function getLink(currentIndex: number, rundown: OntimeRundown): OntimeEve
// currently the link is the previous event // currently the link is the previous event
for (let i = currentIndex - 1; i >= 0; i--) { for (let i = currentIndex - 1; i >= 0; i--) {
const event = rundown[i]; const event = rundown[i];
if (isOntimeEvent(event)) { if (isOntimeEvent(event) && !event.skip) {
return event; return event;
} }
} }
@@ -147,13 +147,13 @@ class RuntimeService {
*/ */
maybeUpdate(playableEvents: OntimeEvent[], affectedIds?: string[]) { maybeUpdate(playableEvents: OntimeEvent[], affectedIds?: string[]) {
const state = runtimeState.getState(); const state = runtimeState.getState();
const hasLoadedElements = state.eventNow || state.eventNext; const hasLoadedElements = state.eventNow !== null || state.eventNext !== null;
if (!hasLoadedElements) { if (!hasLoadedElements) {
return; return;
} }
// we need to reload in a few scenarios: // we need to reload in a few scenarios:
// 1. we are not confident that changes do not affect running event // 1. we are not confident that changes do not affect running event (eg. all events where changed)
const safeOption = typeof affectedIds === 'undefined'; const safeOption = typeof affectedIds === 'undefined';
// 2. the edited event is in memory (now or next) running // 2. the edited event is in memory (now or next) running
const eventInMemory = safeOption ? false : this.affectsLoaded(affectedIds); const eventInMemory = safeOption ? false : this.affectsLoaded(affectedIds);
@@ -166,15 +166,18 @@ class RuntimeService {
} }
// load stuff again, but keep running if our events still exist // load stuff again, but keep running if our events still exist
const eventNow = getEventWithId(state.eventNow.id); const eventNow = getEventWithId(state.eventNow.id);
if (eventNow) { const onlyChangedNow = affectedIds?.length === 1 && affectedIds.at(0) === eventNow.id;
if (onlyChangedNow) {
runtimeState.reload(eventNow); runtimeState.reload(eventNow);
} else {
runtimeState.reloadAll(eventNow, playableEvents);
} }
return; return;
} }
// Maybe the event will become the next
isNext = this.isNewNext(); isNext = this.isNewNext();
if (isNext) { if (isNext) {
// TODO: do i need to load here?
runtimeState.loadNext(playableEvents); runtimeState.loadNext(playableEvents);
} }
} }
+20 -1
View File
@@ -120,6 +120,11 @@ export function updateRundownData(playableRundown: OntimeEvent[]) {
runtimeState.runtime.plannedStart = firstEvent?.timeStart ?? null; runtimeState.runtime.plannedStart = firstEvent?.timeStart ?? null;
runtimeState.runtime.plannedEnd = lastEvent?.timeEnd ?? null; runtimeState.runtime.plannedEnd = lastEvent?.timeEnd ?? null;
if (runtimeState.runtime.plannedEnd === null) {
runtimeState.runtime.expectedEnd = null;
} else {
runtimeState.runtime.expectedEnd = (runtimeState.runtime.plannedEnd + runtimeState.runtime.offset) % dayInMs;
}
} }
/** /**
@@ -255,6 +260,18 @@ export function reload(event?: OntimeEvent) {
runtimeState.timer.expectedFinish = getExpectedFinish(runtimeState); runtimeState.timer.expectedFinish = getExpectedFinish(runtimeState);
} }
/**
* Used in situations when we want to reload all events
* without interrupting timer
* @param eventNow
* @param playableEvents
*/
export function reloadAll(eventNow: OntimeEvent, playableEvents: OntimeEvent[]) {
loadNow(eventNow, playableEvents);
loadNext(playableEvents);
reload(eventNow);
}
export function start(state: RuntimeState = runtimeState): boolean { export function start(state: RuntimeState = runtimeState): boolean {
if (state.eventNow === null) { if (state.eventNow === null) {
return false; return false;
@@ -307,8 +324,10 @@ export function stop(state: RuntimeState = runtimeState): boolean {
if (state.timer.playback === Playback.Stop) { if (state.timer.playback === Playback.Stop) {
return false; return false;
} }
runtimeState.runtime.actualStart = null;
clear(); clear();
runtimeState.runtime.actualStart = null;
runtimeState.runtime.expectedEnd = null;
return true; return true;
} }
@@ -31,7 +31,7 @@ export function getFirstEvent(rundown: OntimeRundownEntry[]): {
} { } {
for (let i = 0; i < rundown.length; i++) { for (let i = 0; i < rundown.length; i++) {
const firstEvent = rundown[i]; const firstEvent = rundown[i];
if (isOntimeEvent(firstEvent)) { if (isOntimeEvent(firstEvent) && !firstEvent.skip) {
return { firstEvent, firstIndex: i }; return { firstEvent, firstIndex: i };
} }
} }
@@ -54,7 +54,7 @@ export function getFirstEventNormal(
for (let i = 0; i < order.length; i++) { for (let i = 0; i < order.length; i++) {
const firstId = order[i]; const firstId = order[i];
const firstEvent = rundown[firstId]; const firstEvent = rundown[firstId];
if (isOntimeEvent(firstEvent)) { if (isOntimeEvent(firstEvent) && !firstEvent.skip) {
return { firstEvent, firstIndex: i }; return { firstEvent, firstIndex: i };
} }
} }
@@ -76,7 +76,7 @@ export function getLastEvent(rundown: OntimeRundown): {
for (let i = rundown.length - 1; i >= 0; i--) { for (let i = rundown.length - 1; i >= 0; i--) {
const lastEvent = rundown.at(i); const lastEvent = rundown.at(i);
if (isOntimeEvent(lastEvent)) { if (isOntimeEvent(lastEvent) && !lastEvent.skip) {
return { lastEvent, lastIndex: i }; return { lastEvent, lastIndex: i };
} }
} }
@@ -103,7 +103,7 @@ export function getLastEventNormal(
for (let i = order.length - 1; i >= 0; i--) { for (let i = order.length - 1; i >= 0; i--) {
const lastId = order[i]; const lastId = order[i];
const lastEvent = rundown[lastId]; const lastEvent = rundown[lastId];
if (isOntimeEvent(lastEvent)) { if (isOntimeEvent(lastEvent) && !lastEvent.skip) {
return { lastEvent, lastIndex: i }; return { lastEvent, lastIndex: i };
} }
} }