* 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 {
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>
+1 -1
View File
@@ -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} />;
}
+1 -1
View File
@@ -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
+14
View File
@@ -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": [
"**/*"
]
}
]
}
+2 -2
View File
@@ -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())));
+11 -9
View File
@@ -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();
}
}
}
+7 -3
View File
@@ -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);
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 });