v2 beta 4 (#361)

* ux: rename end action

* chore: update demo db

* style: tweaks on extracted rundown

* chore: prevent console logs in production code

* feat: go mode

* style: remove window size limits

* style: rename delete action

* style: small tweaks on icons

* fix: style override on params

* chore: update test db

* refactor: small code quality improvements

* refactor: prevent circular imports
This commit is contained in:
Carlos Valente
2023-04-25 22:35:14 +02:00
committed by GitHub
parent 0b94fc71a4
commit 40d389acc9
43 changed files with 545 additions and 590 deletions
+10 -3
View File
@@ -11,9 +11,16 @@
],
"overrides": [
{
"files": ["e2e/**/**.spec.ts", "e2e/**/**.test.ts"],
"extends": ["plugin:playwright/playwright-test"]
"files": [
"e2e/**/**.spec.ts",
"e2e/**/**.test.ts"
],
"extends": [
"plugin:playwright/playwright-test"
]
}
],
"rules": {}
"rules": {
"no-console": "warn"
}
}
@@ -22,7 +22,7 @@ export default function DelayInput(props: DelayInputProps) {
let ignoreChange = false;
useEffect(() => {
if (typeof duration === undefined) {
if (typeof duration === 'undefined') {
return;
}
setValue(millisToString(duration));
@@ -47,6 +47,7 @@ export const setPlayback = {
start: () => socketSendJson('start'),
pause: () => socketSendJson('pause'),
roll: () => socketSendJson('roll'),
startNext: () => socketSendJson('start-next'),
previous: () => {
socketSendJson('previous');
},
@@ -461,10 +461,10 @@ describe('millisToDelayString()', () => {
expect(millisToDelayString(0)).toBeNull();
});
describe('converts values in seconds', () => {
it(`shows a simple string with value in seconds`, () => {
it('shows a simple string with value in seconds', () => {
expect(millisToDelayString(10000)).toBe('+10 sec');
});
it(`... and its negative counterpart`, () => {
it('... and its negative counterpart', () => {
expect(millisToDelayString(-10000)).toBe('-10 sec');
});
@@ -478,16 +478,16 @@ describe('millisToDelayString()', () => {
});
describe('converts values in minutes', () => {
it(`shows a simple string with value in minutes`, () => {
it('shows a simple string with value in minutes', () => {
expect(millisToDelayString(720000)).toBe('+12 min');
});
it(`... and its negative counterpart`, () => {
it('... and its negative counterpart', () => {
expect(millisToDelayString(-720000)).toBe('-12 min');
});
it(`shows a simple string with value in minutes and seconds`, () => {
it('shows a simple string with value in minutes and seconds', () => {
expect(millisToDelayString(630000)).toBe('+00:10:30');
});
it(`... and its negative counterpart`, () => {
it('... and its negative counterpart', () => {
expect(millisToDelayString(-630000)).toBe('-00:10:30');
});
@@ -500,10 +500,10 @@ describe('millisToDelayString()', () => {
});
describe('converts values with full time string', () => {
it(`positive added time`, () => {
it('positive added time', () => {
expect(millisToDelayString(45015000)).toBe('+12:30:15');
});
it(`negative added time`, () => {
it('negative added time', () => {
expect(millisToDelayString(-45015000)).toBe('-12:30:15');
});
});
+3 -3
View File
@@ -114,13 +114,13 @@ function checkAmPm(value: string) {
* @param {string} value
*/
function checkMatchers(value: string) {
const hoursMatch = value.match(/(\d+)h/);
const hoursMatch = /(\d+)h/.exec(value);
const hoursMatchValue = hoursMatch ? parse(hoursMatch[1]) : 0;
const minutesMatch = value.match(/(\d+)m/);
const minutesMatch = /(\d+)m/.exec(value);
const minutesMatchValue = minutesMatch ? parse(minutesMatch[1]) : 0;
const secondsMatch = value.match(/(\d+)s/);
const secondsMatch = /(\d+)s/.exec(value);
const secondsMatchValue = secondsMatch ? parse(secondsMatch[1]) : 0;
if (hoursMatchValue > 0 || minutesMatchValue > 0 || secondsMatchValue > 0) {
@@ -0,0 +1,6 @@
export function isStringBoolean(text: string | null) {
if (text === null) {
return false;
}
return text?.toLowerCase() === 'true' || text === '1';
}
@@ -1,19 +0,0 @@
import { Playback } from 'ontime-types';
import PlaybackDisplay from './PlaybackDisplay';
import Transport from './Transport';
interface PlaybackButtonsProps {
playback: Playback;
noEvents: boolean;
}
export default function PlaybackButtons(props: PlaybackButtonsProps) {
const { playback, noEvents } = props;
return (
<>
<PlaybackDisplay playback={playback} noEvents={noEvents} />
<Transport playback={playback} noEvents={noEvents} />
</>
);
};
@@ -1,130 +1,4 @@
@use '../../../theme/v2Styles' as *;
@use '../../../theme/ontimeColours' as *;
@use '../../../theme/mixins' as *;
.mainContainer {
width: 100%;
display: grid;
margin: 0 auto;
gap: $element-inner-spacing;
}
.timeContainer {
display: grid;
grid-template-areas:
'ind clk clk btn'
'... sta fin btn';
grid-template-rows: 1fr auto;
grid-template-columns: 1.5em 1fr 1fr 5em;
gap: $element-inner-spacing;
justify-items: start;
}
.timer {
grid-area: clk;
white-space: nowrap;
max-width: 300px;
}
.indicators {
grid-area: ind;
width: 100%;
height: 100%;
display: flex;
flex-direction: column;
justify-content: space-evenly;
}
.indRoll,
.indDelay,
.indNegative {
background-color: $gray-1350;
}
.indRoll,
.indRollActive,
.indDelay,
.indDelayActive {
margin: 0 auto;
border-radius: 6px;
width: 12px;
height: 12px;
}
.indRollActive {
background-color: $ontime-roll;
}
.indNegative,
.indNegativeActive {
margin: 0 auto;
width: 90%;
height: 4px;
}
.indNegativeActive {
background-color: $playback-negative;
}
.indDelayActive {
background-color: $ontime-delay;
}
.btn {
grid-area: btn;
display: grid;
grid-template-columns: 1fr 1fr;
grid-template-rows: 1fr 1fr;
width: 100%;
gap: $element-inner-spacing;
}
.minus {
grid-area: min;
display: flex;
flex-direction: column;
}
.start,
.finish,
.roll {
height: 24px;
}
.start {
grid-area: sta;
}
.finish {
grid-area: fin;
}
.roll {
grid-area: 2 / 2 / 2 / 4 ;
}
.time {
color: $section-white;
font-size: $text-body-size;
}
.tag {
color: $label-gray;
font-size: 13px;
}
.rolltag {
color: $ontime-roll;
font-size: $text-body-size;
}
.playbackContainer {
display: flex;
justify-content: space-evenly;
padding-top: 0.5em;
gap: $element-spacing;
}
.invertX {
transform: rotateY(180deg);
}
@@ -2,8 +2,8 @@ import { Playback } from 'ontime-types';
import { usePlaybackControl } from '../../../common/hooks/useSocket';
import PlaybackButtons from './PlaybackButtons';
import PlaybackTimer from './PlaybackTimer';
import PlaybackButtons from './playback-buttons/PlaybackButtons';
import PlaybackTimer from './playback-timer/PlaybackTimer';
import style from './PlaybackControl.module.scss';
@@ -1,55 +0,0 @@
import { IoPause } from '@react-icons/all-files/io5/IoPause';
import { IoPlay } from '@react-icons/all-files/io5/IoPlay';
import { IoTimeOutline } from '@react-icons/all-files/io5/IoTimeOutline';
import { Playback } from 'ontime-types';
import { setPlayback } from '../../../common/hooks/useSocket';
import TapButton from './TapButton';
import style from './PlaybackControl.module.scss';
interface PlaybackProps {
playback: Playback;
noEvents: boolean;
}
export default function PlaybackDisplay(props: PlaybackProps) {
const { playback, noEvents } = props;
const isRolling = playback === Playback.Roll;
const isPlaying = playback === Playback.Play;
const isPaused = playback === Playback.Pause;
const isArmed = playback === Playback.Armed;
const isStopped = playback === Playback.Stop;
return (
<div className={style.playbackContainer}>
<TapButton
onClick={() => setPlayback.start()}
disabled={isStopped || isRolling}
theme={Playback.Play}
active={isPlaying}
>
<IoPlay />
</TapButton>
<TapButton
onClick={() => setPlayback.pause()}
disabled={isStopped || isRolling || isArmed}
theme={Playback.Pause}
active={isPaused}
>
<IoPause />
</TapButton>
<TapButton
onClick={() => setPlayback.roll()}
disabled={!isStopped || noEvents}
theme={Playback.Roll}
active={isRolling}
>
<IoTimeOutline />
</TapButton>
</div>
);
}
@@ -1,30 +0,0 @@
import { ForwardedRef, forwardRef, PropsWithChildren } from 'react';
import { Playback } from 'ontime-types';
import style from './TapButton.module.scss';
interface TapButtonProps {
disabled?: boolean;
square?: boolean;
onClick: () => void;
theme?: Playback | 'neutral';
active?: boolean;
}
const TapButton = forwardRef((props: PropsWithChildren<TapButtonProps>, ref: ForwardedRef<HTMLButtonElement> ) => {
const { children, disabled, onClick, theme = 'neutral', square, active } = props;
return (
<button
className={`${style.tapButton} ${style[theme]} ${square ? style.square : ''} ${active ? style.active : ''}`}
disabled={disabled}
type='button'
onClick={onClick}
ref={ref}
>
{children}
</button>
);
});
TapButton.displayName = "TabButton";
export default TapButton;
@@ -1,49 +0,0 @@
import { Tooltip } from '@chakra-ui/react';
import { IoPlaySkipBack } from '@react-icons/all-files/io5/IoPlaySkipBack';
import { IoPlaySkipForward } from '@react-icons/all-files/io5/IoPlaySkipForward';
import { IoReload } from '@react-icons/all-files/io5/IoReload';
import { IoStop } from '@react-icons/all-files/io5/IoStop';
import { Playback } from 'ontime-types';
import { setPlayback } from '../../../common/hooks/useSocket';
import { tooltipDelayMid } from '../../../ontimeConfig';
import TapButton from './TapButton';
import style from './PlaybackControl.module.scss';
interface TransportProps {
playback: Playback;
noEvents: boolean;
}
export default function Transport(props: TransportProps) {
const { playback, noEvents } = props;
const isRolling = playback === Playback.Roll;
const isStopped = playback === Playback.Stop;
return (
<div className={style.playbackContainer}>
<Tooltip label='Previous event' openDelay={tooltipDelayMid}>
<TapButton onClick={() => setPlayback.previous()} disabled={isRolling || noEvents}>
<IoPlaySkipBack />
</TapButton>
</Tooltip>
<Tooltip label='Next event' openDelay={tooltipDelayMid}>
<TapButton onClick={() => setPlayback.next()} disabled={isRolling || noEvents}>
<IoPlaySkipForward />
</TapButton>
</Tooltip>
<Tooltip label='Reload event' openDelay={tooltipDelayMid}>
<TapButton onClick={() => setPlayback.reload()} disabled={isStopped || isRolling}>
<IoReload className={style.invertX} />
</TapButton>
</Tooltip>
<Tooltip label='Unload Event' openDelay={tooltipDelayMid}>
<TapButton onClick={() => setPlayback.stop()} disabled={isStopped && !isRolling} theme={Playback.Stop}>
<IoStop />
</TapButton>
</Tooltip>
</div>
);
}
@@ -0,0 +1,43 @@
@use '../../../../theme/v2Styles' as *;
.buttonContainer {
padding-top: $element-spacing;
display: grid;
grid-template-areas:
"go playback"
"go transport"
"extra extra";
grid-template-rows: repeat(3, 32px);
gap: $element-spacing;
}
.go {
grid-area: go;
font-size: 5em;
}
@mixin spaced-flex {
display: flex;
justify-content: space-evenly;
gap: $element-spacing;
}
.playbackContainer {
grid-area: playback;
@include spaced-flex;
}
.transportContainer {
grid-area: transport;
@include spaced-flex;
}
.extra {
grid-area: extra;
@include spaced-flex;
}
.invertX {
transform: rotateY(180deg);
}
@@ -0,0 +1,90 @@
import { Tooltip } from '@chakra-ui/react';
import { IoPause } from '@react-icons/all-files/io5/IoPause';
import { IoPlay } from '@react-icons/all-files/io5/IoPlay';
import { IoPlaySkipBack } from '@react-icons/all-files/io5/IoPlaySkipBack';
import { IoPlaySkipForward } from '@react-icons/all-files/io5/IoPlaySkipForward';
import { IoReload } from '@react-icons/all-files/io5/IoReload';
import { IoStop } from '@react-icons/all-files/io5/IoStop';
import { IoTimeOutline } from '@react-icons/all-files/io5/IoTimeOutline';
import { Playback } from 'ontime-types';
import { setPlayback } from '../../../../common/hooks/useSocket';
import { tooltipDelayMid } from '../../../../ontimeConfig';
import TapButton from '../tap-button/TapButton';
import styles from './PlaybackButtons.module.scss';
import style from './PlaybackButtons.module.scss';
interface PlaybackButtonsProps {
playback: Playback;
noEvents: boolean;
}
export default function PlaybackButtons(props: PlaybackButtonsProps) {
const { playback, noEvents } = props;
const isRolling = playback === Playback.Roll;
const isPlaying = playback === Playback.Play;
const isPaused = playback === Playback.Pause;
const isArmed = playback === Playback.Armed;
const isStopped = playback === Playback.Stop;
return (
<div className={styles.buttonContainer}>
<TapButton disabled={isRolling} onClick={() => setPlayback.startNext()} aspect='fill' className={styles.go}>
GO
</TapButton>
<div className={style.playbackContainer}>
<TapButton
onClick={() => setPlayback.start()}
disabled={isStopped || isRolling}
theme={Playback.Play}
active={isPlaying}
>
<IoPlay />
</TapButton>
<TapButton
onClick={() => setPlayback.pause()}
disabled={isStopped || isRolling || isArmed}
theme={Playback.Pause}
active={isPaused}
>
<IoPause />
</TapButton>
</div>
<div className={style.transportContainer}>
<Tooltip label='Previous event' openDelay={tooltipDelayMid}>
<TapButton onClick={() => setPlayback.previous()} disabled={isRolling || noEvents}>
<IoPlaySkipBack />
</TapButton>
</Tooltip>
<Tooltip label='Next event' openDelay={tooltipDelayMid}>
<TapButton onClick={() => setPlayback.next()} disabled={isRolling || noEvents}>
<IoPlaySkipForward />
</TapButton>
</Tooltip>
</div>
<div className={styles.extra}>
<TapButton
onClick={() => setPlayback.roll()}
disabled={!isStopped || noEvents}
theme={Playback.Roll}
active={isRolling}
>
<IoTimeOutline />
</TapButton>
<Tooltip label='Reload event' openDelay={tooltipDelayMid}>
<TapButton onClick={() => setPlayback.reload()} disabled={isStopped || isRolling}>
<IoReload className={style.invertX} />
</TapButton>
</Tooltip>
<Tooltip label='Unload Event' openDelay={tooltipDelayMid}>
<TapButton onClick={() => setPlayback.stop()} disabled={isStopped && !isRolling} theme={Playback.Stop}>
<IoStop />
</TapButton>
</Tooltip>
</div>
</div>
);
}
@@ -0,0 +1,111 @@
@use '../../../../theme/v2Styles' as *;
@use '../../../../theme/ontimeColours' as *;
.timeContainer {
display: grid;
grid-template-areas:
'ind clk clk btn'
'... sta fin btn';
grid-template-rows: 1fr auto;
grid-template-columns: 1.5em 1fr 1fr 5em;
gap: $element-inner-spacing;
justify-items: start;
}
.timer {
grid-area: clk;
white-space: nowrap;
max-width: 300px;
}
.indicators {
grid-area: ind;
width: 100%;
height: 100%;
display: flex;
flex-direction: column;
justify-content: space-evenly;
}
.indRoll,
.indDelay,
.indNegative {
background-color: $black-10;
}
.indRoll,
.indRollActive,
.indDelay,
.indDelayActive {
margin: 0 auto;
border-radius: 6px;
width: 12px;
height: 12px;
}
.indRollActive {
background-color: $ontime-roll;
}
.indNegative,
.indNegativeActive {
margin: 0 auto;
width: 90%;
height: 4px;
}
.indNegativeActive {
background-color: $playback-negative;
}
.indDelayActive {
background-color: $ontime-delay;
}
.btn {
grid-area: btn;
display: grid;
grid-template-columns: 1fr 1fr;
grid-template-rows: 1fr 1fr;
width: 100%;
gap: $element-inner-spacing;
}
.minus {
grid-area: min;
display: flex;
flex-direction: column;
}
.start,
.finish,
.roll {
height: 24px;
}
.start {
grid-area: sta;
}
.finish {
grid-area: fin;
}
.roll {
grid-area: 2 / 2 / 2 / 4 ;
}
.time {
color: $section-white;
font-size: $text-body-size;
}
.tag {
color: $label-gray;
font-size: 13px;
}
.rolltag {
color: $ontime-roll;
font-size: $text-body-size;
}
@@ -2,14 +2,13 @@ import { Tooltip } from '@chakra-ui/react';
import { Playback } from 'ontime-types';
import { millisToString } from 'ontime-utils';
import TimerDisplay from '../../../common/components/timer-display/TimerDisplay';
import { setPlayback, useTimer } from '../../../common/hooks/useSocket';
import { millisToMinutes, millisToSeconds } from '../../../common/utils/dateConfig';
import { tooltipDelayMid } from '../../../ontimeConfig';
import TimerDisplay from '../../../../common/components/timer-display/TimerDisplay';
import { setPlayback, useTimer } from '../../../../common/hooks/useSocket';
import { millisToMinutes, millisToSeconds } from '../../../../common/utils/dateConfig';
import { tooltipDelayMid } from '../../../../ontimeConfig';
import TapButton from '../tap-button/TapButton';
import TapButton from './TapButton';
import style from './PlaybackControl.module.scss';
import style from './PlaybackTimer.module.scss';
interface PlaybackTimerProps {
playback: Playback;
@@ -36,7 +35,7 @@ export default function PlaybackTimer(props: PlaybackTimerProps) {
if (ms < 6000) {
return `${millisToSeconds(ms)} seconds`;
} else if (ms < 12000) {
return `1 minute`;
return '1 minute';
} else {
return `${millisToMinutes(ms)} minutes`;
}
@@ -87,22 +86,22 @@ export default function PlaybackTimer(props: PlaybackTimerProps) {
)}
<div className={style.btn}>
<Tooltip label='Remove 1 minute' openDelay={tooltipDelayMid} shouldWrapChildren={disableButtons}>
<TapButton onClick={() => setPlayback.delay(-1)} disabled={disableButtons} square>
<TapButton onClick={() => setPlayback.delay(-1)} disabled={disableButtons} aspect='square'>
-1
</TapButton>
</Tooltip>
<Tooltip label='Add 1 minute' openDelay={tooltipDelayMid} shouldWrapChildren={disableButtons}>
<TapButton onClick={() => setPlayback.delay(1)} disabled={disableButtons} square>
<TapButton onClick={() => setPlayback.delay(1)} disabled={disableButtons} aspect='square'>
+1
</TapButton>
</Tooltip>
<Tooltip label='Remove 5 minutes' openDelay={tooltipDelayMid} shouldWrapChildren={disableButtons}>
<TapButton onClick={() => setPlayback.delay(-5)} disabled={disableButtons} square>
<TapButton onClick={() => setPlayback.delay(-5)} disabled={disableButtons} aspect='square'>
-5
</TapButton>
</Tooltip>
<Tooltip label='Add 5 minutes' openDelay={tooltipDelayMid} shouldWrapChildren={disableButtons}>
<TapButton onClick={() => setPlayback.delay(+5)} disabled={disableButtons} square>
<TapButton onClick={() => setPlayback.delay(+5)} disabled={disableButtons} aspect='square'>
+5
</TapButton>
</Tooltip>
@@ -1,15 +1,14 @@
@use '../../../theme/v2Styles' as *;
@use '../../../theme/ontimeColours' as *;
@use '../../../../theme/v2Styles' as *;
@use '../../../../theme/ontimeColours' as *;
$button-bg-gray: $gray-1050;
$button-color-white: $gray-50;
@mixin tap-factory($theme-color) {
font-family: $ontime-font-family;
font-size: 22px;
font-size: 18px;
border-radius: $component-border-radius-md;
width: 100%;
aspect-ratio: 3/1;
transition-property: color, background-color;
transition-duration: $transition-time-feedback;
display: grid;
@@ -78,7 +77,17 @@ $button-color-white: $gray-50;
@include tap-factory($ontime-stop);
}
.tapButton.normal {
aspect-ratio: 3/1;
}
.tapButton.square {
aspect-ratio: 1;
font-size: 14px;
}
.tapButton.fill {
aspect-ratio: unset;
height: 100%;
font-size: 1.5em;
}
@@ -0,0 +1,31 @@
import { ForwardedRef, forwardRef, PropsWithChildren } from 'react';
import { Playback } from 'ontime-types';
import { cx } from '../../../../common/utils/styleUtils';
import style from './TapButton.module.scss';
interface TapButtonProps {
disabled?: boolean;
aspect?: 'normal' | 'square' | 'fill';
square?: boolean;
free?: boolean;
onClick: () => void;
theme?: Playback | 'neutral';
active?: boolean;
className?: string;
}
const TapButton = forwardRef((props: PropsWithChildren<TapButtonProps>, ref: ForwardedRef<HTMLButtonElement>) => {
const { children, disabled, onClick, theme = 'neutral', aspect = 'normal', active, className } = props;
const classes = cx([style.tapButton, className, style[theme], style[aspect], active ? style.active : null]);
return (
<button className={classes} disabled={disabled} type='button' onClick={onClick} ref={ref}>
{children}
</button>
);
});
TapButton.displayName = 'TabButton';
export default TapButton;
@@ -37,11 +37,11 @@ $playback-width: 450px;
grid-template-rows: auto 1fr;
grid-template-columns: $menu-width $rundown-width $playback-width auto;
grid-template-areas:
'sett even play info'
'sett even mess info';
'sett rundown play info'
'sett rundown mess info';
gap: 8px;
.editor,
.rundown,
.playback,
.messages,
.info,
@@ -98,7 +98,7 @@ $playback-width: 450px;
visibility: visible;
}
.editor,
.rundown,
.info,
.settings {
visibility: hidden;
@@ -117,7 +117,7 @@ $playback-width: 450px;
visibility: visible;
}
.editor,
.rundown,
.messages,
.info,
.settings {
@@ -128,7 +128,7 @@ $playback-width: 450px;
.mainContainer {
.settings,
.editor,
.rundown,
.messages,
.playback,
.info {
@@ -171,8 +171,9 @@ $playback-width: 450px;
}
}
.editor {
grid-area: even;
.rundown {
grid-area: rundown;
height: 100%;
.content {
height: calc(100% - 24px);
@@ -183,6 +184,7 @@ $playback-width: 450px;
.info {
grid-area: info;
min-width: 17em;
max-width: 800px;
.content {
display: flex;
@@ -131,7 +131,7 @@ const EventEditorTimes = (props: EventEditorTimesProps) => {
onChange={(event) => handleSubmit('endAction', event.target.value)}
variant='ontime'
>
<option value={EndAction.Continue}>Continue</option>
<option value={EndAction.None}>None</option>
<option value={EndAction.Stop}>Stop</option>
<option value={EndAction.LoadNext}>Load Next</option>
<option value={EndAction.PlayNext}>Play Next</option>
+3 -3
View File
@@ -4,7 +4,7 @@ import { FiSave } from '@react-icons/all-files/fi/FiSave';
import { FiUpload } from '@react-icons/all-files/fi/FiUpload';
import { IoExtensionPuzzle } from '@react-icons/all-files/io5/IoExtensionPuzzle';
import { IoExtensionPuzzleOutline } from '@react-icons/all-files/io5/IoExtensionPuzzleOutline';
import { IoHelpCircleOutline } from '@react-icons/all-files/io5/IoHelpCircleOutline';
import { IoHelp } from '@react-icons/all-files/io5/IoHelp';
import { IoOptions } from '@react-icons/all-files/io5/IoOptions';
import { IoPlay } from '@react-icons/all-files/io5/IoPlay';
import { IoSettingsOutline } from '@react-icons/all-files/io5/IoSettingsOutline';
@@ -30,7 +30,7 @@ interface MenuBarProps {
}
const buttonStyle = {
fontSize: '1.5em',
fontSize: '1.25em',
size: 'lg',
colorScheme: 'white',
_hover: {
@@ -155,7 +155,7 @@ export default function MenuBar(props: MenuBarProps) {
<TooltipActionBtn
{...buttonStyle}
className={isAboutOpen ? style.open : ''}
icon={<IoHelpCircleOutline />}
icon={<IoHelp />}
clickHandler={onAboutOpen}
tooltip='About'
aria-label='About'
@@ -30,6 +30,8 @@ export default function Rundown(props: RundownProps) {
const startTimeIsLastEnd = eventSettings.startTimeIsLastEnd;
const showQuickEntry = eventSettings.showQuickEntry;
const isExtracted = window.location.pathname.includes('/rundown');
// cursor
const cursor = useAppMode((state) => state.cursor);
const appMode = useAppMode((state) => state.mode);
@@ -238,6 +240,7 @@ export default function Rundown(props: RundownProps) {
previousEventId={previousEventId}
playback={isSelected ? featureData.playback : undefined}
isRolling={featureData.playback === Playback.Roll}
disableEdit={isExtracted}
/>
{((showQuickEntry && hasCursor) || isLast) && (
<QuickAddBlock
@@ -26,11 +26,23 @@ interface RundownEntryProps {
previousEventId?: string;
playback?: Playback; // we only care about this if this event is playing
isRolling: boolean; // we need to know even if not related to this event
disableEdit: boolean; // we disable edit when the window is extracted
}
export default function RundownEntry(props: RundownEntryProps) {
const { eventIndex, data, selected, hasCursor, next, delay, previousEnd, previousEventId, playback, isRolling } =
props;
const {
eventIndex,
data,
selected,
hasCursor,
next,
delay,
previousEnd,
previousEventId,
playback,
isRolling,
disableEdit,
} = props;
const { emitError } = useEmitLog();
const { addEvent, updateEvent, deleteEvent } = useEventAction();
@@ -163,6 +175,7 @@ export default function RundownEntry(props: RundownEntryProps) {
playback={playback}
isRolling={isRolling}
actionHandler={actionHandler}
disableEdit={disableEdit}
/>
);
} else if (data.type === SupportedEvent.Block) {
@@ -11,7 +11,7 @@ import style from '../editors/Editor.module.scss';
const RundownExport = () => {
return (
<Box className={style.editor} data-testid='panel-rundown'>
<Box className={style.rundown} data-testid='panel-rundown'>
<IoArrowUp className={style.corner} onClick={(event) => handleLinks(event, 'rundown')} />
<ErrorBoundary>
<RundownWrapper />
@@ -20,6 +20,7 @@ $block-cursor-color: $blue-400;
border-radius: $block-border-radius;
margin: 4px 2px;
position: relative;
color: $block-text-color;
}
@mixin block-spacing() {
@@ -7,7 +7,7 @@ import { OntimeBlock, OntimeEvent } from 'ontime-types';
import { cx } from '../../../common/utils/styleUtils';
import EditableBlockTitle from '../common/EditableBlockTitle';
import BlockActionMenu from '../event-block/composite/BlockActionMenu';
import { EventItemActions } from '../RundownEntry';
import type { EventItemActions } from '../RundownEntry';
import style from './BlockBlock.module.scss';
@@ -11,7 +11,7 @@ import DelayInput from '../../../common/components/input/delay-input/DelayInput'
import { useEventAction } from '../../../common/hooks/useEventAction';
import { cx } from '../../../common/utils/styleUtils';
import BlockActionMenu from '../event-block/composite/BlockActionMenu';
import { EventItemActions } from '../RundownEntry';
import type { EventItemActions } from '../RundownEntry';
import style from './DelayBlock.module.scss';
@@ -22,6 +22,9 @@ $skip-opacity: 0.1;
padding-right: $block-clearance;
gap: 2px;
transition-property: background-color;
transition-duration: $transition-time-feedback;
@mixin declare-overrides(){
--status-color-override: #{$gray-200};
--status-color-active-override: #{$green-400};
@@ -6,7 +6,7 @@ import { EndAction, OntimeEvent, Playback, TimerType } from 'ontime-types';
import { useAppMode } from '../../../common/stores/appModeStore';
import { cx, getAccessibleColour } from '../../../common/utils/styleUtils';
import { EventItemActions } from '../RundownEntry';
import type { EventItemActions } from '../RundownEntry';
import EventBlockInner from './EventBlockInner';
@@ -41,6 +41,7 @@ interface EventBlockProps {
value: unknown;
},
) => void;
disableEdit: boolean;
}
export default function EventBlock(props: EventBlockProps) {
@@ -65,6 +66,7 @@ export default function EventBlock(props: EventBlockProps) {
playback,
isRolling,
actionHandler,
disableEdit,
} = props;
const moveCursorTo = useAppMode((state) => state.setCursor);
@@ -164,6 +166,7 @@ export default function EventBlock(props: EventBlockProps) {
playback={playback}
isRolling={isRolling}
actionHandler={actionHandler}
disableEdit={disableEdit}
/>
)}
</div>
@@ -51,6 +51,7 @@ interface EventBlockInnerProps {
playback?: Playback;
isRolling: boolean;
actionHandler: (action: EventItemActions, payload?: any) => void;
disableEdit: boolean;
}
const EventBlockInner = (props: EventBlockInnerProps) => {
@@ -73,6 +74,7 @@ const EventBlockInner = (props: EventBlockInnerProps) => {
playback,
isRolling,
actionHandler,
disableEdit,
} = props;
const [renderInner, setRenderInner] = useState(false);
@@ -159,6 +161,7 @@ const EventBlockInner = (props: EventBlockInnerProps) => {
tabIndex={-1}
backgroundColor={isOpen ? '#2B5ABC' : undefined}
color={isOpen ? 'white' : '#f6f6f6'}
isDisabled={disableEdit}
/>
<BlockActionMenu showAdd showDelay showBlock showClone enableDelete={!selected} actionHandler={actionHandler} />
</div>
@@ -59,7 +59,7 @@ export default function BlockActionMenu(props: BlockActionMenuProps) {
)}
<MenuDivider />
<MenuItem icon={<IoTrashBinSharp />} onClick={handleDelete} isDisabled={!enableDelete} color='#D20300'>
Delete event
Delete
</MenuItem>
</MenuList>
</Menu>
@@ -36,8 +36,8 @@ export default function Clock(props: ClockProps) {
return null;
}
// get config from url: key, text, font, size, hidenav, hideovertime
// eg. http://localhost:3000/minimal?key=f00&text=fff
// get config from url: key, text, font, size, hidenav
// eg. http://localhost:3000/clock?key=f00&text=fff
// Check for user options
const userOptions: OverridableOptions = {
size: 1,
@@ -126,7 +126,6 @@ export default function Clock(props: ClockProps) {
className={`clock-view ${isMirrored ? 'mirror' : ''}`}
style={{
backgroundColor: userOptions.keyColour,
color: userOptions.textColour,
justifyContent: userOptions.justifyContent,
alignItems: userOptions.alignItems,
}}
@@ -136,6 +135,7 @@ export default function Clock(props: ClockProps) {
<div
className='clock'
style={{
color: userOptions.textColour,
fontSize: `${(89 / (clean.length - 1)) * (userOptions.size || 1)}vw`,
fontFamily: userOptions.font,
top: userOptions.top,
@@ -7,6 +7,7 @@ import NavigationMenu from '../../../common/components/navigation-menu/Navigatio
import { useRuntimeStylesheet } from '../../../common/hooks/useRuntimeStylesheet';
import { TimeManagerType } from '../../../common/models/TimeManager.type';
import { OverridableOptions } from '../../../common/models/View.types';
import { isStringBoolean } from '../../../common/utils/viewUtils';
import { formatTimerDisplay, getTimerByType } from '../common/viewerUtils';
import './MinimalTimer.scss';
@@ -33,7 +34,7 @@ export default function MinimalTimer(props: MinimalTimerProps) {
return null;
}
// get config from url: key, text, font, size, hidenav, hideovertime
// get config from url: key, text, font, size, hideovertime
// eg. http://localhost:3000/minimal?key=f00&text=fff
// Check for user options
const userOptions: OverridableOptions = {
@@ -116,19 +117,19 @@ export default function MinimalTimer(props: MinimalTimerProps) {
}
const hideOvertime = searchParams.get('hideovertime');
userOptions.hideOvertime = Boolean(hideOvertime);
userOptions.hideOvertime = isStringBoolean(hideOvertime);
const hideMessagesOverlay = searchParams.get('hidemessages');
userOptions.hideMessagesOverlay = Boolean(hideMessagesOverlay);
userOptions.hideMessagesOverlay = isStringBoolean(hideMessagesOverlay);
const hideEndMessage = searchParams.get('hideendmessage');
userOptions.hideEndMessage = Boolean(hideEndMessage);
userOptions.hideEndMessage = isStringBoolean(hideEndMessage);
const showOverlay = pres.text !== '' && pres.visible;
const isPlaying = time.playback !== Playback.Pause;
const isNegative =
(time.current ?? 0) < 0 && time.timerType !== TimerType.Clock && time.timerType !== TimerType.CountUp;
const showEndMessage = time.current < 0 && general.endMessage && !hideEndMessage;
const showEndMessage = (time.current ?? 0) < 0 && general.endMessage && !hideEndMessage;
const showFinished =
time.finished && !userOptions?.hideOvertime && (time.timerType !== TimerType.Clock || showEndMessage);
-2
View File
@@ -122,8 +122,6 @@ function createWindow() {
height: 1000,
minWidth: 525,
minHeight: 405,
maxWidth: 1920,
maxHeight: 1440,
backgroundColor: '#101010', // $gray-1350
icon: appIcon,
show: false,
@@ -82,6 +82,11 @@ export function dispatchFromAdapter(type: string, payload: unknown, source?: 'os
break;
}
case 'start-next': {
PlaybackService.startNext();
break;
}
case 'startindex': {
const eventIndex = Number(payload);
if (isNaN(eventIndex) || eventIndex <= 0) {
+1 -1
View File
@@ -5,7 +5,7 @@ export const event: Omit<OntimeEvent, 'id'> = {
subtitle: '',
presenter: '',
note: '',
endAction: EndAction.Continue,
endAction: EndAction.None,
timerType: TimerType.CountDown,
timeStart: 0,
timeEnd: 0,
+3 -3
View File
@@ -116,15 +116,15 @@ export class PlaybackService {
return true;
}
} else if (fallbackAction === 'stop') {
logger.info('PLAYBACK', `No next event found! Stopping playback`);
logger.info('PLAYBACK', 'No next event found! Stopping playback');
PlaybackService.stop();
return false;
} else if (fallbackAction === 'pause') {
logger.info('PLAYBACK', `No next event found! Pausing playback`);
logger.info('PLAYBACK', 'No next event found! Pausing playback');
PlaybackService.pause();
return false;
} else {
logger.info('PLAYBACK', `No next event found! Continuing playback`);
logger.info('PLAYBACK', 'No next event found! Continuing playback');
return false;
}
}
+3 -2
View File
@@ -371,10 +371,11 @@ export const fileHandler = async (file) => {
res.data.userFields = parseUserFields(dataFromExcel);
res.message = 'success';
} else {
console.log('Error: No sheets found named ontime or event schedule');
const errorMessage = 'No sheet found named ontime or event schedule';
console.log(errorMessage);
res = {
error: true,
message: `No sheets found named ontime or event schedule`,
message: errorMessage,
};
}
} catch (error) {
+5 -5
View File
@@ -91,7 +91,7 @@ export const parseEventData = (data, enforce): EventData => {
};
} else if (enforce) {
newEventData = { ...dbModel.eventData };
console.log(`Created event object in db`);
console.log('Created event object in db');
}
return newEventData as EventData;
};
@@ -126,7 +126,7 @@ export const parseSettings = (data, enforce): Settings => {
}
} else if (enforce) {
newSettings = dbModel.settings;
console.log(`Created settings object in db`);
console.log('Created settings object in db');
}
return newSettings as Settings;
};
@@ -153,7 +153,7 @@ export const parseViewSettings = (data, enforce): ViewSettings => {
};
} else if (enforce) {
newViews = dbModel.viewSettings;
console.log(`Created viewSettings object in db`);
console.log('Created viewSettings object in db');
}
return newViews as ViewSettings;
};
@@ -201,7 +201,7 @@ export const parseOsc = (data: { osc?: Partial<OSCSettings> }, enforce: boolean)
subscriptions: validatedSubscriptions,
};
} else if (enforce) {
console.log(`Created OSC object in db`);
console.log('Created OSC object in db');
return { ...dbModel.osc };
} else return {};
};
@@ -232,7 +232,7 @@ export const parseHttp = (data, enforce) => {
} else if (enforce) {
// @ts-expect-error -- not yet
newHttp.http = { ...dbModel.http };
console.log(`Created http object in db`);
console.log('Created http object in db');
}
return newHttp;
};
+94 -1
View File
@@ -1,5 +1,98 @@
{
"rundown": [],
"rundown": [
{
"title": "First test event",
"subtitle": "",
"presenter": "",
"note": "",
"endAction": "continue",
"timerType": "count-down",
"timeStart": 32400000,
"timeEnd": 36000000,
"duration": 3600000,
"isPublic": true,
"skip": false,
"colour": "",
"user0": "",
"user1": "",
"user2": "",
"user3": "",
"user4": "",
"user5": "",
"user6": "",
"user7": "",
"user8": "",
"user9": "",
"type": "event",
"revision": 0,
"id": "aa42f"
},
{
"duration": 600000,
"type": "delay",
"revision": 0,
"id": "b1d5a"
},
{
"title": "Second test event",
"subtitle": "",
"presenter": "",
"note": "",
"endAction": "continue",
"timerType": "count-down",
"timeStart": 36000000,
"timeEnd": 39600000,
"duration": 3600000,
"isPublic": true,
"skip": false,
"colour": "",
"user0": "",
"user1": "",
"user2": "",
"user3": "",
"user4": "",
"user5": "",
"user6": "",
"user7": "",
"user8": "",
"user9": "",
"type": "event",
"revision": 0,
"id": "d71bc"
},
{
"title": "Lunch",
"type": "block",
"id": "91682"
},
{
"title": "Third test event",
"subtitle": "",
"presenter": "",
"note": "",
"endAction": "continue",
"timerType": "count-down",
"timeStart": 39600000,
"timeEnd": 720000,
"duration": 0,
"isPublic": true,
"skip": false,
"colour": "",
"user0": "",
"user1": "",
"user2": "",
"user3": "",
"user4": "",
"user5": "",
"user6": "",
"user7": "",
"user8": "",
"user9": "",
"type": "event",
"revision": 0,
"id": "da5b4"
}
],
"eventData": {
"title": "All about Carlos demo event",
"publicUrl": "www.getontime.no",
+47 -222
View File
@@ -2,118 +2,15 @@
"rundown": [
{
"title": "Welcome to Ontime",
"subtitle": "Subtitles are useful",
"presenter": "cpvalente",
"note": "Maybe a running note for the operator?",
"timeStart": 28800000,
"timeEnd": 30600000,
"timeType": "start-end",
"duration": 1800000,
"isPublic": false,
"skip": false,
"colour": "",
"user0": "",
"user1": "",
"user2": "",
"user3": "",
"user4": "",
"user5": "",
"user6": "",
"user7": "",
"user8": "",
"user9": "",
"type": "event",
"revision": 0,
"id": "5946"
},
{
"title": "This is your event list",
"subtitle": "",
"presenter": "cpvalente",
"note": "",
"timeStart": 30600000,
"timeEnd": 34200000,
"timeType": "start-end",
"duration": 3600000,
"isPublic": false,
"skip": false,
"colour": "",
"user0": "",
"user1": "",
"user2": "",
"user3": "",
"user4": "",
"user5": "",
"user6": "",
"user7": "",
"user8": "",
"user9": "",
"type": "event",
"revision": 0,
"id": "c2e7"
},
{
"title": "Events run in the list order",
"subtitle": "",
"presenter": "",
"note": "",
"timeStart": 34200000,
"timeEnd": 34800000,
"timeType": "start-end",
"duration": 600000,
"isPublic": false,
"skip": false,
"colour": "",
"user0": "",
"user1": "",
"user2": "",
"user3": "",
"user4": "",
"user5": "",
"user6": "",
"user7": "",
"user8": "",
"user9": "",
"type": "event",
"revision": 0,
"id": "cc0f"
},
{
"title": "Unless recalled by the OSC address",
"subtitle": "",
"presenter": "",
"note": "In green, below",
"timeStart": 34800000,
"timeEnd": 35400000,
"timeType": "start-end",
"duration": 600000,
"isPublic": false,
"skip": false,
"colour": "",
"user0": "",
"user1": "",
"user2": "",
"user3": "",
"user4": "",
"user5": "",
"user6": "",
"user7": "",
"user8": "",
"user9": "",
"type": "event",
"revision": 0,
"id": "8ee5"
},
{
"title": "Use simpler times to create a timer",
"subtitle": "",
"presenter": "",
"note": "",
"note": "Ontime is an app for managing event rundowns",
"endAction": "none",
"timerType": "count-down",
"timeStart": 0,
"timeEnd": 600000,
"timeType": "start-end",
"duration": 600000,
"isPublic": false,
"isPublic": true,
"skip": false,
"colour": "",
"user0": "",
@@ -127,100 +24,12 @@
"user8": "",
"user9": "",
"type": "event",
"revision": 0,
"id": "8222"
},
{
"duration": 900000,
"type": "delay",
"revision": 0,
"id": "a386"
},
{
"title": "Add delay blocks to affect all events",
"subtitle": "",
"presenter": "",
"note": "* Until a block is found",
"timeStart": 35400000,
"timeEnd": 36600000,
"timeType": "start-end",
"duration": 1200000,
"isPublic": false,
"skip": false,
"colour": "",
"user0": "",
"user1": "",
"user2": "",
"user3": "",
"user4": "",
"user5": "",
"user6": "",
"user7": "",
"user8": "",
"user9": "",
"type": "event",
"revision": 0,
"id": "6dce"
},
{
"title": "Add and remove events with [+] and [-]",
"subtitle": "",
"presenter": "",
"note": "Orange and red buttons on the right",
"timeStart": 36600000,
"timeEnd": 43200000,
"timeType": "start-end",
"duration": 6600000,
"isPublic": false,
"skip": false,
"colour": "",
"user0": "",
"user1": "",
"user2": "",
"user3": "",
"user4": "",
"user5": "",
"user6": "",
"user7": "",
"user8": "",
"user9": "",
"type": "event",
"revision": 0,
"id": "2651"
},
{
"type": "block",
"id": "e6a1"
},
{
"title": "And control whether they are public",
"subtitle": "",
"presenter": "",
"note": "Blue button on the right",
"timeStart": 46800000,
"timeEnd": 57600000,
"timeType": "start-end",
"duration": 10800000,
"isPublic": false,
"skip": false,
"colour": "",
"user0": "",
"user1": "",
"user2": "",
"user3": "",
"user4": "",
"user5": "",
"user6": "",
"user7": "",
"user8": "",
"user9": "",
"type": "event",
"revision": 0,
"id": "1358"
"revision": 1,
"id": "5a6e1"
}
],
"eventData": {
"title": "All about Carlos demo event",
"title": "Ontime demo event",
"publicUrl": "www.getontime.no",
"publicInfo": "WiFi: demoproject \nPassword: ontimeproject",
"backstageUrl": "www.getontime.no",
@@ -229,19 +38,55 @@
},
"settings": {
"app": "ontime",
"version": 1,
"version": 2,
"serverPort": 4001,
"lock": null,
"pinCode": "1234"
"pinCode": "1234",
"timeFormat": "24"
},
"viewSettings": {
"overrideStyles": false
},
"aliases": [
{
"id": "0b0b3",
"enabled": true,
"alias": "test",
"pathAndParams": "lower?bg=ff2&text=f00&size=0.6&transition=5"
}
],
"userFields": {
"user0": "user0",
"user1": "user1",
"user2": "user2",
"user3": "user3",
"user4": "user4",
"user5": "user5",
"user6": "user6",
"user7": "user7",
"user8": "user8",
"user9": "user9"
},
"osc": {
"port": 8888,
"portIn": 8888,
"portOut": 9999,
"targetIP": "127.0.0.1",
"enabled": true
"enabledIn": true,
"enabledOut": true,
"subscriptions": {
"onLoad": [],
"onStart": [],
"onPause": [],
"onStop": [],
"onUpdate": [
{
"id": "10eea",
"enabled": true,
"message": "/ontime/update/{{timer.current}}"
}
],
"onFinish": []
}
},
"http": {
"http": {
@@ -275,25 +120,5 @@
},
"enabled": true
}
},
"aliases": [
{
"id": "0b0b3",
"enabled": true,
"alias": "test",
"pathAndParams": "lower?bg=ff2&text=f00&size=0.6&transition=5"
}
],
"userFields": {
"user0": "user0",
"user1": "user1",
"user2": "user2",
"user3": "user3",
"user4": "user4",
"user5": "user5",
"user6": "user6",
"user7": "user7",
"user8": "user8",
"user9": "user9"
}
}
@@ -1,14 +0,0 @@
import { expect, test } from '@playwright/test';
test.describe('minimal view behaviour can be changed through params', () => {
test.use({ viewport: { width: 1920, height: 1080 } });
test('without overloads', async ({ page }) => {
await page.goto('http://localhost:4001/minimal');
await page.getByTestId('minimal-timer').click();
});
test('hide nav', async ({ page }) => {
await page.goto('http://localhost:4001/minimal?hidenav=true');
const navBar = await page.locator('data-test-id=nav-logo');
await expect(navBar).toHaveCount(0);
});
});
@@ -1,5 +1,5 @@
export enum EndAction {
Continue = 'continue',
None = 'none',
Stop = 'stop',
LoadNext = 'load-next',
PlayNext = 'play-next',