mirror of
https://github.com/cpvalente/ontime.git
synced 2026-07-26 10:38:55 +00:00
Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 805478f141 | |||
| fb1bbd6d5c | |||
| 221ff7b3af |
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 };
|
||||
}
|
||||
|
||||
Generated
+1448
-34
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user