mirror of
https://github.com/cpvalente/ontime.git
synced 2026-08-16 04:43:35 +00:00
V2 monorepo (#285)
* refactor(project structure): UI * refactor(project structure): extract utilities * refactor(project structure): remove unused * refactor(project structure): electron * refactor(project structure): server refactor: migrate to vitest refactor: monorepo config * refactor: extract application menu * refactor: exit process * refactor: extract tray menu * chore: electron build * Added Seconds in studio clock #282 --------- Co-authored-by: Fabian Posenau <fabian@fphome.de> --------- Co-authored-by: Fabian Posenau <fabian.p99@gmx.de> Co-authored-by: Fabian Posenau <fabian@fphome.de>
This commit is contained in:
@@ -0,0 +1,28 @@
|
||||
import { Playback } from '../../../common/models/OntimeTypes';
|
||||
|
||||
import PlaybackDisplay from './PlaybackDisplay';
|
||||
import Transport from './Transport';
|
||||
|
||||
interface PlaybackButtonsProps {
|
||||
playback: Playback;
|
||||
selectedId: string | null;
|
||||
noEvents: boolean;
|
||||
}
|
||||
|
||||
export default function PlaybackButtons(props: PlaybackButtonsProps) {
|
||||
const { playback, selectedId, noEvents } = props;
|
||||
return (
|
||||
<>
|
||||
<PlaybackDisplay
|
||||
playback={playback}
|
||||
selectedId={selectedId}
|
||||
noEvents={noEvents}
|
||||
/>
|
||||
<Transport
|
||||
playback={playback}
|
||||
selectedId={selectedId}
|
||||
noEvents={noEvents}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,129 @@
|
||||
@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-1300;
|
||||
}
|
||||
|
||||
.indRoll,
|
||||
.indRollActive,
|
||||
.indDelay {
|
||||
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);
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
import { usePlaybackControl } from '../../../common/hooks/useSocket';
|
||||
import { Playback } from '../../../common/models/OntimeTypes';
|
||||
|
||||
import PlaybackButtons from './PlaybackButtons';
|
||||
import PlaybackTimer from './PlaybackTimer';
|
||||
|
||||
import style from './PlaybackControl.module.scss';
|
||||
|
||||
export default function PlaybackControl() {
|
||||
const { data } = usePlaybackControl();
|
||||
|
||||
return (
|
||||
<div className={style.mainContainer}>
|
||||
<PlaybackTimer
|
||||
playback={data.playback as Playback}
|
||||
selectedId={data.selectedEventId}
|
||||
/>
|
||||
<PlaybackButtons
|
||||
playback={data.playback}
|
||||
selectedId={data.selectedEventId}
|
||||
noEvents={data.numEvents < 1}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
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 { setPlayback } from '../../../common/hooks/useSocket';
|
||||
import { Playback } from '../../../common/models/OntimeTypes';
|
||||
|
||||
import TapButton from './TapButton';
|
||||
|
||||
import style from './PlaybackControl.module.scss';
|
||||
|
||||
interface PlaybackProps {
|
||||
playback: Playback;
|
||||
selectedId: string | null;
|
||||
noEvents: boolean;
|
||||
}
|
||||
|
||||
export default function PlaybackDisplay(props: PlaybackProps) {
|
||||
const { playback, selectedId, noEvents } = props;
|
||||
const isRolling = playback === 'roll';
|
||||
const isPlaying = playback === 'play';
|
||||
const isPaused = playback === 'pause';
|
||||
const isArmed = playback === 'armed';
|
||||
|
||||
return (
|
||||
<div className={style.playbackContainer}>
|
||||
<TapButton
|
||||
onClick={() => setPlayback.start()}
|
||||
disabled={!selectedId || isRolling}
|
||||
theme='play'
|
||||
active={isPlaying}
|
||||
>
|
||||
<IoPlay />
|
||||
</TapButton>
|
||||
|
||||
<TapButton
|
||||
onClick={() => setPlayback.pause()}
|
||||
disabled={!selectedId || isRolling || isArmed}
|
||||
theme='pause'
|
||||
active={isPaused}
|
||||
>
|
||||
<IoPause />
|
||||
</TapButton>
|
||||
|
||||
<TapButton
|
||||
onClick={() => setPlayback.roll()}
|
||||
disabled={noEvents}
|
||||
theme='roll'
|
||||
active={isRolling}
|
||||
>
|
||||
<IoTimeOutline />
|
||||
</TapButton>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
import { Tooltip } from '@chakra-ui/react';
|
||||
|
||||
import TimerDisplay from '../../../common/components/timer-display/TimerDisplay';
|
||||
import { setPlayback, useTimer } from '../../../common/hooks/useSocket';
|
||||
import { Playback } from '../../../common/models/OntimeTypes';
|
||||
import { stringFromMillis } from '../../../common/utils/time';
|
||||
import { tooltipDelayMid } from '../../../ontimeConfig';
|
||||
|
||||
import TapButton from './TapButton';
|
||||
|
||||
import style from './PlaybackControl.module.scss';
|
||||
|
||||
interface PlaybackTimerProps {
|
||||
playback: Playback;
|
||||
selectedId: string | null;
|
||||
}
|
||||
|
||||
export default function PlaybackTimer(props: PlaybackTimerProps) {
|
||||
const { playback, selectedId } = props;
|
||||
const { data: timerData } = useTimer();
|
||||
|
||||
// TODO: checkout typescript in utilities
|
||||
const started = stringFromMillis(timerData?.startedAt, true);
|
||||
const finish = stringFromMillis(timerData.expectedFinish, true);
|
||||
const isRolling = playback === 'roll';
|
||||
const isWaiting = timerData.secondaryTimer !== null && timerData.secondaryTimer > 0 && timerData.current === null;
|
||||
const disableButtons = selectedId === null || isRolling;
|
||||
const isOvertime = timerData.current !== null && timerData.current < 0;
|
||||
|
||||
return (
|
||||
<div className={style.timeContainer}>
|
||||
<div className={style.indicators}>
|
||||
<Tooltip label='Roll mode active'>
|
||||
<div className={isRolling ? style.indRollActive : style.indRoll} />
|
||||
</Tooltip>
|
||||
<div className={isOvertime ? style.indNegativeActive : style.indNegative} />
|
||||
<div className={style.indDelay} />
|
||||
</div>
|
||||
<div className={style.timer}>
|
||||
<TimerDisplay time={isWaiting ? timerData.secondaryTimer : timerData.current} small />
|
||||
</div>
|
||||
{isWaiting ? (
|
||||
<div className={style.roll}>
|
||||
<span className={style.rolltag}>Roll: Countdown to start</span>
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
<div className={style.start}>
|
||||
<span className={style.tag}>Started at </span>
|
||||
<span className={style.time}>{started}</span>
|
||||
</div>
|
||||
<div className={style.finish}>
|
||||
<span className={style.tag}>Finish at </span>
|
||||
<span className={style.time}>{finish}</span>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
<div className={style.btn}>
|
||||
<Tooltip label='Remove 1 minute' openDelay={tooltipDelayMid} shouldWrapChildren={disableButtons}>
|
||||
<TapButton onClick={() => setPlayback.delay(-1)} disabled={disableButtons} square>
|
||||
-1
|
||||
</TapButton>
|
||||
</Tooltip>
|
||||
<Tooltip label='Add 1 minute' openDelay={tooltipDelayMid} shouldWrapChildren={disableButtons}>
|
||||
<TapButton onClick={() => setPlayback.delay(1)} disabled={disableButtons} square>
|
||||
+1
|
||||
</TapButton>
|
||||
</Tooltip>
|
||||
<Tooltip label='Remove 5 minutes' openDelay={tooltipDelayMid} shouldWrapChildren={disableButtons}>
|
||||
<TapButton onClick={() => setPlayback.delay(-5)} disabled={disableButtons} square>
|
||||
-5
|
||||
</TapButton>
|
||||
</Tooltip>
|
||||
<Tooltip label='Add 5 minutes' openDelay={tooltipDelayMid} shouldWrapChildren={disableButtons}>
|
||||
<TapButton onClick={() => setPlayback.delay(+5)} disabled={disableButtons} square>
|
||||
+5
|
||||
</TapButton>
|
||||
</Tooltip>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
@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;
|
||||
border-radius: $component-border-radius-md;
|
||||
width: 100%;
|
||||
aspect-ratio: 3/1;
|
||||
transition-property: color, background-color;
|
||||
transition-duration: $transition-time-feedback;
|
||||
display: grid;
|
||||
place-content: center;
|
||||
letter-spacing: 0.5px;
|
||||
|
||||
background-color: $button-bg-gray;
|
||||
color: $theme-color;
|
||||
|
||||
&:disabled {
|
||||
cursor: not-allowed;
|
||||
opacity: $opacity-disabled;
|
||||
}
|
||||
|
||||
&:hover:not(:disabled) {
|
||||
color: $button-color-white;
|
||||
background-color: $theme-color;
|
||||
}
|
||||
|
||||
&:active:not(:disabled) {
|
||||
color: $button-bg-gray;
|
||||
background-color: $button-color-white;
|
||||
transition-property: color, background-color;
|
||||
transition-duration: $transition-time-action;
|
||||
}
|
||||
|
||||
&.active {
|
||||
background-color: $theme-color;
|
||||
color: $button-color-white;
|
||||
}
|
||||
}
|
||||
|
||||
.tapButton.neutral {
|
||||
@include tap-factory($gray-50);
|
||||
|
||||
&:hover:not(:disabled) {
|
||||
color: $gray-50;
|
||||
background-color: $gray-1000;
|
||||
}
|
||||
|
||||
&:active:not(:disabled) {
|
||||
color: $button-bg-gray;
|
||||
background-color: $button-color-white;
|
||||
transition-property: color, background-color;
|
||||
transition-duration: $transition-time-action;
|
||||
}
|
||||
}
|
||||
|
||||
.tapButton.play {
|
||||
@include tap-factory($playback-start);
|
||||
}
|
||||
|
||||
.tapButton.roll {
|
||||
@include tap-factory($ontime-roll);
|
||||
}
|
||||
|
||||
.tapButton.pause {
|
||||
@include tap-factory($ontime-paused);
|
||||
}
|
||||
|
||||
.tapButton.ontime {
|
||||
@include tap-factory($ontime-color);
|
||||
}
|
||||
|
||||
.tapButton.stop {
|
||||
@include tap-factory($ontime-stop);
|
||||
}
|
||||
|
||||
.tapButton.square {
|
||||
aspect-ratio: 1;
|
||||
font-size: 14px;
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
import { ForwardedRef, forwardRef, PropsWithChildren } from 'react';
|
||||
|
||||
import { Playback } from '../../../common/models/OntimeTypes';
|
||||
|
||||
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;
|
||||
@@ -0,0 +1,22 @@
|
||||
import { Box } from '@chakra-ui/react';
|
||||
import { IoArrowUp } from '@react-icons/all-files/io5/IoArrowUp';
|
||||
|
||||
import ErrorBoundary from '../../../common/components/error-boundary/ErrorBoundary';
|
||||
import { handleLinks } from '../../../common/utils/linkUtils';
|
||||
|
||||
import PlaybackControl from './PlaybackControl';
|
||||
|
||||
import style from '../../editors/Editor.module.scss';
|
||||
|
||||
export default function TimerControlExport() {
|
||||
return (
|
||||
<Box className={style.playback} data-testid='panel-timer-control'>
|
||||
<IoArrowUp className={style.corner} onClick={(event) => handleLinks(event, 'timercontrol')} />
|
||||
<div className={style.content}>
|
||||
<ErrorBoundary>
|
||||
<PlaybackControl />
|
||||
</ErrorBoundary>
|
||||
</div>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
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 { setPlayback } from '../../../common/hooks/useSocket';
|
||||
import { Playback } from '../../../common/models/OntimeTypes';
|
||||
import { tooltipDelayMid } from '../../../ontimeConfig';
|
||||
|
||||
import TapButton from './TapButton';
|
||||
|
||||
import style from './PlaybackControl.module.scss';
|
||||
|
||||
interface TransportProps {
|
||||
playback: Playback;
|
||||
selectedId: string | null;
|
||||
noEvents: boolean;
|
||||
}
|
||||
|
||||
export default function Transport(props: TransportProps) {
|
||||
const { playback, selectedId, noEvents } = props;
|
||||
const isRolling = playback === 'roll';
|
||||
|
||||
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={!selectedId || isRolling}
|
||||
>
|
||||
<IoReload className={style.invertX} />
|
||||
</TapButton>
|
||||
</Tooltip>
|
||||
<Tooltip label='Unload Event' openDelay={tooltipDelayMid}>
|
||||
<TapButton
|
||||
onClick={() => setPlayback.stop()}
|
||||
disabled={!selectedId && !isRolling}
|
||||
theme='stop'
|
||||
>
|
||||
<IoStop />
|
||||
</TapButton>
|
||||
</Tooltip>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user