mirror of
https://github.com/cpvalente/ontime.git
synced 2026-08-20 14:39:06 +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;
|
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;
|
||||||
|
}
|
||||||
|
|||||||
+1
@@ -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',
|
||||||
|
|||||||
@@ -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 (
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user