* 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:
Carlos Valente
2023-04-03 09:03:36 +02:00
committed by GitHub
parent 03e7428348
commit 32b5ab0574
14 changed files with 297 additions and 2796 deletions
@@ -4,6 +4,10 @@ $input-delayed-border-color: #E69056;
.timeInput { .timeInput {
width: fit-content !important; width: fit-content !important;
.inputButton {
aspect-ratio: 1;
}
.inputField { .inputField {
font-size: $input-font-size; font-size: $input-font-size;
letter-spacing: 1px; letter-spacing: 1px;
@@ -174,6 +174,7 @@ export default function EventEditor() {
name='timerType' name='timerType'
value={event.timerType} value={event.timerType}
onChange={(event) => handleChange('timerType', event.target.value)} onChange={(event) => handleChange('timerType', event.target.value)}
variant='ontime'
> >
<option value={TimerType.CountDown}>Count down</option> <option value={TimerType.CountDown}>Count down</option>
<option value={TimerType.CountUp}>Count up</option> <option value={TimerType.CountUp}>Count up</option>
@@ -185,6 +186,7 @@ export default function EventEditor() {
name='endAction' name='endAction'
value={event.endAction} value={event.endAction}
onChange={(event) => handleChange('endAction', event.target.value)} onChange={(event) => handleChange('endAction', event.target.value)}
variant='ontime'
> >
<option value={EndAction.Continue}>Continue</option> <option value={EndAction.Continue}>Continue</option>
<option value={EndAction.Stop}>Stop</option> <option value={EndAction.Stop}>Stop</option>
+1 -1
View File
@@ -245,7 +245,7 @@ export default function Rundown(props: RundownProps) {
/> />
{((showQuickEntry && index === cursor) || isLast) && ( {((showQuickEntry && index === cursor) || isLast) && (
<QuickAddBlock <QuickAddBlock
showKbd={false} showKbd={index === cursor}
eventId={entry.id} eventId={entry.id}
previousEventId={previousEventId} previousEventId={previousEventId}
disableAddDelay={entry.type === 'delay'} 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 // 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 // changes to the data would make rundown re-render, also re-rendering this component
const actionHandler = useCallback((action: EventItemActions, payload?: number | FieldValue) => { const actionHandler = useCallback(
switch (action) { (action: EventItemActions, payload?: number | FieldValue) => {
case 'event': { switch (action) {
const newEvent = { type: SupportedEvent.Event }; case 'event': {
const options = { const newEvent = { type: SupportedEvent.Event };
startTimeIsLastEnd, const options = {
defaultPublic, startTimeIsLastEnd,
lastEventId: previousEventId, defaultPublic,
after: data.id, lastEventId: previousEventId,
}; after: data.id,
addEvent(newEvent, options); };
break; 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();
} }
deleteEvent(data.id); case 'delay': {
break; addEvent({ type: SupportedEvent.Delay }, { after: data.id });
} break;
case 'clone': { }
const newEvent = cloneEvent(data as OntimeEvent, data.id); case 'block': {
addEvent(newEvent); addEvent({ type: SupportedEvent.Block }, { after: data.id });
break; break;
} }
case 'update': { case 'delete': {
// Handles and filters update requests if (openId === data.id) {
const { field, value } = payload as FieldValue; removeOpenEvent();
const newData: Partial<OntimeEvent> = { id: data.id }; }
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) { if (field === 'durationOverride' && data.type === SupportedEvent.Event) {
// duration defines timeEnd // duration defines timeEnd
newData.duration = value as number; newData.duration = value as number;
newData.timeEnd = data.timeStart + (value as number); newData.timeEnd = data.timeStart + (value as number);
updateEvent(newData); updateEvent(newData);
} else if (field === 'timeStart' && data.type === SupportedEvent.Event) { } else if (field === 'timeStart' && data.type === SupportedEvent.Event) {
newData.duration = calculateDuration(value as number, data.timeEnd); newData.duration = calculateDuration(value as number, data.timeEnd);
newData.timeStart = value as number; newData.timeStart = value as number;
updateEvent(newData); updateEvent(newData);
} else if (field === 'timeEnd' && data.type === SupportedEvent.Event) { } else if (field === 'timeEnd' && data.type === SupportedEvent.Event) {
newData.duration = calculateDuration(data.timeStart, value as number); newData.duration = calculateDuration(data.timeStart, value as number);
newData.timeEnd = value as number; newData.timeEnd = value as number;
updateEvent(newData); updateEvent(newData);
} else if (field in data) { } else if (field in data) {
// @ts-expect-error not sure how to type this // @ts-expect-error not sure how to type this
newData[field] = value; newData[field] = value;
updateEvent(newData); updateEvent(newData);
} else { } else {
emitError(`Unknown field: ${field}`); 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) { if (data.type === SupportedEvent.Event) {
return ( return (
@@ -123,6 +137,8 @@ export default function RundownEntry(props: RundownEntryProps) {
eventIndex={eventIndex + 1} eventIndex={eventIndex + 1}
eventId={data.id} eventId={data.id}
isPublic={data.isPublic} isPublic={data.isPublic}
endAction={data.endAction}
timerType={data.timerType}
title={data.title} title={data.title}
note={data.note} note={data.note}
delay={delay} delay={delay}
@@ -154,15 +154,24 @@
grid-area: status; grid-area: status;
display: flex; display: flex;
justify-content: flex-end; justify-content: flex-end;
align-items: center;
gap: 8px; gap: 8px;
.tag {
font-size: 0.55em;
color: $active-indicator;
}
.statusIcon { .statusIcon {
width: 16px; width: 16px;
height: 16px; height: 16px;
color: $gray-1000; color: $gray-500;
} }
.statusIcon.active { .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 { useSortable } from '@dnd-kit/sortable';
import { CSS } from '@dnd-kit/utilities'; import { CSS } from '@dnd-kit/utilities';
import { IoReorderTwo } from '@react-icons/all-files/io5/IoReorderTwo'; 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 { useCursor } from '../../../common/stores/cursorStore';
import { useEventEditorStore } from '../../../common/stores/eventEditor'; import { useEventEditorStore } from '../../../common/stores/eventEditor';
@@ -21,6 +21,8 @@ interface EventBlockProps {
eventIndex: number; eventIndex: number;
eventId: string; eventId: string;
isPublic: boolean; isPublic: boolean;
endAction: EndAction;
timerType: TimerType;
title: string; title: string;
note: string; note: string;
delay: number; delay: number;
@@ -51,6 +53,8 @@ export default function EventBlock(props: EventBlockProps) {
eventIndex, eventIndex,
eventId, eventId,
isPublic = true, isPublic = true,
endAction,
timerType,
title, title,
note, note,
delay, delay,
@@ -143,6 +147,8 @@ export default function EventBlock(props: EventBlockProps) {
duration={duration} duration={duration}
eventId={eventId} eventId={eventId}
isPublic={isPublic} isPublic={isPublic}
endAction={endAction}
timerType={timerType}
title={title} title={title}
note={note} note={note}
delay={delay} delay={delay}
@@ -1,14 +1,20 @@
import { memo, useCallback, useEffect, useState } from 'react'; import { memo, useCallback, useEffect, useState } from 'react';
import { Editable, EditableInput, EditablePreview, Tooltip } from '@chakra-ui/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 { IoOptions } from '@react-icons/all-files/io5/IoOptions';
import { IoPeople } from '@react-icons/all-files/io5/IoPeople'; import { IoPeople } from '@react-icons/all-files/io5/IoPeople';
import { IoPlay } from '@react-icons/all-files/io5/IoPlay'; 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 { 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 { IoReload } from '@react-icons/all-files/io5/IoReload';
import { IoRemoveCircle } from '@react-icons/all-files/io5/IoRemoveCircle'; import { IoRemoveCircle } from '@react-icons/all-files/io5/IoRemoveCircle';
import { IoRemoveCircleOutline } from '@react-icons/all-files/io5/IoRemoveCircleOutline'; 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 TooltipActionBtn from '../../../common/components/buttons/TooltipActionBtn';
import { useEventAction } from '../../../common/hooks/useEventAction'; import { useEventAction } from '../../../common/hooks/useEventAction';
@@ -38,6 +44,8 @@ interface EventBlockInnerProps {
duration: number; duration: number;
eventId: string; eventId: string;
isPublic: boolean; isPublic: boolean;
endAction: EndAction;
timerType: TimerType;
title: string; title: string;
note: string; note: string;
delay: number; delay: number;
@@ -57,6 +65,8 @@ const EventBlockInner = (props: EventBlockInnerProps) => {
duration, duration,
eventId, eventId,
isPublic = true, isPublic = true,
endAction,
timerType,
title, title,
note, note,
delay, delay,
@@ -180,14 +190,24 @@ const EventBlockInner = (props: EventBlockInnerProps) => {
{selected && <EventBlockProgressBar playback={playback} />} {selected && <EventBlockProgressBar playback={playback} />}
</div> </div>
<div className={style.eventStatus} tabIndex={-1}> <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> <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> </span>
</Tooltip> </Tooltip>
<Tooltip label={`${isPublic ? 'Event is public' : 'Event is private'}`} {...tooltipProps}> <Tooltip label={`${isPublic ? 'Event is public' : 'Event is private'}`} {...tooltipProps}>
<span> <span>
<IoPeople className={`${style.statusIcon} ${isPublic ? style.active : ''}`} /> <IoPeople className={`${style.statusIcon} ${isPublic ? style.active : style.disabled}`} />
</span> </span>
</Tooltip> </Tooltip>
</div> </div>
@@ -212,3 +232,28 @@ const EventBlockInner = (props: EventBlockInnerProps) => {
}; };
export default memo(EventBlockInner); 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} />;
}
+1 -1
View File
@@ -3,7 +3,7 @@ export const ontimeSelect = {
color: '#e2e2e2', // $gray-200 color: '#e2e2e2', // $gray-200
borderRadius: '3px', borderRadius: '3px',
fontWeight: '400', fontWeight: '400',
background: '#262626', // $gray-1100 background: '#262626', // $gray-1100
border: '1px solid transparent', border: '1px solid transparent',
_hover: { _hover: {
background: '#404040', // $gray-1000 background: '#404040', // $gray-1000
+14
View File
@@ -95,6 +95,20 @@
"!**/{mock,mocks,__mock__,__mocks__}", "!**/{mock,mocks,__mock__,__mocks__}",
"!*{.spec.js,*.test.js,*.spec.ts,.test.ts}" "!*{.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": [
"**/*"
]
} }
] ]
} }
+2 -2
View File
@@ -7,7 +7,7 @@ import cors from 'cors';
import { join, resolve } from 'path'; import { join, resolve } from 'path';
import { initSentry, reportSentryException } from './modules/sentry.js'; 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 { ONTIME_VERSION } from './ONTIME_VERSION.js';
import { OSCSettings } from 'ontime-types'; import { OSCSettings } from 'ontime-types';
@@ -61,7 +61,7 @@ app.use('/ontime', ontimeRouter);
app.use('/playback', playbackRouter); app.use('/playback', playbackRouter);
// serve static - css // 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 // serve static - react, in test mode we fetch the React app from module
app.use(express.static(join(currentDirectory, resolvedPath()))); app.use(express.static(join(currentDirectory, resolvedPath())));
+11 -9
View File
@@ -307,15 +307,17 @@ export class TimerService {
_onFinish() { _onFinish() {
eventStore.set('timer', this.timer); eventStore.set('timer', this.timer);
integrationService.dispatch(TimerLifeCycle.onFinish); integrationService.dispatch(TimerLifeCycle.onFinish);
if (this.timer.endAction === EndAction.Stop) { if (this.playback === Playback.Play) {
PlaybackService.stop(); if (this.timer.endAction === EndAction.Stop) {
} else if (this.timer.endAction === EndAction.LoadNext) { PlaybackService.stop();
// we need to delay here to put this action in the queue stack. otherwise it won't be executed properly } else if (this.timer.endAction === EndAction.LoadNext) {
setTimeout(() => { // we need to delay here to put this action in the queue stack. otherwise it won't be executed properly
PlaybackService.loadNext(); setTimeout(() => {
}, 0); PlaybackService.loadNext();
} else if (this.timer.endAction === EndAction.PlayNext) { }, 0);
PlaybackService.startNext(); } else if (this.timer.endAction === EndAction.PlayNext) {
PlaybackService.startNext();
}
} }
} }
+7 -3
View File
@@ -66,10 +66,14 @@ if (import.meta.url) {
// path to server src folder // path to server src folder
export const currentDirectory = dirname(__dirname); 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 // 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 resolveDbPath = join(resolveDbDirectory, config.database.filename);
export const pathToStartDb = isTest export const pathToStartDb = isTest
@@ -77,7 +81,7 @@ export const pathToStartDb = isTest
: join(currentDirectory, '/preloaded-db/', config.database.filename); : join(currentDirectory, '/preloaded-db/', config.database.filename);
// path to public styles // 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 resolveStylesPath = join(resolveStylesDirectory, config.styles.filename);
export const pathToStartStyles = join(currentDirectory, '/external/styles/', config.styles.filename); export const pathToStartStyles = join(currentDirectory, '/external/styles/', config.styles.filename);
File diff suppressed because it is too large Load Diff
@@ -8,7 +8,7 @@ test('test', async ({ context }) => {
// stage timer message // stage timer message
await editorPage.getByPlaceholder('Shown in stage timer').click(); 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.getByPlaceholder('Shown in stage timer').fill('testing stage');
await editorPage.getByRole('button', { name: /toggle timer screen message/i }).click({ timeout: 5000 }); await editorPage.getByRole('button', { name: /toggle timer screen message/i }).click({ timeout: 5000 });