Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Timer speed UI #1299

Open
wants to merge 9 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions apps/client/src/common/components/expand/Expand.module.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
.separator {
display: flex;
align-items: center;
margin-block: 0.5rem;

&:before {
content: '';
flex: 1;
border-bottom: 1px solid $gray-1000; // same as divider
height: 1px;
}
&:after {
content: '';
flex: 1;
border-bottom: 1px solid $gray-1000; // same as divider
height: 1px;
}
}
24 changes: 24 additions & 0 deletions apps/client/src/common/components/expand/Expand.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { PropsWithChildren, useState } from 'react';
import { Button } from '@chakra-ui/react';

import style from './Expand.module.scss';

interface ExpandProps {
initialExpanded?: boolean;
}

export default function Expand(props: PropsWithChildren<ExpandProps>) {
const { children, initialExpanded } = props;
const [expanded, setExpanded] = useState(initialExpanded);

return (
<div className={style.container}>
{expanded && children}
<div className={style.separator}>
<Button size='xs' variant='ontime-ghosted-white' onClick={() => setExpanded((prev) => !prev)}>
{expanded ? 'Show less' : 'Show more'}
</Button>
</div>
</div>
);
}
15 changes: 15 additions & 0 deletions apps/client/src/common/hooks/useSocket.ts
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,21 @@ export const setAuxTimer = {
setDuration: (time: number) => socketSendJson('auxtimer', { '1': { duration: time } }),
};

export const useTimerSpeed = () => {
const featureSelector = (state: RuntimeStore) => ({
speed: state.timer.speed,
});

return useRuntimeStore(featureSelector);
};

export const setTimerSpeed = {
calculateSpeed: () => socketSendJson('calculate-speed'),
getSpeed: () => socketSendJson('get-speed'),
setSpeed: (speed: number) => socketSendJson('set-speed', speed),
resetSpeed: () => socketSendJson('reset-speed'),
};

