mirror of
https://github.com/cpvalente/ontime.git
synced 2026-08-07 08:23:55 +00:00
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:
@@ -7,4 +7,5 @@
|
||||
gap: 0.25rem;
|
||||
|
||||
overflow: hidden;
|
||||
background-color: black;
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ import { useKeyDown } from '../../common/hooks/useKeyDown';
|
||||
import AboutPanel from './panel/about-panel/AboutPanel';
|
||||
import GeneralPanel from './panel/general-panel/GeneralPanel';
|
||||
import IntegrationsPanel from './panel/integrations-panel/IntegrationsPanel';
|
||||
import InterfacePanel from './panel/interface-panel/InterfacePanel';
|
||||
import LogPanel from './panel/log-panel/LogPanel';
|
||||
import ProjectPanel from './panel/project-panel/ProjectPanel';
|
||||
import ProjectSettingsPanel from './panel/project-settings-panel/ProjectSettingsPanel';
|
||||
@@ -32,6 +33,7 @@ export default function AppSettings() {
|
||||
{selectedPanel === 'project' && <ProjectPanel />}
|
||||
{selectedPanel === 'general' && <GeneralPanel />}
|
||||
{selectedPanel === 'sources' && <SourcesPanel />}
|
||||
{selectedPanel === 'interface' && <InterfacePanel />}
|
||||
{selectedPanel === 'integrations' && <IntegrationsPanel />}
|
||||
{selectedPanel === 'project_settings' && <ProjectSettingsPanel />}
|
||||
{selectedPanel === 'about' && <AboutPanel />}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
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 style from './PanelContent.module.scss';
|
||||
@@ -14,7 +14,9 @@ export default function PanelContent(props: PropsWithChildren<PanelContentProps>
|
||||
return (
|
||||
<div className={style.contentWrapper}>
|
||||
<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 className={style.content}>{children}</div>
|
||||
</div>
|
||||
|
||||
@@ -55,7 +55,7 @@ $inner-padding: 1rem;
|
||||
}
|
||||
|
||||
.pad {
|
||||
padding: 0 1rem;
|
||||
margin: 0 2rem;
|
||||
max-height: 550px;
|
||||
overflow-y: scroll;
|
||||
}
|
||||
|
||||
@@ -58,6 +58,7 @@ export default function CheckUpdatesButton(props: CheckUpdatesButtonProps) {
|
||||
isLoading={isFetching}
|
||||
isDisabled={disableButton}
|
||||
size='sm'
|
||||
maxWidth='max-content'
|
||||
>
|
||||
Check for updates
|
||||
</Button>
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import * as Panel from '../PanelUtils';
|
||||
|
||||
import EditorSettingsForm from './EditorSettingsForm';
|
||||
import GeneralPanelForm from './GeneralPanelForm';
|
||||
import ViewSettingsForm from './ViewSettingsForm';
|
||||
|
||||
@@ -9,7 +8,6 @@ export default function GeneralPanel() {
|
||||
<>
|
||||
<Panel.Header>Settings</Panel.Header>
|
||||
<GeneralPanelForm />
|
||||
<EditorSettingsForm />
|
||||
<ViewSettingsForm />
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -91,7 +91,7 @@ export default function HttpIntegrations() {
|
||||
</Button>
|
||||
</div>
|
||||
</Panel.SubHeader>
|
||||
|
||||
<Panel.Divider />
|
||||
<Panel.Section as='form' id='http-form' onSubmit={handleSubmit(onSubmit)} onKeyDown={preventEscape}>
|
||||
{errors?.root && <Panel.Error>{errors.root.message}</Panel.Error>}
|
||||
<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}
|
||||
rightIcon={<IoAdd />}
|
||||
>
|
||||
Add
|
||||
New
|
||||
</Button>
|
||||
</div>
|
||||
</Panel.SubHeader>
|
||||
|
||||
@@ -77,7 +77,7 @@ export default function ProjectCreateForm(props: ProjectCreateFromProps) {
|
||||
</div>
|
||||
</Panel.Title>
|
||||
{error && <Panel.Error>{error}</Panel.Error>}
|
||||
<div className={style.createFormInputField}>
|
||||
<div className={style.innerColumn}>
|
||||
<label>
|
||||
Project title
|
||||
<Input
|
||||
@@ -89,8 +89,6 @@ export default function ProjectCreateForm(props: ProjectCreateFromProps) {
|
||||
{...register('title')}
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
<div className={style.createFormInputField}>
|
||||
<label>
|
||||
Project description
|
||||
<Input
|
||||
@@ -102,8 +100,6 @@ export default function ProjectCreateForm(props: ProjectCreateFromProps) {
|
||||
{...register('description')}
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
<div className={style.createFormInputField}>
|
||||
<label>
|
||||
Public info
|
||||
<Textarea
|
||||
@@ -112,11 +108,10 @@ export default function ProjectCreateForm(props: ProjectCreateFromProps) {
|
||||
maxLength={150}
|
||||
placeholder='Shows always start ontime'
|
||||
autoComplete='off'
|
||||
resize='none'
|
||||
{...register('publicInfo')}
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
<div className={style.createFormInputField}>
|
||||
<label>
|
||||
Public QR code Url
|
||||
<Input
|
||||
@@ -127,8 +122,6 @@ export default function ProjectCreateForm(props: ProjectCreateFromProps) {
|
||||
{...register('publicUrl')}
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
<div className={style.createFormInputField}>
|
||||
<label>
|
||||
Backstage info
|
||||
<Textarea
|
||||
@@ -137,11 +130,10 @@ export default function ProjectCreateForm(props: ProjectCreateFromProps) {
|
||||
maxLength={150}
|
||||
placeholder='Wi-Fi password: 1234'
|
||||
autoComplete='off'
|
||||
resize='none'
|
||||
{...register('backstageInfo')}
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
<div className={style.createFormInputField}>
|
||||
<label>
|
||||
Backstage QR code Url
|
||||
<Input
|
||||
|
||||
@@ -70,84 +70,72 @@ export default function ProjectData() {
|
||||
</Panel.SubHeader>
|
||||
<Panel.Divider />
|
||||
<Panel.Section>
|
||||
<div className={style.createFormInputField}>
|
||||
<label>
|
||||
Project title
|
||||
<Input
|
||||
variant='ontime-filled'
|
||||
size='sm'
|
||||
maxLength={50}
|
||||
placeholder='Your project name'
|
||||
autoComplete='off'
|
||||
{...register('title')}
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
<div className={style.createFormInputField}>
|
||||
<label>
|
||||
Project description
|
||||
<Input
|
||||
variant='ontime-filled'
|
||||
size='sm'
|
||||
maxLength={100}
|
||||
placeholder='Euro Love, Malmö 2024'
|
||||
autoComplete='off'
|
||||
{...register('description')}
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
<div className={style.createFormInputField}>
|
||||
<label>
|
||||
Public info
|
||||
<Textarea
|
||||
variant='ontime-filled'
|
||||
size='sm'
|
||||
maxLength={150}
|
||||
placeholder='Shows always start ontime'
|
||||
autoComplete='off'
|
||||
resize='none'
|
||||
{...register('publicInfo')}
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
<div className={style.createFormInputField}>
|
||||
<label>
|
||||
Public QR code URL
|
||||
<Input
|
||||
variant='ontime-filled'
|
||||
size='sm'
|
||||
placeholder='www.getontime.no'
|
||||
autoComplete='off'
|
||||
{...register('publicUrl')}
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
<div className={style.createFormInputField}>
|
||||
<label>
|
||||
Backstage info
|
||||
<Textarea
|
||||
variant='ontime-filled'
|
||||
size='sm'
|
||||
maxLength={150}
|
||||
placeholder='Wi-Fi password: 1234'
|
||||
autoComplete='off'
|
||||
resize='none'
|
||||
{...register('backstageInfo')}
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
<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>
|
||||
<label>
|
||||
Project title
|
||||
<Input
|
||||
variant='ontime-filled'
|
||||
size='sm'
|
||||
maxLength={50}
|
||||
placeholder='Your project name'
|
||||
autoComplete='off'
|
||||
{...register('title')}
|
||||
/>
|
||||
</label>
|
||||
<label>
|
||||
Project description
|
||||
<Input
|
||||
variant='ontime-filled'
|
||||
size='sm'
|
||||
maxLength={100}
|
||||
placeholder='Euro Love, Malmö 2024'
|
||||
autoComplete='off'
|
||||
{...register('description')}
|
||||
/>
|
||||
</label>
|
||||
<label>
|
||||
Public info
|
||||
<Textarea
|
||||
variant='ontime-filled'
|
||||
size='sm'
|
||||
maxLength={150}
|
||||
placeholder='Shows always start ontime'
|
||||
autoComplete='off'
|
||||
resize='none'
|
||||
{...register('publicInfo')}
|
||||
/>
|
||||
</label>
|
||||
<label>
|
||||
Public QR code URL
|
||||
<Input
|
||||
variant='ontime-filled'
|
||||
size='sm'
|
||||
placeholder='www.getontime.no'
|
||||
autoComplete='off'
|
||||
{...register('publicUrl')}
|
||||
/>
|
||||
</label>
|
||||
<label>
|
||||
Backstage info
|
||||
<Textarea
|
||||
variant='ontime-filled'
|
||||
size='sm'
|
||||
maxLength={150}
|
||||
placeholder='Wi-Fi password: 1234'
|
||||
autoComplete='off'
|
||||
resize='none'
|
||||
{...register('backstageInfo')}
|
||||
/>
|
||||
</label>
|
||||
<label>
|
||||
Backstage QR code URL
|
||||
<Input
|
||||
variant='ontime-filled'
|
||||
size='sm'
|
||||
placeholder='http://docs.getontime.no'
|
||||
autoComplete='off'
|
||||
{...register('backstageUrl')}
|
||||
/>
|
||||
</label>
|
||||
</Panel.Section>
|
||||
</Panel.Card>
|
||||
</Panel.Section>
|
||||
|
||||
@@ -18,10 +18,6 @@
|
||||
flex: 2;
|
||||
}
|
||||
|
||||
.createFormInputField {
|
||||
padding-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.headerButtons,
|
||||
.actionButtons,
|
||||
.createActionButtons {
|
||||
@@ -39,10 +35,6 @@
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.createContainer {
|
||||
padding: 0.5rem 0;
|
||||
}
|
||||
|
||||
.saveButton {
|
||||
text-transform: capitalize;
|
||||
}
|
||||
@@ -50,3 +42,11 @@
|
||||
.containCell {
|
||||
max-width: 400px;
|
||||
}
|
||||
|
||||
.innerColumn {
|
||||
margin: 0 2rem;
|
||||
margin-bottom: 2rem;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1em;
|
||||
}
|
||||
|
||||
+1
@@ -56,6 +56,7 @@ export default function ProjectSettingsPanel() {
|
||||
New
|
||||
</Button>
|
||||
</Panel.SubHeader>
|
||||
<Panel.Divider />
|
||||
<Panel.Section>
|
||||
<Alert status='info' variant='ontime-on-dark-info'>
|
||||
<AlertIcon />
|
||||
|
||||
@@ -13,14 +13,21 @@ export const settingPanels: Readonly<SettingsOption[]> = [
|
||||
label: 'Project',
|
||||
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',
|
||||
label: 'Project Settings',
|
||||
secondary: [{ id: 'project_settings__custom', label: 'Custom fields' }],
|
||||
},
|
||||
{ id: 'interface', label: 'Interface' },
|
||||
{ id: 'views', label: 'Views' },
|
||||
{ id: 'interface', label: 'Interface', secondary: [{ id: 'general__editor', label: 'Editor settings' }] },
|
||||
{
|
||||
id: 'sources',
|
||||
label: 'Data Sources',
|
||||
|
||||
@@ -1,7 +1,15 @@
|
||||
import { Fragment, lazy, useCallback, useEffect, useRef, useState } from 'react';
|
||||
import { closestCenter, DndContext, DragEndEvent, PointerSensor, useSensor, useSensors } from '@dnd-kit/core';
|
||||
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 { useEventAction } from '../../common/hooks/useEventAction';
|
||||
@@ -201,10 +209,13 @@ export default function Rundown({ data }: RundownProps) {
|
||||
return <RundownEmpty handleAddNew={() => insertAtCursor(SupportedEvent.Event, null)} />;
|
||||
}
|
||||
|
||||
let previousEnd: null | number = null;
|
||||
let thisEnd = 0;
|
||||
let previousEnd: MaybeNumber = null;
|
||||
let previousEventId: string | undefined;
|
||||
let thisEnd: MaybeNumber = previousEnd;
|
||||
let thisId = previousEventId;
|
||||
|
||||
let eventIndex = 0;
|
||||
// all events before the current selected are in the past
|
||||
let isPast = Boolean(featureData?.selectedEventId);
|
||||
|
||||
return (
|
||||
@@ -223,16 +234,16 @@ export default function Rundown({ data }: RundownProps) {
|
||||
if (index === 0) {
|
||||
eventIndex = 0;
|
||||
}
|
||||
let isFirstEvent = false;
|
||||
if (isOntimeEvent(event)) {
|
||||
isFirstEvent = eventIndex === 0;
|
||||
// event indexes are 1 based in frontend
|
||||
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 isLoaded = featureData?.selectedEventId === event.id;
|
||||
@@ -265,8 +276,7 @@ export default function Rundown({ data }: RundownProps) {
|
||||
{((showQuickEntry && hasCursor) || isLast) && (
|
||||
<QuickAddBlock
|
||||
showKbd={hasCursor}
|
||||
eventId={event.id}
|
||||
previousEventId={previousEventId}
|
||||
previousEventId={event.id}
|
||||
disableAddDelay={isOntimeDelay(event)}
|
||||
disableAddBlock={isOntimeBlock(event)}
|
||||
/>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
@use '../blockMixins' as *;
|
||||
|
||||
$skip-opacity: 0.1;
|
||||
$skip-opacity: 0.2;
|
||||
|
||||
.eventBlock {
|
||||
@include block-styling;
|
||||
|
||||
@@ -173,7 +173,7 @@ function TimerIcon(props: { type: TimerType; className: string }) {
|
||||
return <IoTime className={className} />;
|
||||
}
|
||||
if (type === TimerType.TimeToEnd) {
|
||||
return <IoFlag className={className} />
|
||||
return <IoFlag className={className} />;
|
||||
}
|
||||
return <IoArrowDown className={className} />;
|
||||
}
|
||||
|
||||
@@ -10,14 +10,14 @@ $gap-left: calc(2rem + 0.25rem + 3rem);
|
||||
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
font-weight: 700;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
@mixin indicator($bg-colour) {
|
||||
padding: 0 0.5rem;
|
||||
border-radius: 2px;
|
||||
background-color: $bg-colour;
|
||||
color: $ui-black;
|
||||
color: $ui-white;
|
||||
}
|
||||
|
||||
.delay {
|
||||
|
||||
@@ -94,8 +94,8 @@ const EventBlockPlayback = (props: EventBlockPlaybackProps) => {
|
||||
aria-label='Skip event'
|
||||
tooltip='Skip event'
|
||||
icon={skip ? <IoRemoveCircle /> : <IoRemoveCircleOutline />}
|
||||
backgroundColor={skip ? '#FA5656' : undefined}
|
||||
_hover={{ backgroundColor: skip ? '#FF7878' : '#404040' }}
|
||||
backgroundColor={skip ? '#B20000' : undefined}
|
||||
_hover={{ backgroundColor: '#FF7878' }}
|
||||
{...tooltipProps}
|
||||
{...blockBtnStyle}
|
||||
clickHandler={toggleSkip}
|
||||
|
||||
@@ -7,7 +7,6 @@
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow-x: auto;
|
||||
|
||||
}
|
||||
|
||||
.content {
|
||||
@@ -49,6 +48,14 @@
|
||||
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 {
|
||||
font-size: calc(1rem - 3px);
|
||||
color: $ontime-delay-text;
|
||||
@@ -81,17 +88,3 @@
|
||||
column-gap: 1.5rem;
|
||||
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 useCustomFields from '../../../common/hooks-query/useCustomFields';
|
||||
import useRundown from '../../../common/hooks-query/useRundown';
|
||||
import { getAccessibleColour } from '../../../common/utils/styleUtils';
|
||||
import { useEventSelection } from '../useEventSelection';
|
||||
|
||||
import EventEditorTimes from './composite/EventEditorTimes';
|
||||
@@ -105,6 +106,7 @@ export default function EventEditor() {
|
||||
const key = `${event.id}-${label}`;
|
||||
const fieldName = `custom-${label}`;
|
||||
const initialValue = event.custom[label]?.value ?? '';
|
||||
const { backgroundColor, color } = getAccessibleColour(customFields[label].colour);
|
||||
|
||||
return (
|
||||
<EventTextArea
|
||||
@@ -114,7 +116,7 @@ export default function EventEditor() {
|
||||
initialValue={initialValue}
|
||||
submitHandler={handleSubmit}
|
||||
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 {
|
||||
showKbd: boolean;
|
||||
eventId: string;
|
||||
previousEventId?: string;
|
||||
previousEventId: string;
|
||||
disableAddDelay?: boolean;
|
||||
disableAddBlock: boolean;
|
||||
}
|
||||
|
||||
const QuickAddBlock = (props: QuickAddBlockProps) => {
|
||||
const { showKbd, eventId, previousEventId, disableAddDelay = true, disableAddBlock } = props;
|
||||
const { showKbd, previousEventId, disableAddDelay = true, disableAddBlock } = props;
|
||||
const { addEvent } = useEventAction();
|
||||
const { emitError } = useEmitLog();
|
||||
|
||||
@@ -43,7 +42,7 @@ const QuickAddBlock = (props: QuickAddBlockProps) => {
|
||||
defaultPublic: isPublicOption,
|
||||
startTimeIsLastEnd: startTimeIsLastEndOption,
|
||||
lastEventId: previousEventId,
|
||||
after: eventId,
|
||||
after: previousEventId,
|
||||
};
|
||||
addEvent(newEvent, options);
|
||||
break;
|
||||
@@ -51,7 +50,7 @@ const QuickAddBlock = (props: QuickAddBlockProps) => {
|
||||
case 'delay': {
|
||||
const options = {
|
||||
lastEventId: previousEventId,
|
||||
after: eventId,
|
||||
after: previousEventId,
|
||||
};
|
||||
addEvent({ type: SupportedEvent.Delay }, options);
|
||||
break;
|
||||
@@ -59,7 +58,7 @@ const QuickAddBlock = (props: QuickAddBlockProps) => {
|
||||
case 'block': {
|
||||
const options = {
|
||||
lastEventId: previousEventId,
|
||||
after: eventId,
|
||||
after: previousEventId,
|
||||
};
|
||||
addEvent({ type: SupportedEvent.Block }, options);
|
||||
break;
|
||||
@@ -70,7 +69,7 @@ const QuickAddBlock = (props: QuickAddBlockProps) => {
|
||||
}
|
||||
}
|
||||
},
|
||||
[previousEventId, eventId, addEvent, emitError],
|
||||
[previousEventId, addEvent, emitError],
|
||||
);
|
||||
|
||||
return (
|
||||
|
||||
@@ -277,7 +277,7 @@ export async function previewSpreadsheet(req: Request, res: Response) {
|
||||
}
|
||||
|
||||
const options = JSON.parse(req.body.options);
|
||||
const data = handleMaybeExcel(filePath, options);
|
||||
const { data } = handleMaybeExcel(filePath, options);
|
||||
res.status(200).send(data);
|
||||
} catch (error) {
|
||||
res.status(500).send({ message: String(error) });
|
||||
|
||||
@@ -68,7 +68,7 @@ export async function addEvent(
|
||||
|
||||
notifyChanges({ timer: [newEvent.id], external: true });
|
||||
|
||||
// notify runtime that rundown size has changed
|
||||
// notify runtime that rundown has changed
|
||||
updateRuntimeOnChange();
|
||||
|
||||
return newEvent;
|
||||
@@ -84,7 +84,7 @@ export async function deleteEvent(eventId: string) {
|
||||
|
||||
notifyChanges({ timer: [eventId], external: true });
|
||||
|
||||
// notify event loader that rundown size has changed
|
||||
// notify event loader that rundown has changed
|
||||
updateRuntimeOnChange();
|
||||
}
|
||||
|
||||
@@ -114,6 +114,9 @@ export async function editEvent(patch: Partial<OntimeEvent> | Partial<OntimeBloc
|
||||
|
||||
notifyChanges({ timer: [patch.id], external: true });
|
||||
|
||||
// notify event loader that rundown has changed
|
||||
updateRuntimeOnChange();
|
||||
|
||||
return newEvent;
|
||||
}
|
||||
|
||||
@@ -127,6 +130,9 @@ export async function batchEditEvents(ids: string[], data: Partial<OntimeEvent>)
|
||||
await scopedMutation({ patch: data, eventIds: ids });
|
||||
|
||||
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 });
|
||||
|
||||
// notify event loader that rundown has changed
|
||||
updateRuntimeOnChange();
|
||||
|
||||
return reorderedItem;
|
||||
}
|
||||
|
||||
@@ -162,6 +171,9 @@ export async function swapEvents(from: string, to: string) {
|
||||
await scopedMutation({ fromId: from, toId: to });
|
||||
|
||||
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
|
||||
*/
|
||||
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();
|
||||
// notify timer service of changed events
|
||||
// timer can be true or an array of changed IDs
|
||||
if (Array.isArray(options.timer)) {
|
||||
runtimeService.maybeUpdate(playableEvents, options.timer);
|
||||
}
|
||||
runtimeService.maybeUpdate(playableEvents);
|
||||
const affected = Array.isArray(options.timer) ? options.timer : undefined;
|
||||
runtimeService.maybeUpdate(playableEvents, affected);
|
||||
}
|
||||
|
||||
if (options.external) {
|
||||
|
||||
@@ -8,7 +8,7 @@ export function getLink(currentIndex: number, rundown: OntimeRundown): OntimeEve
|
||||
// currently the link is the previous event
|
||||
for (let i = currentIndex - 1; i >= 0; i--) {
|
||||
const event = rundown[i];
|
||||
if (isOntimeEvent(event)) {
|
||||
if (isOntimeEvent(event) && !event.skip) {
|
||||
return event;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -147,13 +147,13 @@ class RuntimeService {
|
||||
*/
|
||||
maybeUpdate(playableEvents: OntimeEvent[], affectedIds?: string[]) {
|
||||
const state = runtimeState.getState();
|
||||
const hasLoadedElements = state.eventNow || state.eventNext;
|
||||
const hasLoadedElements = state.eventNow !== null || state.eventNext !== null;
|
||||
if (!hasLoadedElements) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 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';
|
||||
// 2. the edited event is in memory (now or next) running
|
||||
const eventInMemory = safeOption ? false : this.affectsLoaded(affectedIds);
|
||||
@@ -166,15 +166,18 @@ class RuntimeService {
|
||||
}
|
||||
// load stuff again, but keep running if our events still exist
|
||||
const eventNow = getEventWithId(state.eventNow.id);
|
||||
if (eventNow) {
|
||||
const onlyChangedNow = affectedIds?.length === 1 && affectedIds.at(0) === eventNow.id;
|
||||
if (onlyChangedNow) {
|
||||
runtimeState.reload(eventNow);
|
||||
} else {
|
||||
runtimeState.reloadAll(eventNow, playableEvents);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Maybe the event will become the next
|
||||
isNext = this.isNewNext();
|
||||
if (isNext) {
|
||||
// TODO: do i need to load here?
|
||||
runtimeState.loadNext(playableEvents);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -120,6 +120,11 @@ export function updateRundownData(playableRundown: OntimeEvent[]) {
|
||||
|
||||
runtimeState.runtime.plannedStart = firstEvent?.timeStart ?? 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);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 {
|
||||
if (state.eventNow === null) {
|
||||
return false;
|
||||
@@ -307,8 +324,10 @@ export function stop(state: RuntimeState = runtimeState): boolean {
|
||||
if (state.timer.playback === Playback.Stop) {
|
||||
return false;
|
||||
}
|
||||
runtimeState.runtime.actualStart = null;
|
||||
|
||||
clear();
|
||||
runtimeState.runtime.actualStart = null;
|
||||
runtimeState.runtime.expectedEnd = null;
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
@@ -31,7 +31,7 @@ export function getFirstEvent(rundown: OntimeRundownEntry[]): {
|
||||
} {
|
||||
for (let i = 0; i < rundown.length; i++) {
|
||||
const firstEvent = rundown[i];
|
||||
if (isOntimeEvent(firstEvent)) {
|
||||
if (isOntimeEvent(firstEvent) && !firstEvent.skip) {
|
||||
return { firstEvent, firstIndex: i };
|
||||
}
|
||||
}
|
||||
@@ -54,7 +54,7 @@ export function getFirstEventNormal(
|
||||
for (let i = 0; i < order.length; i++) {
|
||||
const firstId = order[i];
|
||||
const firstEvent = rundown[firstId];
|
||||
if (isOntimeEvent(firstEvent)) {
|
||||
if (isOntimeEvent(firstEvent) && !firstEvent.skip) {
|
||||
return { firstEvent, firstIndex: i };
|
||||
}
|
||||
}
|
||||
@@ -76,7 +76,7 @@ export function getLastEvent(rundown: OntimeRundown): {
|
||||
|
||||
for (let i = rundown.length - 1; i >= 0; i--) {
|
||||
const lastEvent = rundown.at(i);
|
||||
if (isOntimeEvent(lastEvent)) {
|
||||
if (isOntimeEvent(lastEvent) && !lastEvent.skip) {
|
||||
return { lastEvent, lastIndex: i };
|
||||
}
|
||||
}
|
||||
@@ -103,7 +103,7 @@ export function getLastEventNormal(
|
||||
for (let i = order.length - 1; i >= 0; i--) {
|
||||
const lastId = order[i];
|
||||
const lastEvent = rundown[lastId];
|
||||
if (isOntimeEvent(lastEvent)) {
|
||||
if (isOntimeEvent(lastEvent) && !lastEvent.skip) {
|
||||
return { lastEvent, lastIndex: i };
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user