Compare commits

...

3 Commits

Author SHA1 Message Date
Carlos Valente 805478f141 refactor: target chrome 89 2025-01-28 17:22:38 +01:00
Carlos Valente fb1bbd6d5c refactor: fix roll next day 2025-01-27 19:24:11 +01:00
Alex Christoffer Rasmussen 221ff7b3af Timeuntil in UI (#1461)
* cherry pick EventBlockChip

* pull data down throug components

* add comment

* cleanup css

Co-authored-by: Carlos Valente <34649812+cpvalente@users.noreply.github.com>

---------

Co-authored-by: Carlos Valente <34649812+cpvalente@users.noreply.github.com>
2025-01-27 16:39:32 +01:00
13 changed files with 1610 additions and 38 deletions
+2
View File
@@ -68,6 +68,7 @@
"@types/react-dom": "^18.0.10",
"@typescript-eslint/eslint-plugin": "catalog:",
"@typescript-eslint/parser": "catalog:",
"@vitejs/plugin-legacy": "^6.0.0",
"@vitejs/plugin-react": "^4.2.1",
"eslint": "catalog:",
"eslint-config-prettier": "catalog:",
@@ -81,6 +82,7 @@
"ontime-utils": "workspace:*",
"prettier": "catalog:",
"sass": "^1.57.1",
"terser": "^5.37.0",
"typescript": "catalog:",
"vite": "^5.2.11",
"vite-plugin-compression2": "^1.3.3",
@@ -250,3 +250,11 @@ export const useIsOnline = () => {
return useRuntimeStore(featureSelector);
};
export const usePlayback = () => {
const featureSelector = (state: RuntimeStore) => ({
playback: state.timer.playback,
});
return useRuntimeStore(featureSelector);
};
@@ -269,6 +269,7 @@ export default function Rundown({ data }: RundownProps) {
// all events before the current selected are in the past
let isPast = Boolean(featureData?.selectedEventId);
let isNextDay = false;
let totalGap = 0;
const isEditMode = appMode === AppMode.Edit;
return (
@@ -297,6 +298,7 @@ export default function Rundown({ data }: RundownProps) {
if (isPlayableEvent(entry)) {
isNextDay = checkIsNextDay(entry, lastEvent);
totalGap += !isPast ? entry.gap : 0;
if (isNewLatest(entry, lastEvent)) {
// populate previous entry
thisEvent = entry;
@@ -331,6 +333,7 @@ export default function Rundown({ data }: RundownProps) {
playback={isLoaded ? featureData.playback : undefined}
isRolling={featureData.playback === Playback.Roll}
isNextDay={isNextDay}
totalGap={totalGap}
/>
</div>
</div>
@@ -37,6 +37,7 @@ 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
totalGap: number;
}
export default function RundownEntry(props: RundownEntryProps) {
@@ -52,6 +53,7 @@ export default function RundownEntry(props: RundownEntryProps) {
isRolling,
eventIndex,
isNextDay,
totalGap,
} = props;
const { emitError } = useEmitLog();
const { addEvent, updateEvent, batchUpdateEvents, deleteEvent, swapEvents } = useEventAction();
@@ -173,6 +175,8 @@ export default function RundownEntry(props: RundownEntryProps) {
isRolling={isRolling}
gap={data.gap}
isNextDay={isNextDay}
dayOffset={data.dayOffset}
totalGap={totalGap}
actionHandler={actionHandler}
/>
);
@@ -9,7 +9,7 @@ $skip-opacity: 0.2;
display: grid;
grid-template-areas:
'binder ... ... ...'
'binder pb-actions times ...'
'binder pb-actions times chip'
'binder pb-actions title title'
'binder pb-actions estatus estatus'
'binder ... ... ...';
@@ -131,6 +131,10 @@ $skip-opacity: 0.2;
height: 100%;
}
.chipSection {
grid-area: chip;
}
.titleSection {
grid-area: title;
display: flex;
@@ -49,6 +49,8 @@ interface EventBlockProps {
isRolling: boolean;
gap: number;
isNextDay: boolean;
dayOffset: number;
totalGap: number;
actionHandler: (
action: EventItemActions,
payload?:
@@ -87,6 +89,8 @@ export default function EventBlock(props: EventBlockProps) {
isRolling,
gap,
isNextDay,
dayOffset,
totalGap,
actionHandler,
} = props;
const { selectedEventId, setSelectedEventId, clearSelectedEventId } = useEventIdSwapping();
@@ -303,6 +307,9 @@ export default function EventBlock(props: EventBlockProps) {
loaded={loaded}
playback={playback}
isRolling={isRolling}
dayOffset={dayOffset}
isPast={isPast}
totalGap={totalGap}
/>
)}
</div>
@@ -11,12 +11,14 @@ import { IoPlaySkipForward } from '@react-icons/all-files/io5/IoPlaySkipForward'
import { IoStop } from '@react-icons/all-files/io5/IoStop';
import { IoTime } from '@react-icons/all-files/io5/IoTime';
import { EndAction, MaybeString, Playback, TimerType, TimeStrategy } from 'ontime-types';
import { dayInMs } from 'ontime-utils';
import { cx } from '../../../common/utils/styleUtils';
import { tooltipDelayMid } from '../../../ontimeConfig';
import EditableBlockTitle from '../common/EditableBlockTitle';
import TimeInputFlow from '../time-input-flow/TimeInputFlow';
import EventBlockChip from './composite/EventBlockChip';
import EventBlockPlayback from './composite/EventBlockPlayback';
import EventBlockProgressBar from './composite/EventBlockProgressBar';
@@ -42,6 +44,9 @@ interface EventBlockInnerProps {
loaded: boolean;
playback?: Playback;
isRolling: boolean;
dayOffset: number;
isPast: boolean;
totalGap: number;
}
function EventBlockInner(props: EventBlockInnerProps) {
@@ -64,6 +69,9 @@ function EventBlockInner(props: EventBlockInnerProps) {
loaded,
playback,
isRolling,
dayOffset,
isPast,
totalGap,
} = props;
const [renderInner, setRenderInner] = useState(false);
@@ -108,6 +116,17 @@ function EventBlockInner(props: EventBlockInnerProps) {
loaded={loaded}
disablePlayback={skip || isRolling}
/>
{!skip && (
<EventBlockChip
className={style.chipSection}
id={eventId}
trueTimeStart={timeStart + dayOffset * dayInMs}
isPast={isPast}
isLoaded={loaded}
totalGap={totalGap}
isLinkedAndNext={isNext && linkStart !== null}
/>
)}
<div className={style.statusElements} id='block-status' data-ispublic={isPublic}>
<span className={style.eventNote}>{note}</span>
<div className={loaded ? style.progressBg : `${style.progressBg} ${style.hidden}`}>
@@ -0,0 +1,21 @@
.chip {
background-color: $gray-1100;
white-space: nowrap;
font-size: calc(1rem - 3px);
color: $label-gray;
padding: 0.125rem 0.5rem;
border-radius: 2px;
&.over {
color: $playback-negative;
}
&.under {
color: $playback-ahead;
}
&.due {
color: $warning-orange;
}
}
@@ -0,0 +1,77 @@
import { useMemo } from 'react';
import { Tooltip } from '@chakra-ui/react';
import { isPlaybackActive, MILLIS_PER_MINUTE, MILLIS_PER_SECOND } from 'ontime-utils';
import { usePlayback, useTimelineStatus } from '../../../../common/hooks/useSocket';
import { cx } from '../../../../common/utils/styleUtils';
import { formatDuration } from '../../../../common/utils/time';
import { tooltipDelayFast } from '../../../../ontimeConfig';
import style from './EventBlockChip.module.scss';
interface EventBlockChipProps {
id: string;
trueTimeStart: number;
isPast: boolean;
isLoaded: boolean;
className: string;
totalGap: number;
isLinkedAndNext: boolean;
}
export default function EventBlockChip(props: EventBlockChipProps) {
const { trueTimeStart, isPast, isLoaded, className, totalGap, isLinkedAndNext } = props;
const { playback } = usePlayback();
if (isLoaded) {
return null; //TODO: the is a small flash of 'DUE' on the loaded event as clock data arrives before isLoaded propagates
}
const playbackActive = isPlaybackActive(playback);
if (!playbackActive || isPast) {
return null; //TODO: Event report will go here
}
if (playbackActive) {
// we extracted the component to avoid unnecessary calculations and re-renders
return (
<EventUntil
className={className}
trueTimeStart={trueTimeStart}
totalGap={totalGap}
isLinkedAndNext={isLinkedAndNext}
/>
);
}
return null;
}
interface EventUntilProps {
className: string;
trueTimeStart: number;
totalGap: number;
isLinkedAndNext: boolean;
}
function EventUntil(props: EventUntilProps) {
const { trueTimeStart, className, totalGap, isLinkedAndNext } = props;
const { clock, offset } = useTimelineStatus();
const [timeUntilString, isDue] = useMemo(() => {
const consumedOffset = isLinkedAndNext ? offset : Math.min(offset + totalGap, 0);
const offsetTimestart = trueTimeStart - consumedOffset;
const timeUntil = offsetTimestart - clock;
const isDue = timeUntil < MILLIS_PER_SECOND;
return [isDue ? 'DUE' : `${formatDuration(Math.abs(timeUntil), timeUntil > 2 * MILLIS_PER_MINUTE)}`, isDue];
}, [totalGap, isLinkedAndNext, offset, trueTimeStart, clock]);
return (
<Tooltip label='Expected time until start' openDelay={tooltipDelayFast}>
<div className={cx([style.chip, isDue ? style.due : null, className])}>
<div>{timeUntilString}</div>
</div>
</Tooltip>
);
}
+1
View File
@@ -25,6 +25,7 @@ $ontime-delay-text: #E69056;
$ontime-paused: #c05621;
$ontime-stop: #E4281E;
$playback-negative: $red-500;
$playback-ahead: $green-500;
$active-indicator: #8bb33d;
$text-black: $gray-1350;
+2
View File
@@ -1,4 +1,5 @@
import { sentryVitePlugin } from '@sentry/vite-plugin';
import legacy from '@vitejs/plugin-legacy';
import react from '@vitejs/plugin-react';
import { fileURLToPath, URL } from 'node:url';
import { defineConfig } from 'vite';
@@ -32,6 +33,7 @@ export default defineConfig({
excludeReplayWorker: true,
},
}),
legacy({ targets: ['chrome >= 89'] }),
compression({
algorithm: 'brotliCompress',
exclude: /\.(html)$/, // Ontime cloud: Exclude HTML files from compression so we can change the base property at runtime
+13 -3
View File
@@ -492,6 +492,7 @@ export type UpdateResult = {
export function update(): UpdateResult {
// 0. there are some things we always do
const previousClock = runtimeState.clock;
runtimeState.clock = clock.timeNow(); // we update the clock on every update call
// 1. is playback idle?
@@ -501,7 +502,8 @@ export function update(): UpdateResult {
// 2. are we waiting to roll?
if (runtimeState.timer.playback === Playback.Roll && runtimeState.timer.secondaryTimer !== null) {
return updateIfWaitingToRoll();
const hasCrossedMidnight = previousClock > runtimeState.clock;
return updateIfWaitingToRoll(hasCrossedMidnight);
}
// 3. at this point we know that we are playing an event
@@ -543,16 +545,24 @@ export function update(): UpdateResult {
return { hasTimerFinished: false, hasSecondaryTimerFinished: false };
}
function updateIfWaitingToRoll() {
function updateIfWaitingToRoll(hasCrossedMidnight: boolean) {
// eslint-disable-next-line no-unused-labels -- dev code path
DEV: {
if (runtimeState.eventNow === null || runtimeState._timer.secondaryTarget === null) {
throw new Error('runtimeState.updateIfWaitingToRoll: invalid state received');
}
}
//account for offset
// account for offset
const offsetClock = runtimeState.clock + runtimeState.runtime.offset;
runtimeState.timer.phase = TimerPhase.Pending;
if (hasCrossedMidnight) {
// if we crossed midnight, we need to update the target
// this is the same logic from the roll function
runtimeState._timer.secondaryTarget = normaliseRollStart(runtimeState.eventNow.timeStart, offsetClock);
}
runtimeState.timer.secondaryTimer = runtimeState._timer.secondaryTarget - offsetClock;
return { hasTimerFinished: false, hasSecondaryTimerFinished: runtimeState.timer.secondaryTimer <= 0 };
}
+1448 -34
View File
File diff suppressed because it is too large Load Diff