mirror of
https://github.com/cpvalente/ontime.git
synced 2026-08-08 08:53:51 +00:00
V2 align (#321)
* fix: fetch in offline environments (#295) * Add arm platforms to docker build (#297) * add arm platforms to docker build --------- Co-authored-by: Fabian Posenau <fabian@fphome.de> * fix: docker build (#298) Co-authored-by: Fabian Posenau <fabian@fphome.de> * Timer: fix too many renders error when using ?progress (#305) * Timer: fix too many renders error when using ?progress * version bump (#314) * style: small tweaks * fix: prevent actions in roll mode * chore: update external assets * style: show shortcuts in quick blocks * refactor: revert optimisations on callbacks * fun: industry dictionary * style: indicators for event features --------- Co-authored-by: Fabian Posenau <fabian.p99@gmx.de> Co-authored-by: Fabian Posenau <fabian@fphome.de> Co-authored-by: Marks Polakovs <github@markspolakovs.me>
This commit is contained in:
@@ -4,6 +4,10 @@ $input-delayed-border-color: #E69056;
|
||||
.timeInput {
|
||||
width: fit-content !important;
|
||||
|
||||
.inputButton {
|
||||
aspect-ratio: 1;
|
||||
}
|
||||
|
||||
.inputField {
|
||||
font-size: $input-font-size;
|
||||
letter-spacing: 1px;
|
||||
|
||||
@@ -174,6 +174,7 @@ export default function EventEditor() {
|
||||
name='timerType'
|
||||
value={event.timerType}
|
||||
onChange={(event) => handleChange('timerType', event.target.value)}
|
||||
variant='ontime'
|
||||
>
|
||||
<option value={TimerType.CountDown}>Count down</option>
|
||||
<option value={TimerType.CountUp}>Count up</option>
|
||||
@@ -185,6 +186,7 @@ export default function EventEditor() {
|
||||
name='endAction'
|
||||
value={event.endAction}
|
||||
onChange={(event) => handleChange('endAction', event.target.value)}
|
||||
variant='ontime'
|
||||
>
|
||||
<option value={EndAction.Continue}>Continue</option>
|
||||
<option value={EndAction.Stop}>Stop</option>
|
||||
|
||||
@@ -245,7 +245,7 @@ export default function Rundown(props: RundownProps) {
|
||||
/>
|
||||
{((showQuickEntry && index === cursor) || isLast) && (
|
||||
<QuickAddBlock
|
||||
showKbd={false}
|
||||
showKbd={index === cursor}
|
||||
eventId={entry.id}
|
||||
previousEventId={previousEventId}
|
||||
disableAddDelay={entry.type === 'delay'}
|
||||
|
||||
@@ -48,70 +48,84 @@ export default function RundownEntry(props: RundownEntryProps) {
|
||||
|
||||
// we assume the data is not changing in the lifecycle of this component
|
||||
// changes to the data would make rundown re-render, also re-rendering this component
|
||||
const actionHandler = useCallback((action: EventItemActions, payload?: number | FieldValue) => {
|
||||
switch (action) {
|
||||
case 'event': {
|
||||
const newEvent = { type: SupportedEvent.Event };
|
||||
const options = {
|
||||
startTimeIsLastEnd,
|
||||
defaultPublic,
|
||||
lastEventId: previousEventId,
|
||||
after: data.id,
|
||||
};
|
||||
addEvent(newEvent, options);
|
||||
break;
|
||||
}
|
||||
case 'delay': {
|
||||
addEvent({ type: SupportedEvent.Delay }, { after: data.id });
|
||||
break;
|
||||
}
|
||||
case 'block': {
|
||||
addEvent({ type: SupportedEvent.Block }, { after: data.id });
|
||||
break;
|
||||
}
|
||||
case 'delete': {
|
||||
if (openId === data.id) {
|
||||
removeOpenEvent();
|
||||
const actionHandler = useCallback(
|
||||
(action: EventItemActions, payload?: number | FieldValue) => {
|
||||
switch (action) {
|
||||
case 'event': {
|
||||
const newEvent = { type: SupportedEvent.Event };
|
||||
const options = {
|
||||
startTimeIsLastEnd,
|
||||
defaultPublic,
|
||||
lastEventId: previousEventId,
|
||||
after: data.id,
|
||||
};
|
||||
addEvent(newEvent, options);
|
||||
break;
|
||||
}
|
||||
deleteEvent(data.id);
|
||||
break;
|
||||
}
|
||||
case 'clone': {
|
||||
const newEvent = cloneEvent(data as OntimeEvent, data.id);
|
||||
addEvent(newEvent);
|
||||
break;
|
||||
}
|
||||
case 'update': {
|
||||
// Handles and filters update requests
|
||||
const { field, value } = payload as FieldValue;
|
||||
const newData: Partial<OntimeEvent> = { id: data.id };
|
||||
case 'delay': {
|
||||
addEvent({ type: SupportedEvent.Delay }, { after: data.id });
|
||||
break;
|
||||
}
|
||||
case 'block': {
|
||||
addEvent({ type: SupportedEvent.Block }, { after: data.id });
|
||||
break;
|
||||
}
|
||||
case 'delete': {
|
||||
if (openId === data.id) {
|
||||
removeOpenEvent();
|
||||
}
|
||||
deleteEvent(data.id);
|
||||
break;
|
||||
}
|
||||
case 'clone': {
|
||||
const newEvent = cloneEvent(data as OntimeEvent, data.id);
|
||||
addEvent(newEvent);
|
||||
break;
|
||||
}
|
||||
case 'update': {
|
||||
// Handles and filters update requests
|
||||
const { field, value } = payload as FieldValue;
|
||||
const newData: Partial<OntimeEvent> = { id: data.id };
|
||||
|
||||
if (field === 'durationOverride' && data.type === SupportedEvent.Event) {
|
||||
// duration defines timeEnd
|
||||
newData.duration = value as number;
|
||||
newData.timeEnd = data.timeStart + (value as number);
|
||||
updateEvent(newData);
|
||||
} else if (field === 'timeStart' && data.type === SupportedEvent.Event) {
|
||||
newData.duration = calculateDuration(value as number, data.timeEnd);
|
||||
newData.timeStart = value as number;
|
||||
updateEvent(newData);
|
||||
} else if (field === 'timeEnd' && data.type === SupportedEvent.Event) {
|
||||
newData.duration = calculateDuration(data.timeStart, value as number);
|
||||
newData.timeEnd = value as number;
|
||||
updateEvent(newData);
|
||||
} else if (field in data) {
|
||||
// @ts-expect-error not sure how to type this
|
||||
newData[field] = value;
|
||||
updateEvent(newData);
|
||||
} else {
|
||||
emitError(`Unknown field: ${field}`);
|
||||
if (field === 'durationOverride' && data.type === SupportedEvent.Event) {
|
||||
// duration defines timeEnd
|
||||
newData.duration = value as number;
|
||||
newData.timeEnd = data.timeStart + (value as number);
|
||||
updateEvent(newData);
|
||||
} else if (field === 'timeStart' && data.type === SupportedEvent.Event) {
|
||||
newData.duration = calculateDuration(value as number, data.timeEnd);
|
||||
newData.timeStart = value as number;
|
||||
updateEvent(newData);
|
||||
} else if (field === 'timeEnd' && data.type === SupportedEvent.Event) {
|
||||
newData.duration = calculateDuration(data.timeStart, value as number);
|
||||
newData.timeEnd = value as number;
|
||||
updateEvent(newData);
|
||||
} else if (field in data) {
|
||||
// @ts-expect-error not sure how to type this
|
||||
newData[field] = value;
|
||||
updateEvent(newData);
|
||||
} else {
|
||||
emitError(`Unknown field: ${field}`);
|
||||
}
|
||||
break;
|
||||
}
|
||||
break;
|
||||
default:
|
||||
throw new Error(`Unhandled event ${action}`);
|
||||
}
|
||||
default:
|
||||
throw new Error(`Unhandled event ${action}`);
|
||||
}
|
||||
}, []);
|
||||
},
|
||||
[
|
||||
addEvent,
|
||||
data,
|
||||
defaultPublic,
|
||||
deleteEvent,
|
||||
emitError,
|
||||
openId,
|
||||
previousEventId,
|
||||
removeOpenEvent,
|
||||
startTimeIsLastEnd,
|
||||
updateEvent,
|
||||
],
|
||||
);
|
||||
|
||||
if (data.type === SupportedEvent.Event) {
|
||||
return (
|
||||
@@ -123,6 +137,8 @@ export default function RundownEntry(props: RundownEntryProps) {
|
||||
eventIndex={eventIndex + 1}
|
||||
eventId={data.id}
|
||||
isPublic={data.isPublic}
|
||||
endAction={data.endAction}
|
||||
timerType={data.timerType}
|
||||
title={data.title}
|
||||
note={data.note}
|
||||
delay={delay}
|
||||
|
||||
@@ -154,15 +154,24 @@
|
||||
grid-area: status;
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
|
||||
.tag {
|
||||
font-size: 0.55em;
|
||||
color: $active-indicator;
|
||||
}
|
||||
|
||||
.statusIcon {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
color: $gray-1000;
|
||||
color: $gray-500;
|
||||
}
|
||||
|
||||
.statusIcon.active {
|
||||
color: $ui-white;
|
||||
color: $active-indicator;
|
||||
}
|
||||
.statusIcon.disabled {
|
||||
color: $gray-1000;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@ import { useEffect, useLayoutEffect, useRef, useState } from 'react';
|
||||
import { useSortable } from '@dnd-kit/sortable';
|
||||
import { CSS } from '@dnd-kit/utilities';
|
||||
import { IoReorderTwo } from '@react-icons/all-files/io5/IoReorderTwo';
|
||||
import { OntimeEvent, Playback } from 'ontime-types';
|
||||
import { EndAction, OntimeEvent, Playback, TimerType } from 'ontime-types';
|
||||
|
||||
import { useCursor } from '../../../common/stores/cursorStore';
|
||||
import { useEventEditorStore } from '../../../common/stores/eventEditor';
|
||||
@@ -21,6 +21,8 @@ interface EventBlockProps {
|
||||
eventIndex: number;
|
||||
eventId: string;
|
||||
isPublic: boolean;
|
||||
endAction: EndAction;
|
||||
timerType: TimerType;
|
||||
title: string;
|
||||
note: string;
|
||||
delay: number;
|
||||
@@ -51,6 +53,8 @@ export default function EventBlock(props: EventBlockProps) {
|
||||
eventIndex,
|
||||
eventId,
|
||||
isPublic = true,
|
||||
endAction,
|
||||
timerType,
|
||||
title,
|
||||
note,
|
||||
delay,
|
||||
@@ -143,6 +147,8 @@ export default function EventBlock(props: EventBlockProps) {
|
||||
duration={duration}
|
||||
eventId={eventId}
|
||||
isPublic={isPublic}
|
||||
endAction={endAction}
|
||||
timerType={timerType}
|
||||
title={title}
|
||||
note={note}
|
||||
delay={delay}
|
||||
|
||||
@@ -1,14 +1,20 @@
|
||||
import { memo, useCallback, useEffect, useState } from 'react';
|
||||
import { Editable, EditableInput, EditablePreview, Tooltip } from '@chakra-ui/react';
|
||||
import { IoCaretDownCircle } from '@react-icons/all-files/io5/IoCaretDownCircle';
|
||||
import { IoCaretUpCircle } from '@react-icons/all-files/io5/IoCaretUpCircle';
|
||||
import { IoOptions } from '@react-icons/all-files/io5/IoOptions';
|
||||
import { IoPeople } from '@react-icons/all-files/io5/IoPeople';
|
||||
import { IoPlay } from '@react-icons/all-files/io5/IoPlay';
|
||||
import { IoPlayCircle } from '@react-icons/all-files/io5/IoPlayCircle';
|
||||
import { IoPlayForwardCircle } from '@react-icons/all-files/io5/IoPlayForwardCircle';
|
||||
import { IoPlayOutline } from '@react-icons/all-files/io5/IoPlayOutline';
|
||||
import { IoPlaySkipForward } from '@react-icons/all-files/io5/IoPlaySkipForward';
|
||||
import { IoPlaySkipForwardCircle } from '@react-icons/all-files/io5/IoPlaySkipForwardCircle';
|
||||
import { IoReload } from '@react-icons/all-files/io5/IoReload';
|
||||
import { IoRemoveCircle } from '@react-icons/all-files/io5/IoRemoveCircle';
|
||||
import { IoRemoveCircleOutline } from '@react-icons/all-files/io5/IoRemoveCircleOutline';
|
||||
import { Playback } from 'ontime-types';
|
||||
import { IoStopCircle } from '@react-icons/all-files/io5/IoStopCircle';
|
||||
import { IoTime } from '@react-icons/all-files/io5/IoTime';
|
||||
import { EndAction, Playback, TimerType } from 'ontime-types';
|
||||
|
||||
import TooltipActionBtn from '../../../common/components/buttons/TooltipActionBtn';
|
||||
import { useEventAction } from '../../../common/hooks/useEventAction';
|
||||
@@ -38,6 +44,8 @@ interface EventBlockInnerProps {
|
||||
duration: number;
|
||||
eventId: string;
|
||||
isPublic: boolean;
|
||||
endAction: EndAction;
|
||||
timerType: TimerType;
|
||||
title: string;
|
||||
note: string;
|
||||
delay: number;
|
||||
@@ -57,6 +65,8 @@ const EventBlockInner = (props: EventBlockInnerProps) => {
|
||||
duration,
|
||||
eventId,
|
||||
isPublic = true,
|
||||
endAction,
|
||||
timerType,
|
||||
title,
|
||||
note,
|
||||
delay,
|
||||
@@ -180,14 +190,24 @@ const EventBlockInner = (props: EventBlockInnerProps) => {
|
||||
{selected && <EventBlockProgressBar playback={playback} />}
|
||||
</div>
|
||||
<div className={style.eventStatus} tabIndex={-1}>
|
||||
<Tooltip label='Next event' isDisabled={!next} {...tooltipProps}>
|
||||
{next && (
|
||||
<Tooltip label='Next event' {...tooltipProps}>
|
||||
<span className={style.tag}>NEXT</span>
|
||||
</Tooltip>
|
||||
)}
|
||||
<Tooltip label={`End action: ${endAction}`} {...tooltipProps}>
|
||||
<span>
|
||||
<IoPlaySkipForward className={`${style.statusIcon} ${next ? style.active : ''}`} />
|
||||
<EndActionIcon action={endAction} className={style.statusIcon} />
|
||||
</span>
|
||||
</Tooltip>
|
||||
<Tooltip label={`Time type: ${timerType}`} {...tooltipProps}>
|
||||
<span>
|
||||
<TimerIcon type={timerType} className={style.statusIcon} />
|
||||
</span>
|
||||
</Tooltip>
|
||||
<Tooltip label={`${isPublic ? 'Event is public' : 'Event is private'}`} {...tooltipProps}>
|
||||
<span>
|
||||
<IoPeople className={`${style.statusIcon} ${isPublic ? style.active : ''}`} />
|
||||
<IoPeople className={`${style.statusIcon} ${isPublic ? style.active : style.disabled}`} />
|
||||
</span>
|
||||
</Tooltip>
|
||||
</div>
|
||||
@@ -212,3 +232,28 @@ const EventBlockInner = (props: EventBlockInnerProps) => {
|
||||
};
|
||||
|
||||
export default memo(EventBlockInner);
|
||||
|
||||
function EndActionIcon(props: { action: EndAction; className: string }) {
|
||||
const { action, className } = props;
|
||||
if (action === EndAction.LoadNext) {
|
||||
return <IoPlaySkipForwardCircle className={className} />;
|
||||
}
|
||||
if (action === EndAction.PlayNext) {
|
||||
return <IoPlayForwardCircle className={className} />;
|
||||
}
|
||||
if (action === EndAction.Stop) {
|
||||
return <IoStopCircle className={className} />;
|
||||
}
|
||||
return <IoPlayCircle className={className} />;
|
||||
}
|
||||
|
||||
function TimerIcon(props: { type: TimerType; className: string }) {
|
||||
const { type, className } = props;
|
||||
if (type === TimerType.CountUp) {
|
||||
return <IoCaretUpCircle className={className} />;
|
||||
}
|
||||
if (type === TimerType.Clock) {
|
||||
return <IoTime className={className} />;
|
||||
}
|
||||
return <IoCaretDownCircle className={className} />;
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@ export const ontimeSelect = {
|
||||
color: '#e2e2e2', // $gray-200
|
||||
borderRadius: '3px',
|
||||
fontWeight: '400',
|
||||
background: '#262626', // $gray-1100
|
||||
background: '#262626', // $gray-1100
|
||||
border: '1px solid transparent',
|
||||
_hover: {
|
||||
background: '#404040', // $gray-1000
|
||||
|
||||
@@ -95,6 +95,20 @@
|
||||
"!**/{mock,mocks,__mock__,__mocks__}",
|
||||
"!*{.spec.js,*.test.js,*.spec.ts,.test.ts}"
|
||||
]
|
||||
},
|
||||
{
|
||||
"from": "../server/src/preloaded-db/",
|
||||
"to": "extraResources/preloaded-db/",
|
||||
"filter": [
|
||||
"**/*"
|
||||
]
|
||||
},
|
||||
{
|
||||
"from": "../server/src/external/",
|
||||
"to": "extraResources/external/",
|
||||
"filter": [
|
||||
"**/*"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -7,7 +7,7 @@ import cors from 'cors';
|
||||
import { join, resolve } from 'path';
|
||||
|
||||
import { initSentry, reportSentryException } from './modules/sentry.js';
|
||||
import { currentDirectory, environment, isProduction, resolvedPath } from './setup.js';
|
||||
import { currentDirectory, environment, externalsStartDirectory, isProduction, resolvedPath } from './setup.js';
|
||||
import { ONTIME_VERSION } from './ONTIME_VERSION.js';
|
||||
import { OSCSettings } from 'ontime-types';
|
||||
|
||||
@@ -61,7 +61,7 @@ app.use('/ontime', ontimeRouter);
|
||||
app.use('/playback', playbackRouter);
|
||||
|
||||
// serve static - css
|
||||
app.use('/external', express.static(join(currentDirectory, 'external')));
|
||||
app.use('/external', express.static(externalsStartDirectory));
|
||||
|
||||
// serve static - react, in test mode we fetch the React app from module
|
||||
app.use(express.static(join(currentDirectory, resolvedPath())));
|
||||
|
||||
@@ -307,15 +307,17 @@ export class TimerService {
|
||||
_onFinish() {
|
||||
eventStore.set('timer', this.timer);
|
||||
integrationService.dispatch(TimerLifeCycle.onFinish);
|
||||
if (this.timer.endAction === EndAction.Stop) {
|
||||
PlaybackService.stop();
|
||||
} else if (this.timer.endAction === EndAction.LoadNext) {
|
||||
// we need to delay here to put this action in the queue stack. otherwise it won't be executed properly
|
||||
setTimeout(() => {
|
||||
PlaybackService.loadNext();
|
||||
}, 0);
|
||||
} else if (this.timer.endAction === EndAction.PlayNext) {
|
||||
PlaybackService.startNext();
|
||||
if (this.playback === Playback.Play) {
|
||||
if (this.timer.endAction === EndAction.Stop) {
|
||||
PlaybackService.stop();
|
||||
} else if (this.timer.endAction === EndAction.LoadNext) {
|
||||
// we need to delay here to put this action in the queue stack. otherwise it won't be executed properly
|
||||
setTimeout(() => {
|
||||
PlaybackService.loadNext();
|
||||
}, 0);
|
||||
} else if (this.timer.endAction === EndAction.PlayNext) {
|
||||
PlaybackService.startNext();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -66,10 +66,14 @@ if (import.meta.url) {
|
||||
// path to server src folder
|
||||
export const currentDirectory = dirname(__dirname);
|
||||
|
||||
const appPath = isTest ? '../' : getAppDataPath();
|
||||
const testDbStartDirectory = isTest ? '../' : getAppDataPath();
|
||||
export const externalsStartDirectory = isProduction ? getAppDataPath() : join(currentDirectory, 'external');
|
||||
|
||||
// path to public db
|
||||
export const resolveDbDirectory = join(appPath, isTest ? config.database.testdb : config.database.directory);
|
||||
export const resolveDbDirectory = join(
|
||||
testDbStartDirectory,
|
||||
isTest ? config.database.testdb : config.database.directory,
|
||||
);
|
||||
export const resolveDbPath = join(resolveDbDirectory, config.database.filename);
|
||||
|
||||
export const pathToStartDb = isTest
|
||||
@@ -77,7 +81,7 @@ export const pathToStartDb = isTest
|
||||
: join(currentDirectory, '/preloaded-db/', config.database.filename);
|
||||
|
||||
// path to public styles
|
||||
export const resolveStylesDirectory = join(appPath, config.styles.directory);
|
||||
export const resolveStylesDirectory = join(externalsStartDirectory, config.styles.directory);
|
||||
export const resolveStylesPath = join(resolveStylesDirectory, config.styles.filename);
|
||||
|
||||
export const pathToStartStyles = join(currentDirectory, '/external/styles/', config.styles.filename);
|
||||
|
||||
+110
-2711
File diff suppressed because it is too large
Load Diff
@@ -8,7 +8,7 @@ test('test', async ({ context }) => {
|
||||
|
||||
// stage timer message
|
||||
await editorPage.getByPlaceholder('Shown in stage timer').click();
|
||||
await expect(editorPage.getByPlaceholder('Shown in stage timer')).toBeEnabled(true);
|
||||
await expect(editorPage.getByPlaceholder('Shown in stage timer')).toBeEnabled();
|
||||
|
||||
await editorPage.getByPlaceholder('Shown in stage timer').fill('testing stage');
|
||||
await editorPage.getByRole('button', { name: /toggle timer screen message/i }).click({ timeout: 5000 });
|
||||
|
||||
Reference in New Issue
Block a user