export const useCuesheet = () => {
const featureSelector = (state: RuntimeStore) => ({
playback: state.timer.playback,
Expand Down
7 changes: 7 additions & 0 deletions apps/client/src/features/control/playback/PlaybackControl.tsx
Original file line number Diff line number Diff line change
@@ -1,17 +1,21 @@
import { Playback } from 'ontime-types';

import Expand from '../../../common/components/expand/Expand';
import { usePlaybackControl } from '../../../common/hooks/useSocket';

import AddTime from './add-time/AddTime';
import { AuxTimer } from './aux-timer/AuxTimer';
import PlaybackButtons from './playback-buttons/PlaybackButtons';
import PlaybackTimer from './playback-timer/PlaybackTimer';
import TimerSpeed from './timer-speed/TimerSpeed';

import style from './PlaybackControl.module.scss';

export default function PlaybackControl() {
const data = usePlaybackControl();

// TODO: save expanded state in local storage

return (
<div className={style.mainContainer}>
<PlaybackTimer playback={data.playback as Playback}>
Expand All @@ -23,6 +27,9 @@ export default function PlaybackControl() {
selectedEventIndex={data.selectedEventIndex}
timerPhase={data.timerPhase}
/>
<Expand initialExpanded>
<TimerSpeed />
</Expand>
<AuxTimer />
</div>
);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,6 @@
.timeContainer {
display: grid;
grid-template-areas:
'indicators timer addtime'
'status status addtime';
grid-template-rows: 1fr auto;
grid-template-areas:'indicators timer addtime';
grid-template-columns: 1.25rem 1fr 6.5rem;
gap: $element-inner-spacing;
}
Expand Down Expand Up @@ -53,14 +50,6 @@

// ---------> LABELS

.status {
grid-area: status;
height: 1.5rem;
display: flex;
gap: $section-spacing;
margin-left: 1.5rem;
}

.tag {
color: $label-gray;
font-size: calc(1rem - 2px);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { PropsWithChildren } from 'react';
import { Tooltip } from '@chakra-ui/react';
import { Playback, TimerPhase } from 'ontime-types';
import { dayInMs, millisToMinutes, millisToSeconds, millisToString } from 'ontime-utils';
import { millisToMinutes, millisToSeconds } from 'ontime-utils';

import { useTimer } from '../../../../common/hooks/useSocket';
import TimerDisplay from '../timer-display/TimerDisplay';
Expand Down Expand Up @@ -38,16 +38,22 @@ export default function PlaybackTimer(props: PropsWithChildren<PlaybackTimerProp
const { playback, children } = props;
const timer = useTimer();

const started = millisToString(timer.startedAt);
const expectedFinish = timer.expectedFinish !== null ? timer.expectedFinish % dayInMs : null;
const finish = millisToString(expectedFinish);

const isRolling = playback === Playback.Roll;
const isWaiting = timer.phase === TimerPhase.Pending;
const isOvertime = timer.phase === TimerPhase.Overtime;
const hasAddedTime = Boolean(timer.addedTime);

const rollLabel = isRolling ? 'Roll mode active' : '';
// TODO: can we remove this from the timer area?
const rollLabel = (() => {
if (!isRolling) {
return '';
}
if (isWaiting) {
return 'Roll: Countdown to start';
}

return 'Roll mode active';
})();

const addedTimeLabel = resolveAddedTimeLabel(timer.addedTime);

Expand All @@ -63,22 +69,6 @@ export default function PlaybackTimer(props: PropsWithChildren<PlaybackTimerProp
</Tooltip>
</div>
<TimerDisplay time={isWaiting ? timer.secondaryTimer : timer.current} />
<div className={style.status}>
{isWaiting ? (
<span className={style.rolltag}>Roll: Countdown to start</span>
) : (
<>
<span className={style.start}>
<span className={style.tag}>Started at</span>
<span className={style.time}>{started}</span>
</span>
<span className={style.finish}>
<span className={style.tag}>Expect end</span>
<span className={style.time}>{finish}</span>
</span>
</>
)}
</div>
{children}
</div>
);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
import { Button } from '@chakra-ui/react';
import { dayInMs, millisToString } from 'ontime-utils';

import { setTimerSpeed, useClock, useTimer } from '../../../../common/hooks/useSocket';

import style from './TimerSpeed.module.scss';

type SpeedOverridePhase = 'idle' | 'calculating' | 'applied';

interface MeetScheduleProps {
speed: number;
newSpeed: number;
}

export default function MeetSchedule(props: MeetScheduleProps) {
const { speed, newSpeed } = props;
const { setSpeed, resetSpeed } = setTimerSpeed;
const { startedAt, expectedFinish, current } = useTimer();

const handleApply = () => {
setSpeed(newSpeed);
};

const handleReset = () => {
resetSpeed();
};

const phase: SpeedOverridePhase = (() => {
if (newSpeed === speed) {
if (speed !== 1) {
return 'applied';
}
return 'idle';
}
return 'calculating';
})();

// TODO: check that it does AM-PM
const started = millisToString(startedAt);
// TODO: finish at should account for speed factor
const finishAt = millisToString(expectedFinish !== null ? expectedFinish % dayInMs : null);

// TODO: this would cause re-renders on every second, we want to isolate this
const newFinish = millisToString(useExpectedTime(current ?? 0, newSpeed));

return (
<>
<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}>Expect end</span>
<span className={style.time}>{finishAt}</span>
</div>
{phase === 'calculating' && (
<div className={style.finish}>
<span className={style.tag}>Estimated end</span>
<span className={style.time}>{newFinish}</span>
</div>
)}
</div>

<div style={{ display: 'flex', gap: '1rem', justifyContent: 'space-between' }}>
<div>
<span>{`${speed}x`}</span>
{newSpeed !== speed && <span className={style.highlight}>{` ⇢ ${newSpeed}x`}</span>}
</div>
<div style={{ display: 'flex', gap: '0.5rem' }}>
{phase !== 'applied' ? (
<Button size='sm' variant='ontime-subtle-white' onClick={handleApply} isDisabled={phase !== 'calculating'}>
Apply
</Button>
) : (
<Button size='sm' variant='ontime-subtle-white' onClick={handleReset}>
Reset
</Button>
)}
</div>
</div>
</>
);
}

// TODO: extract and test
// calculate the new finish time
function useExpectedTime(remainingTimeMs: number, speedFactor: number): number {
const { clock } = useClock();
const adjustedRemainingTimeMs = remainingTimeMs / speedFactor;
const newFinishTimeMs = clock + adjustedRemainingTimeMs;
return newFinishTimeMs;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
.panelContainer {
display: flex;
flex-direction: column;
gap: 0.5rem;
margin-top: 2rem;
}

// TODO: unify panel label
.label {
display: block;
font-size: $inner-section-text-size;
color: $label-gray;
}

.highlight {
color: $orange-600;
}

.speedContainer {
background-color: $gray-1100;
width: 100%;
height: 4px;
border-radius: 1px;
position: relative;
overflow: hidden;
cursor: pointer;
}

.speedRegular {
position: absolute;
left: 0;
height: 100%;
width: 33.33%;
background-color: $ui-white;
}

.speedOverride {
position: absolute;
background-color: $orange-700;
left: 0;
height: 100%;
width: calc(var(--override, 0) * 1%);
}

.labels {
margin-top: 0.25rem;
font-size: calc(1rem - 3px);
color: $label-gray;
position: relative;

> span {
position: absolute;
transform: translateX(-50%);

&:first-child {
transform: translateX(0);
}

&:last-child {
transform: translateX(-100%);
}
}

.override {
color: $gray-1100;
background-color: $orange-600;
padding: 0 0.25rem;
border-radius: 2px;
top: calc(-4px - 0.5rem); // speed + gap * 2
transform: translate(-50%, -100%);
// TODO: account for case where we translate all the way left
font-weight: 600;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import { useState } from 'react';
import { Slider, SliderFilledTrack, SliderMark, SliderThumb, SliderTrack } from '@chakra-ui/react';

import { useTimerSpeed } from '../../../../common/hooks/useSocket';

import MeetSchedule from './MeetSchedule';

import style from './TimerSpeed.module.scss';

const labelStyles = {
mt: '2',
ml: '-2.5',
fontSize: 'sm',
};

export default function TimerSpeed() {
const { speed } = useTimerSpeed();
const [newSpeed, setNewSpeed] = useState(1);

return (
<div className={style.panelContainer}>
<div className={style.label}>Timer speed</div>
<MeetSchedule speed={speed} newSpeed={newSpeed} />
<Slider defaultValue={newSpeed} min={0.5} max={2.0} step={0.01} onChange={(v) => setNewSpeed(v)}>
<SliderMark value={0.5} {...labelStyles}>
0.5x
</SliderMark>
<SliderMark value={1.0} {...labelStyles}>
1.0x
</SliderMark>
<SliderMark value={1.5} {...labelStyles}>
1.5x
</SliderMark>
<SliderMark value={2.0} {...labelStyles}>
2.0x
</SliderMark>
<SliderTrack>
<SliderFilledTrack />
</SliderTrack>
<SliderThumb />
</Slider>
</div>
);
}
Loading