Compare commits

..

25 Commits

Author SHA1 Message Date
Carlos Valente 00aa8a09fa refactor(roll breaking): make starting with roll backtrace planned times 2025-09-21 07:42:47 +02:00
Carlos Valente d04f958c11 fix: initialise group time 2025-09-21 07:42:47 +02:00
Alex Christoffer Rasmussen 5716fc31cf feat: export package to help api consumers (#1624) 2025-09-21 07:42:47 +02:00
Carlos Valente a5d628af0c fix: prevent params drawer from being larger than screen 2025-09-21 07:42:47 +02:00
Carlos Valente 111a1f95ba fix: correct offset direction in expected time calculations 2025-09-21 07:42:47 +02:00
Carlos Valente 4bf74d86b3 refactor: set start metadata when starting from roll 2025-09-21 07:42:47 +02:00
arc-alex d2638d64e4 fix: group end elements should only send timertype data nothing else 2025-09-21 07:42:47 +02:00
arc-alex 02cddeec4d refactor: add a try grourd to the cell data generator to better pinpoint errors 2025-09-21 07:42:47 +02:00
Carlos Valente a8fe8b5f1e refactor: allow filtering schedule 2025-09-21 07:42:47 +02:00
Carlos Valente 8d2db7ffcd Relative mode (#1764)
* feat: add group start time to state

* refactor: calculate expected times when offset mode changes

* feat: show relative data in ui

* fixup! feat: add group start time to state

* fixup! refactor: calculate expected times when offset mode changes

* fixup! feat: show relative data in ui

* try to handle multiday

* show planed/expected rundown end day offset values

* refactor: style tweaks to timers

* refactor: cleanup data use

* day offset

* update tests

* test getExpectedStart multiday

* write start epoch to restore file

* current day in relative mode

* remove todo

* move find day offset to utils

---------

Co-authored-by: arc-alex <ac@omnivox.dk>
2025-09-21 07:42:47 +02:00
Alex Christoffer Rasmussen 313590905f fix docker ignore (#1780) 2025-09-21 07:42:47 +02:00
Carlos Valente 7bbdab7dd9 fix(cuesheet): infinite render loop on resizing columns 2025-09-21 07:42:47 +02:00
Carlos Valente 304832ecde fix: maintain multiline in cuesheet cells 2025-09-21 07:42:47 +02:00
Carlos Valente 0ab4abfcfe docs: add contribution guidelines 2025-09-21 07:42:47 +02:00
Carlos Valente 1847822f31 fix: skip nested events 2025-09-21 07:42:47 +02:00
Carlos Valente 4499495212 feat: allow finding milestones 2025-09-21 07:42:47 +02:00
Carlos Valente 0035070db8 refactor: improve automated following 2025-09-21 07:42:47 +02:00
Carlos Valente 890a97d071 fix: allow moving a group after another 2025-09-21 07:42:47 +02:00
Carlos Valente 5441e5b07f refactor: improve pin styling 2025-09-21 07:42:47 +02:00
Carlos Valente ab4b94f2d8 fix: prevent layout reflow on different entry types 2025-09-21 07:42:47 +02:00
Carlos Valente fd7ccb4a5b refactor: add default value to secondary source 2025-09-21 07:42:47 +02:00
Carlos Valente 2e6bf234d5 fix: phase style overrides in timer 2025-09-21 07:42:47 +02:00
Alex Christoffer Rasmussen c934b0ecdd cleanup (#1776)
* chore: cleanup leftover console log

* chore: prevent error in client tsconfig
2025-09-21 07:42:47 +02:00
Alex Christoffer Rasmussen cd6d6f24eb fix: sheet default import map (#1775)
* fix: default import map

* fix: update test
2025-09-21 07:42:47 +02:00
arc-alex e632a08557 chore: bump version 2025-09-21 07:42:47 +02:00
84 changed files with 1492 additions and 370 deletions
+2 -1
View File
@@ -16,7 +16,8 @@
# Ignore build folders
node_modules
dist
**/node_modules
**/dist
# Ignore default volumes created by running docker compose up
ontime-db
+42
View File
@@ -0,0 +1,42 @@
name: Ontime Resolver build
on:
release:
types: [published]
workflow_dispatch:
jobs:
build_cli:
permissions:
id-token: write
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Set up QEMU
uses: docker/setup-qemu-action@v3
- name: Use Node.js
uses: actions/setup-node@v4
with:
node-version: 22
registry-url: 'https://registry.npmjs.org'
- name: Setup pnpm
uses: pnpm/action-setup@v3
with:
version: 10
- name: Install dependencies
run: pnpm install --frozen-lockfile
- name: Build project packages
run: pnpm turbo @getontime/resolver#build
- name: Publish to NPM
run: pnpm publish --access public --no-git-checks
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
NPM_CONFIG_PROVENANCE: true
working-directory: ./apps/resolver
+13
View File
@@ -99,3 +99,16 @@ Other useful commands
- __List running processes__ by running `docker ps`
- __Kill running process__ by running `docker kill <process-id>`
## CONTRIBUTION GUIDELINES
If you want to propose changes to the codebase, please reach out before opening a Pull Request.
For new PRs, please follow the following checklist:
* [ ] You have updated and ran unit locally and they are passing. Unit tests are generally created for all utility functions and business logic
* [ ] You have ran code formatting and linting in all your changes
* [ ] The branch is clean and the commits are meaningfully separated and contain descriptive messages
* [ ] The PR body contains description and motivation for the changes
After this checklist is complete, you can request a review from one of the maintainers to get feedback and approval on the changes. \
We will review as soon as possible
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@getontime/cli",
"version": "4.0.0-beta.2",
"version": "4.0.0-beta.3",
"author": "Carlos Valente",
"description": "Time keeping for live events",
"repository": "https://github.com/cpvalente/ontime",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "ontime-ui",
"version": "4.0.0-beta.2",
"version": "4.0.0-beta.3",
"private": true,
"type": "module",
"dependencies": {
@@ -11,9 +11,11 @@
}
.pin {
margin-top: 0.5rem;
display: flex;
align-items: center;
justify-content: center;
gap: 4px;
input {
font-size: 4rem;
@@ -28,6 +28,7 @@
bottom: 0;
width: 40rem;
max-width: 100vw;
height: 100vh;
display: flex;
@@ -1,6 +1,5 @@
import { RefObject, useCallback, useEffect, useRef } from 'react';
import { useSelectedEventId } from './useSocket';
import { RefObject, useCallback, useEffect } from 'react';
import { MaybeString } from 'ontime-types';
function scrollToComponent<ComponentRef extends HTMLElement, ScrollRef extends HTMLElement>(
componentRef: RefObject<ComponentRef>,
@@ -18,37 +17,26 @@ function scrollToComponent<ComponentRef extends HTMLElement, ScrollRef extends H
scrollRef.current.scrollTo({ top, behavior: 'smooth' });
}
function snapToComponent<ComponentRef extends HTMLElement, ScrollRef extends HTMLElement>(
componentRef: RefObject<ComponentRef>,
scrollRef: RefObject<ScrollRef>,
topOffset: number,
) {
if (!componentRef.current || !scrollRef.current) {
return;
}
const componentRect = componentRef.current.getBoundingClientRect();
const scrollRect = scrollRef.current.getBoundingClientRect();
const top = componentRect.top - scrollRect.top + scrollRef.current.scrollTop - topOffset;
// maintain current x scroll position
scrollRef.current.scrollTo(scrollRef.current.scrollLeft, top);
}
interface UseFollowComponentProps {
followRef: RefObject<HTMLElement | null>;
scrollRef: RefObject<HTMLElement | null>;
doFollow: boolean;
topOffset?: number;
setScrollFlag?: (newValue: boolean) => void;
followTrigger?: MaybeString; // this would be an entry id or null
}
export default function useFollowComponent(props: UseFollowComponentProps) {
const { followRef, scrollRef, doFollow, topOffset = 100, setScrollFlag } = props;
// when cursor moves, view should follow
export default function useFollowComponent({
followRef,
scrollRef,
doFollow,
topOffset = 100,
setScrollFlag,
followTrigger,
}: UseFollowComponentProps) {
// when trigger moves, view should follow
useEffect(() => {
if (!doFollow) {
if (!doFollow || !followTrigger) {
return;
}
@@ -60,16 +48,14 @@ export default function useFollowComponent(props: UseFollowComponentProps) {
setScrollFlag?.(false);
});
}
// eslint-disable-next-line -- the prompt seems incorrect
}, [followRef?.current, scrollRef?.current]);
}, [followTrigger, doFollow, followRef, scrollRef, setScrollFlag, topOffset]);
const scrollToRefComponent = useCallback(
(componentRef = followRef, containerRef = scrollRef, offset = topOffset) => {
if (componentRef.current && containerRef.current) {
if (componentRef && containerRef) {
// @ts-expect-error -- we know this are not null
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
scrollToComponent(componentRef!, scrollRef!, offset);
scrollToComponent(componentRef!, containerRef!, offset);
}
},
[followRef, scrollRef, topOffset],
@@ -77,32 +63,3 @@ export default function useFollowComponent(props: UseFollowComponentProps) {
return scrollToRefComponent;
}
export function useFollowSelected(doFollow: boolean, topOffset = 100) {
const selectedEvenId = useSelectedEventId();
const selectedRef = useRef<HTMLTableRowElement>(null);
const scrollRef = useRef<HTMLDivElement>(null);
useEffect(() => {
if (!doFollow) {
return;
}
if (selectedEvenId && selectedRef.current && scrollRef.current) {
// Use requestAnimationFrame to ensure the component is fully loaded
window.requestAnimationFrame(() => {
snapToComponent(
{ current: selectedRef.current } as RefObject<HTMLElement>,
{ current: scrollRef.current } as RefObject<HTMLElement>,
topOffset,
);
});
}
}, [doFollow, selectedEvenId, topOffset]);
return {
selectedRef,
scrollRef,
};
}
+40 -20
View File
@@ -11,7 +11,6 @@ const createSelector =
export const setClientRemote = {
setIdentify: (payload: { target: string; identify: boolean }) => sendSocket('client', payload),
setRedirect: (payload: { target: string; redirect: string }) => {
console.log('--- got', payload);
sendSocket('client', payload);
},
setClientName: (payload: { target: string; rename: string }) => sendSocket('client', payload),
@@ -165,24 +164,6 @@ export const useProgressData = createSelector((state: RuntimeStore) => ({
timeDanger: state.eventNow?.timeDanger ?? null,
}));
export const useRundownOverview = createSelector((state: RuntimeStore) => ({
plannedStart: state.rundown.plannedStart,
actualStart: state.rundown.actualStart,
plannedEnd: state.rundown.plannedEnd,
expectedEnd: state.offset.expectedRundownEnd,
}));
export const useRuntimePlaybackOverview = createSelector((state: RuntimeStore) => ({
playback: state.timer.playback,
clock: state.clock,
numEvents: state.rundown.numEvents,
selectedEventIndex: state.rundown.selectedEventIndex,
offset: state.offset.mode === OffsetMode.Absolute ? state.offset.absolute : state.offset.relative,
groupExpectedEnd: state.offset.expectedGroupEnd,
}));
export const useTimelineStatus = createSelector((state: RuntimeStore) => ({
clock: state.clock,
offset: state.offset.absolute,
@@ -191,7 +172,7 @@ export const useTimelineStatus = createSelector((state: RuntimeStore) => ({
export const useExpectedStartData = createSelector((state: RuntimeStore) => ({
offset: state.offset.mode === OffsetMode.Absolute ? state.offset.absolute : state.offset.relative,
mode: state.offset.mode,
currentDay: state.eventNow?.dayOffset ?? 0,
currentDay: state.rundown.currentDay ?? 0,
actualStart: state.rundown.actualStart,
plannedStart: state.rundown.plannedStart,
clock: state.clock,
@@ -228,6 +209,45 @@ export const usePlayback = () => {
return useRuntimeStore(featureSelector);
};
/* ======================= Overview data subscriptions ======================= */
export const useRundownOverview = createSelector((state: RuntimeStore) => ({
plannedStart: state.rundown.plannedStart,
actualStart: state.rundown.actualStart,
plannedEnd: state.rundown.plannedEnd,
expectedEnd: state.offset.expectedRundownEnd,
}));
export const useProgressOverview = createSelector((state: RuntimeStore) => ({
numEvents: state.rundown.numEvents,
selectedEventIndex: state.rundown.selectedEventIndex,
}));
export const useOffsetOverview = createSelector((state: RuntimeStore) => ({
offset: state.offset.mode === OffsetMode.Absolute ? state.offset.absolute : state.offset.relative,
playback: state.timer.playback,
}));
export const useGroupTimerOverView = createSelector((state: RuntimeStore) => ({
clock: state.clock,
mode: state.offset.mode,
groupExpectedEnd: state.offset.expectedGroupEnd,
// we can force these numbers to 0 fo this use case to avoid null checks
actualGroupStart: state.rundown.actualGroupStart ?? 0,
currentDay: state.eventNow?.dayOffset ?? 0,
playback: state.timer.playback,
}));
export const useFlagTimerOverView = createSelector((state: RuntimeStore) => ({
clock: state.clock,
mode: state.offset.mode,
// we can force these numbers to 0 fo this use case to avoid null checks
actualStart: state.rundown.actualStart ?? 0,
plannedStart: state.rundown.plannedStart ?? 0,
currentDay: state.eventNow?.dayOffset ?? 0,
playback: state.timer.playback,
}));
/* ======================= View specific subscriptions ======================= */
export const useTimerSocket = createSelector((state: RuntimeStore) => ({
@@ -1,19 +0,0 @@
export type OverridableOptions = {
keyColour?: string;
textColour?: string;
textBackground?: string;
font?: string;
size?: number;
justifyContent?: 'start' | 'center' | 'end';
alignItems?: 'start' | 'center' | 'end';
left?: string;
top?: string;
hideNav?: boolean;
hideOvertime?: boolean;
hideMessagesOverlay?: boolean;
hideEndMessage?: boolean;
language?: string;
showProgressBar?: boolean;
hideTimerSeconds?: boolean;
removeLeadingZeros?: boolean;
};
+2 -2
View File
@@ -1,5 +1,5 @@
import {
ApiAction,
ApiActionTag,
Log,
MessageTag,
RefetchKey,
@@ -199,7 +199,7 @@ export const connectSocket = () => {
};
};
export function sendSocket<T extends MessageTag | ApiAction>(
export function sendSocket<T extends MessageTag | ApiActionTag>(
tag: T,
payload: T extends MessageTag ? Pick<WsPacketToServer & { tag: T }, 'payload'>['payload'] : unknown,
): void {
@@ -49,6 +49,7 @@ export default function Operator() {
scrollRef,
doFollow: !lockAutoScroll,
topOffset: selectedOffset,
followTrigger: selectedEventId,
});
useWindowTitle('Operator');
@@ -181,7 +182,7 @@ export default function Operator() {
const { isPast, isSelected, isLinkedToLoaded, totalGap } = process(nestedEntry);
// hide past events (if setting) and skipped events
if (hidePast && isPast) {
if ((hidePast && isPast) || nestedEntry.skip) {
return null;
}
@@ -47,9 +47,10 @@
.daySpan {
&::after {
content: '*';
content: "+"attr(data-day-offset);
vertical-align: super;
font-size: 0.75em;
font-size: 0.6em;
letter-spacing: 0;
color: $info-blue;
}
}
@@ -67,3 +68,10 @@
font-size: calc(1rem - 2px);
text-align: right;
}
.dueTime {
text-transform: capitalize;
font-size: 1rem;
letter-spacing: 0;
color: $playback-over;
}
@@ -8,23 +8,26 @@ import {
TbFolderPin,
TbFolderStar,
} from 'react-icons/tb';
import { OntimeEvent, OntimeGroup, TimerPhase, TimerType } from 'ontime-types';
import { isPlaybackActive, millisToString } from 'ontime-utils';
import { OffsetMode, OntimeEvent, OntimeGroup, TimerPhase, TimerType } from 'ontime-types';
import { dayInMs, isPlaybackActive, millisToString } from 'ontime-utils';
import Tooltip from '../../../common/components/tooltip/Tooltip';
import {
useClock,
useCurrentGroupId,
useFlagTimerOverView,
useGroupTimerOverView,
useNextFlag,
useOffsetOverview,
useProgressOverview,
useRundownOverview,
useRuntimePlaybackOverview,
useTimer,
} from '../../../common/hooks/useSocket';
import { useEntry } from '../../../common/hooks-query/useRundown';
import { getOffsetState, getOffsetText } from '../../../common/utils/offset';
import { cx, enDash, timerPlaceholder } from '../../../common/utils/styleUtils';
import { formatTime } from '../../../common/utils/time';
import { calculateEndAndDaySpan, formattedTime } from '../overview.utils';
import { calculateEndAndDaySpan, formatDueTime, formattedTime } from '../overview.utils';
import { OverUnder, TimeColumn } from './TimeLayout';
@@ -58,8 +61,8 @@ export function StartTimes() {
<Tooltip text='Planned end time' render={<TbCalendarPin className={style.icon} />} />
{maybePlannedDaySpan > 0 ? (
<Tooltip
text={`Event spans over ${maybePlannedDaySpan + 1} days`}
render={<span className={cx([style.time, style.daySpan])} />}
text={`Rundown spans over ${maybePlannedDaySpan + 1} days`}
render={<span className={cx([style.time, style.daySpan])} data-day-offset={maybePlannedDaySpan} />}
>
{plannedEndText}
</Tooltip>
@@ -71,8 +74,8 @@ export function StartTimes() {
<Tooltip text='Expected end time' render={<TbCalendarStar className={style.icon} />} />
{maybeExpectedEnd !== null && maybeExpectedDaySpan > 0 ? (
<Tooltip
text={`Event spans over ${maybeExpectedDaySpan + 1} days`}
render={<span className={cx([style.time, style.daySpan])} />}
text={`Rundown spans over ${maybeExpectedDaySpan + 1} days`}
render={<span className={cx([style.time, style.daySpan])} data-day-offset={maybeExpectedDaySpan} />}
>
{formattedTime(maybeExpectedEnd)}
</Tooltip>
@@ -96,61 +99,110 @@ export function MetadataTimes() {
);
}
//TODO: there a some things here we still need to think about, mainly what to do whit the planed group duration in relation to the events
function GroupTimes() {
const { clock, groupExpectedEnd } = useRuntimePlaybackOverview();
const { clock, mode, groupExpectedEnd, actualGroupStart, currentDay, playback } = useGroupTimerOverView();
const { currentGroupId } = useCurrentGroupId();
const group = useEntry(currentGroupId) as OntimeGroup | null;
// the group end time dose not encode any day offsets
const plannedGroupEnd = group && group.timeStart !== null ? group.timeStart + group.duration - clock : null;
const plannedTimeUntilGroupEnd = formattedTime(plannedGroupEnd, 3, TimerType.CountDown);
const active = isPlaybackActive(playback);
// the group end time dose not encode any day offsets so it is calculated with group start time and duration
const plannedGroupEnd = (() => {
if (!active) return null;
if (!group || group.timeStart === null) return null;
const normalizedClock = clock + currentDay * dayInMs;
return mode === OffsetMode.Absolute
? group.timeStart + group.duration - normalizedClock
: actualGroupStart + group.duration - normalizedClock;
})();
const plannedTimeUntilGroupEnd = formatDueTime(plannedGroupEnd, 3, TimerType.CountDown);
const expectedGroupEnd = groupExpectedEnd !== null ? groupExpectedEnd - clock : null;
const expectedTimeUntilGroupEnd = formattedTime(expectedGroupEnd, 3, TimerType.CountDown);
const groupTitle = group?.title ?? null;
const expectedTimeUntilGroupEnd = formatDueTime(expectedGroupEnd, 3, TimerType.CountDown);
return (
<div className={style.metadataRow}>
<span className={groupTitle ? style.labelTitle : style.label}>{`${groupTitle ? groupTitle : 'Group'} `}</span>
<span className={group?.title ? style.labelTitle : style.label}>{`${group?.title || 'Group'} `}</span>
<div className={style.labelledElement}>
<Tooltip text='Time to planned group end' render={<TbFolderPin className={style.icon} />} />
<span className={cx([style.time, !group && style.muted])}>{plannedTimeUntilGroupEnd}</span>
<span
className={cx([
style.time,
(!group || !active) && style.muted,
plannedTimeUntilGroupEnd === 'due' && style.dueTime,
])}
>
{plannedTimeUntilGroupEnd}
</span>
</div>
<div className={style.labelledElement}>
<Tooltip text='Time to expected group end' render={<TbFolderStar className={style.icon} />} />
<span className={cx([style.time, groupExpectedEnd === null && style.muted])}>{expectedTimeUntilGroupEnd}</span>
<span
className={cx([
style.time,
!groupExpectedEnd && style.muted,
expectedTimeUntilGroupEnd === 'due' && style.dueTime,
])}
>
{expectedTimeUntilGroupEnd}
</span>
</div>
</div>
);
}
function FlagTimes() {
const { clock } = useClock();
const { clock, mode, actualStart, plannedStart, playback, currentDay } = useFlagTimerOverView();
const { id, expectedStart } = useNextFlag();
const entry = useEntry(id) as OntimeEvent | null;
const plannedFlagStart = entry ? entry.timeStart - clock : null;
const plannedTimeUntilDisplay = formattedTime(plannedFlagStart, 3, TimerType.CountDown);
const active = isPlaybackActive(playback);
const plannedFlagStart = (() => {
if (!active) return null;
if (!entry) return null;
const normalizedTimeStart = entry.timeStart + entry.dayOffset * dayInMs;
const normalizedClock = clock + currentDay * dayInMs;
return mode === OffsetMode.Absolute
? normalizedTimeStart - normalizedClock
: normalizedTimeStart + actualStart - plannedStart - normalizedClock;
})();
const plannedTimeUntilDisplay = formatDueTime(plannedFlagStart, 3, TimerType.CountDown);
const expectedTimeUntil = expectedStart !== null ? expectedStart - clock : null;
const expectedTimeUntilDisplay = formattedTime(expectedTimeUntil, 3, TimerType.CountDown);
const expectedTimeUntilDisplay = formatDueTime(expectedTimeUntil, 3, TimerType.CountDown);
const title = entry?.title ?? null;
return (
<div className={style.metadataRow}>
<span className={title ? style.labelTitle : style.label}>{`${title ? title : 'Flag'} `}</span>
<span className={title ? style.labelTitle : style.label}>{`${title || 'Flag'} `}</span>
<div className={style.labelledElement}>
<Tooltip text='Time to next flag planned start' render={<TbFlagPin className={style.icon} />} />
<span data-testid='flag-plannedStart' className={cx([style.time, !entry && style.muted])}>
<span
data-testid='flag-plannedStart'
className={cx([
style.time,
(!entry || !active) && style.muted,
plannedTimeUntilDisplay === 'due' && style.dueTime,
])}
>
{plannedTimeUntilDisplay}
</span>
</div>
<div className={style.labelledElement}>
<Tooltip text='Time to next flag expected start' render={<TbFlagStar className={style.icon} />} />
<span data-testid='flag-expectedStart' className={cx([style.time, expectedTimeUntil === null && style.muted])}>
<span
data-testid='flag-expectedStart'
className={cx([
style.time,
expectedTimeUntil === null && style.muted,
expectedTimeUntilDisplay === 'due' && style.dueTime,
])}
>
{expectedTimeUntilDisplay}
</span>
</div>
@@ -159,7 +211,7 @@ function FlagTimes() {
}
export function ProgressOverview() {
const { numEvents, selectedEventIndex } = useRuntimePlaybackOverview();
const { numEvents, selectedEventIndex } = useProgressOverview();
const current = selectedEventIndex !== null ? selectedEventIndex + 1 : enDash;
const progressText = numEvents ? `${current} of ${numEvents || enDash}` : enDash;
@@ -168,7 +220,7 @@ export function ProgressOverview() {
}
export function OffsetOverview() {
const { offset, playback } = useRuntimePlaybackOverview();
const { offset, playback } = useOffsetOverview();
const isPlaying = isPlaybackActive(playback);
const offsetState = getOffsetState(isPlaying ? offset : null);
@@ -3,6 +3,23 @@ import { dayInMs, millisToString } from 'ontime-utils';
import { timerPlaceholder, timerPlaceholderMin } from '../../common/utils/styleUtils';
/**
* Composition to stop negative timers from being formatted
* They should show a due string instead
*
* This is used for cases when a negative timer is unwanted
* eg: count down to a milestone
*/
export function formatDueTime(
time: MaybeNumber,
segments: number = 3,
direction?: TimerType.CountDown | TimerType.CountUp,
dueString = 'due',
): string {
if (time !== null && time <= 0) return dueString;
return formattedTime(time, segments, direction);
}
/**
* Encapsulates the logic for formatting time in overview
*/
+7 -2
View File
@@ -82,7 +82,12 @@ export default function Rundown({ data, rundownMetadata }: RundownProps) {
const cursorRef = useRef<HTMLDivElement | null>(null);
const scrollRef = useRef<HTMLDivElement | null>(null);
useFollowComponent({ followRef: cursorRef, scrollRef, doFollow: editorMode === AppMode.Run });
useFollowComponent({
followRef: cursorRef,
scrollRef,
doFollow: true,
followTrigger: editorMode === AppMode.Edit ? cursor : featureData?.selectedEventId,
});
// DND KIT
const sensors = useSensors(useSensor(PointerSensor, { activationConstraint: { distance: 10 } }));
@@ -312,7 +317,7 @@ export default function Rundown({ data, rundownMetadata }: RundownProps) {
setMetadata(rundownMetadata);
}, [order, entries, rundownMetadata]);
// in run mode, we follow selection
// in run mode, we follow the playback selection and open groups as needed
useEffect(() => {
if (editorMode !== AppMode.Run || !featureData?.selectedEventId) {
return;
@@ -28,20 +28,22 @@
padding-inline: 0;
box-shadow: $box-shadow-right;
flex: 1 2 auto; /* flex-grow: 1, flex-shrink: 2, flex-basis: auto */
flex: 1 1 0; /* flex-grow: 1, flex-shrink: 1, flex-basis: 0 */
min-width: 38rem;
max-width: 60rem;
max-width: none;
width: 0;
}
.side {
max-height: 100%;
max-width: 45rem;
margin: 0.5rem 0;
padding: 1rem;
padding-right: 0;
background-color: $gray-1325;
border-radius: 0 8px 8px 0;
flex: 1 1 auto; /* flex-grow: 1, flex-shrink: 1, flex-basis: auto */
max-width: 45rem;
flex: 1 1 0; /* flex-grow: 1, flex-shrink: 1, flex-basis: 0 */
// width is locked to swatch picker elements
min-width: calc(15 * 2rem + 13 * 0.5rem);
width: 0;
}
@@ -73,9 +73,7 @@ interface EventUntilProps {
isLinkedToLoaded: boolean;
}
function EventUntil(props: EventUntilProps) {
const { timeStart, delay, dayOffset, totalGap, isLinkedToLoaded } = props;
function EventUntil({ timeStart, delay, dayOffset, totalGap, isLinkedToLoaded }: EventUntilProps) {
const timeUntil = useTimeUntilExpectedStart({ timeStart, delay, dayOffset }, { totalGap, isLinkedToLoaded });
const isDue = timeUntil < MILLIS_PER_SECOND;
@@ -41,9 +41,9 @@ export function canDrop(
order?: 'after' | 'before',
isTargetCollapsed?: boolean,
): boolean {
// this would mean inserting a group inside another
// inserting before would mean adding a group inside another
if (targetType === 'end-group') {
return false;
return order === 'after';
}
// this means swapping places with another group
-1
View File
@@ -17,7 +17,6 @@ $header-font-size: clamp(24px, 2.5vw, 48px);
// General styling
$accent-color: $red-500; // --accent-color-override
$delay-color: $ontime-delay-text;
$viewer-label-color: rgba(white, 25%);
// Main Properties of a viewer
@@ -10,13 +10,14 @@ import {
makeProjectDataOptions,
} from '../../common/components/view-params-editor/viewParams.utils';
import { PresetContext } from '../../common/context/PresetContext';
import { scheduleOptions } from '../common/schedule/schedule.options';
import { getScheduleOptions } from '../common/schedule/schedule.options';
export const getBackstageOptions = (
timeFormat: string,
customFields: CustomFields,
projectData: ProjectData,
): ViewOption[] => {
const customFieldOptions = makeOptionsFromCustomFields(customFields, []);
const secondaryOptions = makeOptionsFromCustomFields(customFields, [
{ value: 'none', label: 'None' },
{ value: 'note', label: 'Note' },
@@ -39,7 +40,7 @@ export const getBackstageOptions = (
},
],
},
scheduleOptions,
getScheduleOptions(customFieldOptions),
{
title: OptionTitle.ElementVisibility,
collapsible: true,
@@ -1,14 +1,5 @@
import {
createContext,
PropsWithChildren,
RefObject,
useContext,
useEffect,
useLayoutEffect,
useRef,
useState,
} from 'react';
import { isOntimeEvent, OntimeEntry, OntimeEvent } from 'ontime-types';
import { createContext, PropsWithChildren, RefObject, use, useEffect, useLayoutEffect, useRef, useState } from 'react';
import { EntryId, isOntimeEvent, OntimeEntry, OntimeEvent } from 'ontime-types';
import { usePartialRundown } from '../../../common/hooks-query/useRundown';
@@ -25,13 +16,18 @@ interface ScheduleContextState {
const ScheduleContext = createContext<ScheduleContextState | undefined>(undefined);
interface ScheduleProviderProps {
selectedEventId: string | null;
selectedEventId: EntryId | null;
}
export const ScheduleProvider = ({ children, selectedEventId }: PropsWithChildren<ScheduleProviderProps>) => {
const { cycleInterval, stopCycle } = useScheduleOptions();
const { data: events } = usePartialRundown((event: OntimeEntry) => {
return isOntimeEvent(event);
const { cycleInterval, stopCycle, filter } = useScheduleOptions();
const { data: events } = usePartialRundown((entry: OntimeEntry) => {
if (filter) {
// custom keys are prepended with custom-
const customKey = filter.startsWith('custom-') ? filter.slice('custom-'.length) : filter;
return isOntimeEvent(entry) && Boolean(entry.custom[customKey]);
}
return isOntimeEvent(entry);
});
const [firstIndex, setFirstIndex] = useState(-1);
@@ -135,7 +131,7 @@ export const ScheduleProvider = ({ children, selectedEventId }: PropsWithChildre
selectedEventIndex = 0;
return (
<ScheduleContext.Provider
<ScheduleContext
value={{
events: viewEvents as OntimeEvent[],
selectedEventId,
@@ -145,12 +141,12 @@ export const ScheduleProvider = ({ children, selectedEventId }: PropsWithChildre
}}
>
{children}
</ScheduleContext.Provider>
</ScheduleContext>
);
};
export const useSchedule = () => {
const context = useContext(ScheduleContext);
const context = use(ScheduleContext);
if (!context) {
throw new Error('useSchedule() can only be used inside a ScheduleContext');
}
@@ -22,8 +22,7 @@ interface ScheduleItemProps {
delay: number;
}
export default function ScheduleItem(props: ScheduleItemProps) {
const { timeStart, timeEnd, title, colour, skip, delay } = props;
export default function ScheduleItem({ timeStart, timeEnd, title, colour, skip, delay }: ScheduleItemProps) {
const { showExpected } = useScheduleOptions();
if (showExpected) {
@@ -67,9 +66,7 @@ export default function ScheduleItem(props: ScheduleItemProps) {
);
}
function DelayedScheduleItem(props: ScheduleItemProps) {
const { timeStart, timeEnd, title, colour, skip, delay } = props;
function DelayedScheduleItem({ timeStart, timeEnd, title, colour, skip, delay }: ScheduleItemProps) {
const start = formatTime(timeStart, formatOptions);
const end = formatTime(timeEnd, formatOptions);
const delayedStart = formatTime(timeStart + delay, formatOptions);
@@ -95,9 +92,7 @@ function DelayedScheduleItem(props: ScheduleItemProps) {
);
}
function ExpectedScheduleItem(props: ScheduleItemProps) {
const { timeStart, timeEnd, title, colour, skip, delay } = props;
function ExpectedScheduleItem({ timeStart, timeEnd, title, colour, skip, delay }: ScheduleItemProps) {
return (
<li className={cx(['entry', skip && 'entry--skip'])}>
<div className='entry-times'>
@@ -116,12 +111,11 @@ interface ExpectedTimeProps {
delay: number;
}
function ExpectedTime(props: ExpectedTimeProps) {
const { time, delay } = props;
function ExpectedTime({ time, delay }: ExpectedTimeProps) {
const { offset } = useRuntimeOffset();
const expectedOffset = offset - delay;
const expectedTime = formatTime(time - offset, formatOptions);
const expectedTime = formatTime(time + offset, formatOptions);
const expectedState = getOffsetState(expectedOffset);
return <SuperscriptTime className={`entry-times--${expectedState}`} time={expectedTime} />;
@@ -1,14 +1,23 @@
import { useMemo } from 'react';
import { useSearchParams } from 'react-router';
import { SelectOption } from '../../../common/components/select/Select';
import { OptionTitle } from '../../../common/components/view-params-editor/constants';
import { ViewOption } from '../../../common/components/view-params-editor/viewParams.types';
import type { ViewOption } from '../../../common/components/view-params-editor/viewParams.types';
import { isStringBoolean } from '../../../features/viewers/common/viewUtils';
export const scheduleOptions: ViewOption = {
export const getScheduleOptions = (customFieldOptions: SelectOption[]): ViewOption => ({
title: OptionTitle.Schedule,
collapsible: true,
options: [
{
id: 'filter',
title: 'Filter',
description: 'Hide events without data in the selected custom field',
type: 'option',
values: customFieldOptions,
defaultValue: 'None',
},
{
id: 'stopCycle',
title: 'Stop cycling through event pages',
@@ -31,9 +40,10 @@ export const scheduleOptions: ViewOption = {
defaultValue: false,
},
],
};
});
type ScheduleOptions = {
filter: string | null;
cycleInterval: number;
stopCycle: boolean;
showExpected: boolean;
@@ -41,6 +51,7 @@ type ScheduleOptions = {
function getScheduleOptionsFromParams(searchParams: URLSearchParams): ScheduleOptions {
return {
filter: searchParams.get('filter'),
cycleInterval: Number(searchParams.get('cycleInterval')) || 10,
stopCycle: isStringBoolean(searchParams.get('stopCycle')),
showExpected: isStringBoolean(searchParams.get('showExpected')),
@@ -141,7 +141,7 @@ $item-height: 3.5rem;
}
.sub__schedule--delayed {
color: $delay-color;
color: $ontime-delay-text;
}
.sub__schedule--strike {
@@ -53,6 +53,7 @@ export default function CountdownSubscriptions({ subscribedEvents, goToEditMode
scrollRef,
doFollow: !lockAutoScroll,
topOffset: 0,
followTrigger: selectedEventId,
});
// reset scroll if nothing is selected
@@ -11,14 +11,14 @@ import {
import { ColumnDef } from '@tanstack/react-table';
import type { ExtendedEntry } from '../../../common/utils/rundownMetadata';
import useColumnManager from '../cuesheet-table/useColumnManager';
import { useColumnOrder } from '../cuesheet-table/useColumnManager';
interface CuesheetDndProps {
columns: ColumnDef<ExtendedEntry>[];
}
export default function CuesheetDnd({ columns, children }: PropsWithChildren<CuesheetDndProps>) {
const { columnOrder, saveColumnOrder } = useColumnManager(columns);
const { columnOrder, saveColumnOrder } = useColumnOrder(columns);
const sensors = useSensors(
useSensor(PointerSensor, {
@@ -13,14 +13,14 @@ import type { ExtendedEntry } from '../../../common/utils/rundownMetadata';
import { AppMode } from '../../../ontimeConfig';
import { usePersistedCuesheetOptions } from '../cuesheet.options';
import CuesheetHeader from './cuesheet-table-elements/CuesheetHeader';
import { CuesheetHeader, SortableCuesheetHeader } from './cuesheet-table-elements/CuesheetHeader';
import DelayRow from './cuesheet-table-elements/DelayRow';
import EventRow from './cuesheet-table-elements/EventRow';
import GroupRow from './cuesheet-table-elements/GroupRow';
import MilestoneRow from './cuesheet-table-elements/MilestoneRow';
import CuesheetTableMenu from './cuesheet-table-menu/CuesheetTableMenu';
import CuesheetTableSettings from './cuesheet-table-settings/CuesheetTableSettings';
import useColumnManager from './useColumnManager';
import { useColumnOrder, useColumnSizes, useColumnVisibility } from './useColumnManager';
import style from './CuesheetTable.module.scss';
@@ -79,8 +79,9 @@ export default function CuesheetTable({ columns, cuesheetMode }: CuesheetTablePr
[cuesheetMode, data, hideIndexColumn, hideTableSeconds, showDelayedTimes, updateEntry, updateTimer],
);
const { columnVisibility, columnOrder, columnSizing, resetColumnOrder, setColumnVisibility, setColumnSizing } =
useColumnManager(columns);
const { columnOrder, resetColumnOrder } = useColumnOrder(columns);
const { columnSizing, setColumnSizing } = useColumnSizes();
const { columnVisibility, setColumnVisibility } = useColumnVisibility();
const table = useReactTable({
data,
@@ -237,11 +238,14 @@ export default function CuesheetTable({ columns, cuesheetMode }: CuesheetTablePr
TableHead: (virtuosoProps) => <thead className={style.tableHeader} {...virtuosoProps} />,
}}
fixedHeaderContent={() => {
return table
.getHeaderGroups()
.map((headerGroup) => (
<CuesheetHeader key={headerGroup.id} cuesheetMode={cuesheetMode} headerGroup={headerGroup} />
));
return table.getHeaderGroups().map((headerGroup) => {
const HeaderComponent = table.getState().columnSizingInfo.isResizingColumn
? CuesheetHeader
: SortableCuesheetHeader;
// if the table is being resized, we render non-sortable headers to avoid performance issues
return <HeaderComponent key={headerGroup.id} cuesheetMode={cuesheetMode} headerGroup={headerGroup} />;
});
}}
/>
@@ -7,7 +7,7 @@ import { getAccessibleColour } from '../../../../common/utils/styleUtils';
import { AppMode } from '../../../../ontimeConfig';
import { usePersistedCuesheetOptions } from '../../cuesheet.options';
import { SortableCell } from './SortableCell';
import { Draggable, SortableCell, TableCell } from './SortableCell';
import style from '../CuesheetTable.module.scss';
@@ -16,8 +16,9 @@ interface CuesheetHeaderProps {
cuesheetMode: AppMode;
}
export default function CuesheetHeader({ headerGroup, cuesheetMode }: CuesheetHeaderProps) {
export function SortableCuesheetHeader({ headerGroup, cuesheetMode }: CuesheetHeaderProps) {
const hideIndexColumn = usePersistedCuesheetOptions((state) => state.hideIndexColumn);
return (
<tr key={headerGroup.id}>
{cuesheetMode === AppMode.Edit && <th className={style.actionColumn} tabIndex={-1} />}
@@ -43,8 +44,10 @@ export default function CuesheetHeader({ headerGroup, cuesheetMode }: CuesheetHe
return (
<SortableCell
key={header.column.columnDef.id}
header={header}
columnId={header.column.id}
colSpan={header.colSpan}
injectedStyles={{ width: `calc(var(--header-${header?.id}-size) * 1px)`, ...customStyles }}
draggable={<Draggable header={header} />}
>
{header.isPlaceholder ? null : flexRender(header.column.columnDef.header, header.getContext())}
</SortableCell>
@@ -54,3 +57,43 @@ export default function CuesheetHeader({ headerGroup, cuesheetMode }: CuesheetHe
</tr>
);
}
export function CuesheetHeader({ headerGroup, cuesheetMode }: CuesheetHeaderProps) {
const hideIndexColumn = usePersistedCuesheetOptions((state) => state.hideIndexColumn);
return (
<tr key={headerGroup.id}>
{cuesheetMode === AppMode.Edit && <th className={style.actionColumn} tabIndex={-1} />}
{!hideIndexColumn && (
<th className={style.indexColumn} tabIndex={-1}>
#
</th>
)}
{headerGroup.headers.map((header) => {
const customBackground = header.column.columnDef.meta?.colour;
const canWrite = header.column.columnDef.meta?.canWrite;
const customStyles: CSSProperties = {
opacity: canWrite ? 1 : 0.6,
};
if (customBackground) {
const customColour = getAccessibleColour(customBackground);
customStyles.backgroundColor = customColour.backgroundColor;
customStyles.color = customColour.color;
}
return (
<TableCell
key={header.column.columnDef.id}
columnId={header.column.id}
colSpan={header.colSpan}
injectedStyles={{ width: `calc(var(--header-${header?.id}-size) * 1px)`, ...customStyles }}
draggable={<Draggable header={header} />}
>
{header.isPlaceholder ? null : flexRender(header.column.columnDef.header, header.getContext())}
</TableCell>
);
})}
</tr>
);
}
@@ -4,3 +4,7 @@
padding-top: 0.25em;
width: 100%;
}
.multiline {
white-space: break-spaces;
}
@@ -2,6 +2,10 @@ import { PropsWithChildren } from 'react';
import style from './GhostedText.module.scss';
export default function GhostedText({ children }: PropsWithChildren) {
return <div className={style.ghostedText}>{children}</div>;
interface GhostedTextProps {
multiline?: boolean;
}
export default function GhostedText({ children, multiline }: PropsWithChildren<GhostedTextProps>) {
return <div className={`${style.ghostedText} ${multiline ? style.multiline : ''}`}>{children}</div>;
}
@@ -8,16 +8,16 @@ import type { ExtendedEntry } from '../../../../common/utils/rundownMetadata';
import style from '../CuesheetTable.module.scss';
interface SortableCellProps {
header: Header<ExtendedEntry, unknown>;
columnId: string;
colSpan: number;
injectedStyles: CSSProperties;
children: ReactNode;
draggable: ReactNode;
}
export function SortableCell({ header, injectedStyles, children }: SortableCellProps) {
const { column, colSpan } = header;
export function SortableCell({ columnId, colSpan, injectedStyles, children, draggable }: SortableCellProps) {
const { attributes, listeners, setNodeRef, transform, transition, isDragging } = useSortable({
id: column.id,
id: columnId,
});
// build drag styles
@@ -34,12 +34,31 @@ export function SortableCell({ header, injectedStyles, children }: SortableCellP
<div {...attributes} {...listeners}>
{children}
</div>
<div
onDoubleClick={() => header.column.resetSize()}
onMouseDown={header.getResizeHandler()}
onTouchStart={header.getResizeHandler()}
className={style.resizer}
/>
{draggable}
</th>
);
}
export function TableCell({ colSpan, injectedStyles, children, draggable }: SortableCellProps) {
return (
<th style={injectedStyles} colSpan={colSpan} tabIndex={-1}>
<div>{children}</div>
{draggable}
</th>
);
}
interface DraggableProps {
header: Header<ExtendedEntry, unknown>;
}
export function Draggable({ header }: DraggableProps) {
return (
<div
onDoubleClick={() => header.column.resetSize()}
onMouseDown={header.getResizeHandler()}
onTouchStart={header.getResizeHandler()}
className={style.resizer}
/>
);
}
@@ -143,7 +143,7 @@ function MakeMultiLineField({ row, column, table }: CellContext<ExtendedEntry, u
const canWrite = column.columnDef.meta?.canWrite;
if (!canWrite) {
return <GhostedText>{initialValue}</GhostedText>;
return <GhostedText multiline>{initialValue}</GhostedText>;
}
return <MultiLineCell initialValue={initialValue as string} handleUpdate={update} />;
@@ -1,6 +1,6 @@
import { useCallback, useEffect, useState } from 'react';
import { useLocalStorage } from '@mantine/hooks';
import { ColumnDef } from '@tanstack/react-table';
import { ColumnDef, ColumnSizingState, Updater } from '@tanstack/react-table';
import { debounce } from '../../../common/utils/debounce';
import { makeStageKey } from '../../../common/utils/localStorage';
@@ -14,17 +14,8 @@ const saveSizesToStorage = debounce((sizes: Record<string, number>) => {
localStorage.setItem(tableSizesKey, JSON.stringify(sizes));
}, 500);
export default function useColumnManager(columns: ColumnDef<ExtendedEntry>[]) {
const [columnVisibility, setColumnVisibility] = useLocalStorage({
key: tableHiddenKey,
defaultValue: {},
});
const [columnOrder, saveColumnOrder] = useLocalStorage<string[]>({
key: tableOrderKey,
defaultValue: columns.map((col) => col.id as string),
});
const [columnSizing, setColumnSizingState] = useState(() => {
export function useColumnSizes() {
const [columnSizing, setColumnSizingState] = useState<Record<string, number>>(() => {
try {
const stored = localStorage.getItem(tableSizesKey);
return stored ? JSON.parse(stored) : {};
@@ -38,10 +29,22 @@ export default function useColumnManager(columns: ColumnDef<ExtendedEntry>[]) {
saveSizesToStorage(columnSizing);
}, [columnSizing]);
const setColumnSizing = useCallback((sizes: typeof columnSizing) => {
setColumnSizingState(sizes);
const setColumnSizing = useCallback((sizesOrUpdater: Updater<ColumnSizingState>) => {
setColumnSizingState(sizesOrUpdater);
}, []);
return {
columnSizing,
setColumnSizing,
};
}
export function useColumnOrder(columns: ColumnDef<ExtendedEntry>[]) {
const [columnOrder, saveColumnOrder] = useLocalStorage<string[]>({
key: tableOrderKey,
defaultValue: columns.map((col) => col.id as string),
});
// update column order if columns change
useEffect(() => {
const newColumns = columns.map((col) => col.id as string);
@@ -55,12 +58,20 @@ export default function useColumnManager(columns: ColumnDef<ExtendedEntry>[]) {
}, [columns, saveColumnOrder]);
return {
columnVisibility,
columnOrder,
columnSizing,
resetColumnOrder,
setColumnVisibility,
saveColumnOrder,
setColumnSizing,
resetColumnOrder,
};
}
export function useColumnVisibility() {
const [columnVisibility, setColumnVisibility] = useLocalStorage({
key: tableHiddenKey,
defaultValue: {},
});
return {
columnVisibility,
setColumnVisibility,
};
}
@@ -66,4 +66,5 @@
.scrollContainer {
max-height: 70vh;
overflow: auto;
padding-top: 1rem;
}
@@ -71,8 +71,7 @@ export default function Finder({ isOpen, onClose }: FinderProps) {
results.map((entry, index) => {
const isSelected = selected === index;
const displayIndex = entry.type === SupportedEntry.Event ? entry.eventIndex : '-';
const displayCue = entry.type === SupportedEntry.Event ? entry.cue : '';
const colour = entry.type === SupportedEntry.Event ? entry.colour : '';
const displayCue = 'cue' in entry ? entry.cue : '';
return (
<li
@@ -83,7 +82,7 @@ export default function Finder({ isOpen, onClose }: FinderProps) {
onClick={submit}
>
<div className={style.data}>
<div className={style.index} style={{ '--color': colour }}>
<div className={style.index} style={{ '--color': entry.colour }}>
{displayIndex}
</div>
<div className={style.cue}>{displayCue}</div>
@@ -99,7 +98,7 @@ export default function Finder({ isOpen, onClose }: FinderProps) {
footerElements={
<div className={style.footer}>
Use the keywords <span className={style.em}>cue</span>, <span className={style.em}>index</span> or
<span className={style.em}>title</span> to filter search
<span className={style.em}>title</span> to filter search.
</div>
}
/>
@@ -1,6 +1,6 @@
import { ChangeEvent, useCallback, useEffect, useRef, useState } from 'react';
import { useSessionStorage } from '@mantine/hooks';
import { EntryId, isOntimeEvent, isOntimeGroup, MaybeString, SupportedEntry } from 'ontime-types';
import { EntryId, isOntimeEvent, isOntimeGroup, isOntimeMilestone, MaybeString, SupportedEntry } from 'ontime-types';
import { useFlatRundown } from '../../../common/hooks-query/useRundown';
import { useEventSelection } from '../../../features/rundown/useEventSelection';
@@ -9,14 +9,15 @@ const maxResults = 12;
type FilterableGroup = {
type: SupportedEntry.Group;
id: string;
id: EntryId;
index: number;
title: string;
colour: string;
};
type FilterableEvent = {
type: SupportedEntry.Event;
id: string;
id: EntryId;
index: number;
eventIndex: number;
title: string;
@@ -25,7 +26,17 @@ type FilterableEvent = {
parent: MaybeString;
};
type FilterableEntry = FilterableGroup | FilterableEvent;
type FilterableMilestone = {
type: SupportedEntry.Milestone;
id: EntryId;
index: number;
title: string;
cue: string;
colour: string;
parent: MaybeString;
};
type FilterableEntry = FilterableGroup | FilterableEvent | FilterableMilestone;
export default function useFinder() {
const { data, rundownId } = useFlatRundown();
@@ -59,7 +70,7 @@ export default function useFinder() {
lastSearchString.current = searchValue;
if (searchValue.startsWith('index ')) {
const searchString = searchValue.replace('index ', '').trim();
const searchString = searchValue.slice('index '.length).trim();
const { results, error } = searchByIndex(searchString);
setResults(results);
setError(error);
@@ -67,14 +78,14 @@ export default function useFinder() {
}
if (searchValue.startsWith('cue ')) {
const searchString = searchValue.replace('cue ', '').trim();
const searchString = searchValue.slice('cue '.length).trim();
const { results, error } = searchByCue(searchString);
setResults(results);
setError(error);
return;
}
const searchString = searchValue.replace('title ', '').trim();
const searchString = searchValue.startsWith('title ') ? searchValue.slice('title '.length).trim() : searchValue;
const { results, error } = searchByTitle(searchString);
setResults(results);
setError(error);
@@ -162,33 +173,46 @@ export default function useFinder() {
break;
}
const event = data[i];
if (isOntimeEvent(event)) {
if (event.title.toLowerCase().includes(searchString)) {
const entry = data[i];
if (isOntimeEvent(entry)) {
if (entry.title.toLowerCase().includes(searchString)) {
remaining--;
results.push({
type: SupportedEntry.Event,
id: event.id,
id: entry.id,
index: i,
eventIndex,
title: event.title,
cue: event.cue,
colour: event.colour,
parent: event.parent,
title: entry.title,
cue: entry.cue,
colour: entry.colour,
parent: entry.parent,
} satisfies FilterableEvent);
}
eventIndex++;
}
if (isOntimeGroup(event)) {
if (event.title.toLowerCase().includes(searchString)) {
} else if (isOntimeGroup(entry)) {
if (entry.title.toLowerCase().includes(searchString)) {
remaining--;
results.push({
type: SupportedEntry.Group,
id: event.id,
id: entry.id,
index: i,
title: event.title,
title: entry.title,
colour: entry.colour,
} satisfies FilterableGroup);
}
} else if (isOntimeMilestone(entry)) {
if (entry.title.toLowerCase().includes(searchString)) {
remaining--;
results.push({
type: SupportedEntry.Milestone,
id: entry.id,
index: i,
title: entry.title,
cue: entry.cue,
colour: entry.colour,
parent: entry.parent,
} satisfies FilterableMilestone);
}
}
}
return { results, error: null };
@@ -200,7 +224,7 @@ export default function useFinder() {
const select = useCallback(
(selectedEvent: FilterableEntry) => {
// First expand the parent group if this is an event inside a group
if (selectedEvent.type === SupportedEntry.Event && selectedEvent.parent !== null) {
if ('parent' in selectedEvent && selectedEvent.parent !== null) {
// Try direct state update instead of using callback
const currentGroups = [...new Set(collapsedGroups)];
const newGroups = currentGroups.filter((id) => id !== selectedEvent.parent);
@@ -105,7 +105,7 @@ $timeline-color: color-mix(in srgb, transparent 60%, var(--background-color-over
}
.delay {
color: $delay-color;
color: $ontime-delay-text;
}
.timeOverview {
@@ -117,7 +117,7 @@ $timeline-color: color-mix(in srgb, transparent 60%, var(--background-color-over
.cross {
text-decoration: line-through;
text-decoration-thickness: 2px;
text-decoration-color: $delay-color;
text-decoration-color: $ontime-delay-text;
}
.separeLeft {
@@ -183,7 +183,7 @@ export function getUpcomingEvents(events: PlayableEvent[], selectedId: MaybeStri
* Utility function calculates time to start
*/
export function getTimeToStart(now: number, start: number, delay: number, offset: number): number {
return start + delay - now - offset;
return start + delay - now + offset;
}
interface TimelineLayout {
+3 -3
View File
@@ -68,7 +68,7 @@ function Timer({ customFields, projectData, isMirrored, settings, viewSettings }
hidePhase,
font,
keyColour,
textColour,
timerColour,
} = useTimerOptions();
const { getLocalizedString } = useTranslation();
@@ -131,11 +131,11 @@ function Timer({ customFields, projectData, isMirrored, settings, viewSettings }
);
// gather presentation styles
const timerColour = getTimerColour(viewSettings, textColour, showWarning, showDanger);
const resolvedTimerColour = getTimerColour(viewSettings, timerColour, showWarning, showDanger);
const { timerFontSize, externalFontSize } = getEstimatedFontSize(display, secondaryContent);
const userStyles = {
...(keyColour && { '--timer-bg': keyColour }),
...(textColour && { '--timer-colour': timerColour }),
...(resolvedTimerColour && { '--timer-colour': resolvedTimerColour }),
...(font && { '--timer-font': font }),
};
+9 -7
View File
@@ -25,10 +25,12 @@ const timerDisplayOptions: SelectOption[] = [
export const getTimerOptions = (timeFormat: string, customFields: CustomFields): ViewOption[] => {
const mainOptions = makeOptionsFromCustomFields(customFields, [
{ value: 'none', label: 'None' },
{ value: 'title', label: 'Title' },
{ value: 'note', label: 'Note' },
]);
const secondaryOptions = makeOptionsFromCustomFields(customFields, [
{ value: 'none', label: 'None' },
{ value: 'title', label: 'Title' },
{ value: 'note', label: 'Note' },
]);
@@ -84,7 +86,7 @@ export const getTimerOptions = (timeFormat: string, customFields: CustomFields):
description: 'Select the data source for the main text',
type: 'option',
values: mainOptions,
defaultValue: 'Title',
defaultValue: 'title',
},
{
id: 'secondary-src',
@@ -92,7 +94,7 @@ export const getTimerOptions = (timeFormat: string, customFields: CustomFields):
description: 'Select the data source for the secondary text',
type: 'option',
values: secondaryOptions,
defaultValue: '',
defaultValue: 'none',
},
],
},
@@ -163,9 +165,9 @@ export const getTimerOptions = (timeFormat: string, customFields: CustomFields):
defaultValue: '101010',
},
{
id: 'textColour',
title: 'Text Colour',
description: 'Text colour. Default: #f6f6f6',
id: 'timerColour',
title: 'Timer Colour',
description: 'Timer colour. Default: #f6f6f6',
type: 'colour',
defaultValue: 'f6f6f6',
},
@@ -191,7 +193,7 @@ type TimerOptions = {
hidePhase: boolean;
font?: string;
keyColour?: string;
textColour?: string;
timerColour?: string;
};
/**
@@ -226,7 +228,7 @@ function getOptionsFromParams(searchParams: URLSearchParams, defaultValues?: URL
font: getValue('font') ?? undefined,
keyColour: makeColourString(getValue('keyColour')),
textColour: makeColourString(getValue('textColour')),
timerColour: makeColourString(getValue('timerColour')),
};
}
+2 -1
View File
@@ -32,7 +32,8 @@
"preserveConstEnums": true,
"allowJs": true,
"baseUrl": "src",
"jsx": "react-jsx"
"jsx": "react-jsx",
"outDir": "build"
},
"include": [
"src"
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "ontime-electron",
"version": "4.0.0-beta.2",
"version": "4.0.0-beta.3",
"author": "Carlos Valente",
"description": "Time keeping for live events",
"repository": "https://github.com/cpvalente/ontime",
+1
View File
@@ -0,0 +1 @@
*.tgz
+17
View File
@@ -0,0 +1,17 @@
# Ontime Resolver
Congratulations! You got this far into Ontime's rabbit hole and want to manage your installation.
The Resolver is an attempt to expose our ontime's api so it is easier to integrate with
## Links
- [Ontime's repository](https://github.com/cpvalente/ontime)
- [Ontime's documentation](https://docs.getontime.no/)
- [Ontime's website](https://getontime.no/)
## Sponsoring
You can help the development of this project or say thank you with a one time donation. \
See the [terms of donations](https://github.com/cpvalente/ontime/blob/master/SPONSOR.md)
[![](https://img.shields.io/static/v1?label=Sponsor&message=%E2%9D%A4&logo=GitHub&color=%23fe8e86)](https://github.com/sponsors/cpvalente)
[![](https://img.shields.io/static/v1?label=Buy%20me%20a%20coffee&message=%E2%9D%A4&logo=buymeacoffee&color=%23fe8e86)](https://www.buymeacoffee.com/cpvalente)
+29
View File
@@ -0,0 +1,29 @@
{
"name": "@getontime/resolver",
"version": "4.0.0-beta.3",
"type": "module",
"repository": "https://github.com/cpvalente/ontime",
"types": "./dist/main.d.ts",
"main": "./dist/main.js",
"private": true,
"description": "shared typings for ontime",
"scripts": {
"lint": "eslint . --quiet",
"prebuild": "pnpm rimraf ./dist",
"build": "tsup && pnpm rimraf ./dist/index.js",
"postbuild": "pnpm rimraf ./dist/index.d.ts"
},
"keywords": ["ontime", "resolver", "parser"],
"author": "",
"license": "AGPL-3.0-only",
"devDependencies": {
"@sprout2000/esbuild-copy-plugin": "^1.1.19",
"@typescript-eslint/parser": "catalog:",
"eslint": "catalog:",
"tsup": "^8.5.0",
"rimraf": "catalog:",
"typescript": "catalog:",
"ontime-types": "workspace:^4.0.0"
},
"files": ["dist"]
}
+20
View File
@@ -0,0 +1,20 @@
// api
export { MessageTag, RefetchKey } from 'ontime-types';
export type { ApiAction, ApiActionTag, ApiResponse } from 'ontime-types';
export type { WsPacketToClient, WsPacketToServer } from 'ontime-types';
// stores
export type { RuntimeStore, TimerState, MessageState, RundownState, Offset } from 'ontime-types';
export { TimerPhase, Playback, runtimeStorePlaceholder, OffsetMode } from 'ontime-types';
// aux timer
export type { SimpleTimerState } from 'ontime-types';
export { SimplePlayback, SimpleDirection } from 'ontime-types';
// entries
export type { OntimeEvent, OntimeGroup, EntryCustomFields, CustomFields, Rundown } from 'ontime-types';
export { SupportedEntry, isOntimeEvent, isOntimeGroup, isOntimeDelay, isOntimeMilestone } from 'ontime-types';
// functions
export { isWsPacketToClient } from './websocket.js';
export type { SocketSender } from './websocket.js';
+18
View File
@@ -0,0 +1,18 @@
import { ApiAction, ApiActionTag, MessageTag, WsPacketToClient, WsPacketToServer } from 'ontime-types';
/**
* A helper type for sending correct websocket messages to ontime
*/
export type SocketSender = <T extends MessageTag | ApiActionTag>(
tag: T,
payload: T extends MessageTag
? Pick<WsPacketToServer & { tag: T }, 'payload'>['payload']
: Pick<ApiAction & { tag: T }, 'payload'>['payload'],
) => void;
/**
* a soft type guard for WS packets
*/
export function isWsPacketToClient(data: unknown): data is WsPacketToClient {
return typeof data === 'object' && data !== null && 'tag' in data && 'payload' in data;
}
+35
View File
@@ -0,0 +1,35 @@
{
"compilerOptions": {
"target": "esnext",
"module": "preserve",
"moduleResolution": "bundler",
"resolveJsonModule": true,
"isolatedModules": true,
"skipLibCheck": true,
"allowSyntheticDefaultImports": true,
"strict": true,
"forceConsistentCasingInFileNames": true,
"noImplicitAny": true,
"noImplicitReturns": true,
"allowUnreachableCode": false,
"allowUnusedLabels": false,
"noImplicitThis": true,
"strictNullChecks": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noFallthroughCasesInSwitch": true,
"removeComments": true,
"preserveConstEnums": true,
"allowJs": true,
"declaration": true,
"outDir": "dist",
"sourceMap": true,
"baseUrl": "src",
},
"include": [
"src",
],
"exclude": [
"node_modules",
]
}
+20
View File
@@ -0,0 +1,20 @@
import { defineConfig } from 'tsup';
import copyPlugin from '@sprout2000/esbuild-copy-plugin';
export default defineConfig({
clean: false, //we can't use clean as i dose so after the copy plugin runs
entry: ['src/main.ts'],
outDir: 'dist',
bundle: true,
dts: { resolve: true },
format: 'esm',
target: 'esnext',
platform: 'node',
esbuildPlugins: [
copyPlugin.copyPlugin({
src: './node_modules/ontime-types/dist',
dest: './dist',
}),
],
});
+2 -2
View File
@@ -2,7 +2,7 @@
"name": "ontime-server",
"type": "module",
"main": "src/index.ts",
"version": "4.0.0-beta.2",
"version": "4.0.0-beta.3",
"exports": "./src/index.js",
"dependencies": {
"@googleapis/sheets": "^5.0.5",
@@ -41,7 +41,7 @@
"prettier": "catalog:",
"server-timing": "^3.3.3",
"shx": "^0.3.4",
"ts-essentials": "^10.0.3",
"ts-essentials": "catalog:",
"tsx": "^4.19.2",
"typescript": "catalog:",
"vitest": "catalog:"
@@ -10,7 +10,7 @@ export const dataFromExcelTemplate = [
'Timer type',
'Count to end',
'Skip',
'Notes',
'Note',
't0',
'Test1',
'test2',
@@ -1,5 +1,5 @@
import {
ApiAction,
ApiActionTag,
MessageState,
OffsetMode,
OntimeEvent,
@@ -30,7 +30,7 @@ let lastRequest: Date | null = null;
export function dispatchFromAdapter(tag: string, payload: unknown, _source?: 'osc' | 'ws' | 'http') {
const action = tag.toLowerCase();
const handler = actionHandlers[action as ApiAction];
const handler = actionHandlers[action as ApiActionTag];
lastRequest = new Date();
if (handler) {
@@ -46,7 +46,7 @@ export function getLastRequest() {
type ActionHandler = (payload: unknown) => { payload: unknown };
const actionHandlers: Record<ApiAction, ActionHandler> = {
const actionHandlers: Record<ApiActionTag, ActionHandler> = {
/* General */
version: () => ({ payload: ONTIME_VERSION }),
poll: () => ({
+1 -1
View File
@@ -337,7 +337,7 @@ export const demoDb: DatabaseModel = {
alias: 'minimal',
target: OntimeView.Timer,
search:
'hideclock=true&hidecards=true&hideprogress=true&hidemessage=true&hidesecondary=true&hidelogo=true&font=arial+black&keycolour=00ff00&textcolour=ffffff',
'hideclock=true&hidecards=true&hideprogress=true&hidemessage=true&hidesecondary=true&hidelogo=true&font=arial+black&keycolour=00ff00&timerColour=ffffff',
},
],
customFields: {
@@ -1,7 +1,8 @@
import { dayInMs, millisToString } from 'ontime-utils';
import { dayInMs, MILLIS_PER_HOUR, millisToString } from 'ontime-utils';
import { EndAction, Playback, TimeStrategy, TimerPhase, TimerType } from 'ontime-types';
import {
findDayOffset,
getCurrent,
getExpectedFinish,
getRuntimeOffset,
@@ -726,6 +727,7 @@ describe('getRuntimeOffset()', () => {
eventNow: {
id: '1',
timeStart: 100,
dayOffset: 0,
},
timer: {
startedAt: 150,
@@ -738,7 +740,10 @@ describe('getRuntimeOffset()', () => {
rundown: {
actualStart: 150,
plannedStart: 100,
currentDay: 0,
},
clock: 150,
_startDayOffset: 0,
} as RuntimeState;
const { absolute } = getRuntimeOffset(state);
@@ -750,6 +755,7 @@ describe('getRuntimeOffset()', () => {
eventNow: {
id: '1',
timeStart: 100,
dayOffset: 0,
},
timer: {
startedAt: 150, // we started 50ms delayed
@@ -762,7 +768,9 @@ describe('getRuntimeOffset()', () => {
rundown: {
actualStart: 150,
plannedStart: 100,
currentDay: 0,
},
_startDayOffset: 0,
} as RuntimeState;
const { absolute } = getRuntimeOffset(state);
@@ -775,6 +783,7 @@ describe('getRuntimeOffset()', () => {
id: '1',
timeStart: 100,
timeEnd: 140,
dayOffset: 0,
},
timer: {
startedAt: 100, // we started ontime
@@ -787,7 +796,9 @@ describe('getRuntimeOffset()', () => {
rundown: {
actualStart: 100,
plannedStart: 100,
currentDay: 0,
},
_startDayOffset: 0,
} as RuntimeState;
const { absolute } = getRuntimeOffset(state);
@@ -800,6 +811,7 @@ describe('getRuntimeOffset()', () => {
id: '1',
timeStart: 100,
timeEnd: 150,
dayOffset: 0,
},
clock: 150,
timer: {
@@ -813,7 +825,9 @@ describe('getRuntimeOffset()', () => {
rundown: {
actualStart: 100,
plannedStart: 100,
currentDay: 0,
},
_startDayOffset: 0,
} as RuntimeState;
const { absolute } = getRuntimeOffset(state);
@@ -830,6 +844,7 @@ describe('getRuntimeOffset()', () => {
duration: 3600000,
timeStrategy: 'lock-duration',
linkStart: false,
dayOffset: 0,
},
rundown: {
selectedEventIndex: 0,
@@ -837,6 +852,7 @@ describe('getRuntimeOffset()', () => {
plannedStart: 77400000,
plannedEnd: 84600000,
actualStart: null,
currentDay: 0,
},
offset: {
absolute: -77400000,
@@ -852,6 +868,7 @@ describe('getRuntimeOffset()', () => {
secondaryTimer: null,
startedAt: null,
},
_startDayOffset: 0,
_timer: { pausedAt: null },
} as RuntimeState;
@@ -874,6 +891,7 @@ describe('getRuntimeOffset()', () => {
endAction: EndAction.None,
timerType: TimerType.CountDown,
countToEnd: true,
dayOffset: 0,
skip: false,
note: '',
colour: '',
@@ -890,6 +908,7 @@ describe('getRuntimeOffset()', () => {
plannedStart: 77400000, // 21:30:00
plannedEnd: 81000000, // 22:30:00
actualStart: 78000000, // 21:40:00
currentDay: 0,
},
offset: {
absolute: 0,
@@ -905,6 +924,7 @@ describe('getRuntimeOffset()', () => {
secondaryTimer: null,
startedAt: 78000000,
},
_startDayOffset: 0,
_timer: { pausedAt: null },
} as RuntimeState;
@@ -922,6 +942,7 @@ describe('getRuntimeOffset()', () => {
timeStart: 77400000, // 21:30:00
timeEnd: 81000000, // 22:30:00
duration: 3600000, // 01:00:00
dayOffset: 0,
timeStrategy: TimeStrategy.LockEnd,
linkStart: false,
endAction: EndAction.None,
@@ -943,6 +964,7 @@ describe('getRuntimeOffset()', () => {
plannedStart: 77400000, // 21:30:00
plannedEnd: 81000000, // 22:30:00
actualStart: 78000000, // 21:40:00
currentDay: 0,
},
offset: {
absolute: 0,
@@ -958,6 +980,7 @@ describe('getRuntimeOffset()', () => {
secondaryTimer: null,
startedAt: 78000000,
},
_startDayOffset: 0,
_timer: { pausedAt: null },
} as RuntimeState;
@@ -978,6 +1001,7 @@ describe('getRuntimeOffset()', () => {
endAction: EndAction.None,
timerType: TimerType.CountDown,
countToEnd: true,
dayOffset: 0,
},
rundown: {
selectedEventIndex: 0,
@@ -985,6 +1009,7 @@ describe('getRuntimeOffset()', () => {
plannedStart: 77400000, // 21:30:00
plannedEnd: 81000000, // 22:30:00
actualStart: 82000000, // 22:46:40 <--- started now
currentDay: 0,
},
offset: {
absolute: 0,
@@ -1000,6 +1025,7 @@ describe('getRuntimeOffset()', () => {
secondaryTimer: null,
startedAt: 82000000, // <--- started now
},
_startDayOffset: 0,
_timer: { pausedAt: null },
} as RuntimeState;
@@ -1017,6 +1043,7 @@ describe('getRuntimeOffset() relative', () => {
eventNow: {
id: '1',
timeStart: 150,
dayOffset: 0,
},
timer: {
startedAt: 150,
@@ -1029,7 +1056,9 @@ describe('getRuntimeOffset() relative', () => {
rundown: {
actualStart: 150,
plannedStart: 150,
currentDay: 0,
},
_startDayOffset: 0,
} as RuntimeState;
const { absolute, relative } = getRuntimeOffset(state);
@@ -1041,6 +1070,7 @@ describe('getRuntimeOffset() relative', () => {
eventNow: {
id: '1',
timeStart: 100,
dayOffset: 0,
},
timer: {
startedAt: 150,
@@ -1053,7 +1083,9 @@ describe('getRuntimeOffset() relative', () => {
rundown: {
actualStart: 150,
plannedStart: 100,
currentDay: 0,
},
_startDayOffset: 0,
} as RuntimeState;
const { absolute, relative } = getRuntimeOffset(state);
@@ -1065,6 +1097,7 @@ describe('getRuntimeOffset() relative', () => {
eventNow: {
id: '1',
timeStart: 150,
dayOffset: 0,
},
timer: {
startedAt: 100,
@@ -1077,7 +1110,9 @@ describe('getRuntimeOffset() relative', () => {
rundown: {
actualStart: 100,
plannedStart: 150,
currentDay: 0,
},
_startDayOffset: 0,
} as RuntimeState;
const { absolute, relative } = getRuntimeOffset(state);
@@ -1258,3 +1293,21 @@ describe('getTimerPhase()', () => {
expect(phase).toBe(TimerPhase.Pending);
});
});
describe('findDay()', () => {
test('finds dayOffset', () => {
//both have 1 hour offset but the clock are on different days
expect(findDayOffset(0, 23 * MILLIS_PER_HOUR)).toBe(-1); // -> 23
expect(findDayOffset(0, 13 * MILLIS_PER_HOUR)).toBe(-1); // -> 13
expect(findDayOffset(0, 12 * MILLIS_PER_HOUR)).toBe(-1); // -> 12
expect(findDayOffset(0, 11 * MILLIS_PER_HOUR)).toBe(0); // -> 11
expect(findDayOffset(1 * MILLIS_PER_HOUR, 0)).toBe(0); // -> -1
//both have 1 hour offset but the clock are on different days
expect(findDayOffset(23 * MILLIS_PER_HOUR, 0)).toBe(1); // -> -23
expect(findDayOffset(13 * MILLIS_PER_HOUR, 0)).toBe(1); // -> -13
expect(findDayOffset(12 * MILLIS_PER_HOUR, 0)).toBe(0); // -> -12
expect(findDayOffset(11 * MILLIS_PER_HOUR, 0)).toBe(0); // -> -11
expect(findDayOffset(22 * MILLIS_PER_HOUR, 23 * MILLIS_PER_HOUR)).toBe(0); // -> 1
});
});
@@ -12,6 +12,7 @@ describe('isRestorePoint()', () => {
addedTime: 2,
pausedAt: 3,
firstStart: 1,
startEpoch: 1,
};
expect(isRestorePoint(restorePoint)).toBe(true);
@@ -22,6 +23,7 @@ describe('isRestorePoint()', () => {
addedTime: 0,
pausedAt: null,
firstStart: 1,
startEpoch: 1,
};
expect(isRestorePoint(restorePoint)).toBe(true);
});
@@ -35,6 +37,7 @@ describe('isRestorePoint()', () => {
addedTime: 0,
pausedAt: null,
groupStartAt: 10,
startEpoch: 1,
};
expect(isRestorePoint(restorePoint)).toBe(false);
});
@@ -56,6 +59,7 @@ describe('isRestorePoint()', () => {
addedTime: 0,
pausedAt: null,
groupStartAt: 10,
startEpoch: 1,
};
expect(isRestorePoint(restorePoint)).toBe(false);
});
@@ -16,6 +16,7 @@ describe('restoreService', () => {
addedTime: 5678,
pausedAt: 9087,
firstStart: 1234,
startEpoch: 1234,
};
const mockRead = vi.fn().mockResolvedValue(expected);
@@ -33,6 +34,7 @@ describe('restoreService', () => {
addedTime: 0,
pausedAt: null,
firstStart: 1234,
startEpoch: 1234,
};
const mockRead = vi.fn().mockResolvedValue(expected);
@@ -79,6 +81,7 @@ describe('restoreService', () => {
addedTime: 1234,
pausedAt: 1234,
firstStart: 1234,
startEpoch: 1234,
};
const mockWrite = vi.fn().mockResolvedValue(undefined);
@@ -95,6 +98,7 @@ describe('restoreService', () => {
addedTime: 5678,
pausedAt: 5678,
firstStart: 5678,
startEpoch: 5678,
};
const mockWrite = vi.fn().mockRejectedValue(new Error('Write failed'));
@@ -20,6 +20,7 @@ export function isRestorePoint(restorePoint: unknown): restorePoint is RestorePo
'addedTime',
'pausedAt',
'firstStart',
'startEpoch',
])
) {
return false;
@@ -49,5 +50,9 @@ export function isRestorePoint(restorePoint: unknown): restorePoint is RestorePo
return false;
}
if (!is.number(restorePoint.startEpoch) && restorePoint.startEpoch !== null) {
return false;
}
return true;
}
@@ -7,4 +7,5 @@ export type RestorePoint = {
addedTime: number;
pausedAt: MaybeNumber;
firstStart: MaybeNumber;
startEpoch: MaybeNumber;
};
@@ -743,6 +743,7 @@ function broadcastResult(_target: any, _propertyKey: string, descriptor: Propert
addedTime: state.timer.addedTime,
pausedAt: state._timer.pausedAt,
firstStart: state.rundown.actualStart,
startEpoch: state._startEpoch,
})
.catch((_e) => {
//we don't do anything with the error here
@@ -412,15 +412,19 @@ export async function upload(sheetId: string, options: ImportMap) {
},
});
// update the corresponding row with event data
sheetOrder.forEach((entryId, index) => {
const isGroupEnd = entryId.startsWith('group-end-');
const id = isGroupEnd ? entryId.split('group-end-')[1] : entryId;
const entry = isGroupEnd
try {
// update the corresponding row with event data
sheetOrder.forEach((entryId, index) => {
const isGroupEnd = entryId.startsWith('group-end-');
const id = isGroupEnd ? entryId.split('group-end-')[1] : entryId;
const entry = isGroupEnd
? ({ id: entryId, type: SupportedEntry.Group } as OntimeGroup)
: structuredClone(rundown.entries[id]);
updateRundown.push(cellRequestFromEvent(entry, index, worksheetId, sheetMetadata));
});
updateRundown.push(cellRequestFromEvent(entry, index, worksheetId, sheetMetadata));
});
} catch (e) {
throw new Error(`Sheet write failed to correctly parse rundown: ${e}`)
}
const writeResponse = await sheets({ version: 'v4', auth: currentAuthClient }).spreadsheets.batchUpdate({
spreadsheetId: sheetId,
@@ -154,12 +154,6 @@ function getCellData(key: OntimeEntryCommonKeys | 'blank', entry: OntimeEntry) {
return {};
}
// we need to flatten the milestones
if (key.startsWith('custom')) {
const customKey = key.split(':')[1];
return { userEnteredValue: { stringValue: entry.custom[customKey] } };
}
// we need to remap the event type to timer type in the case of groups and milestones
if (key === 'timerType') {
if (isOntimeGroup(entry))
@@ -168,6 +162,17 @@ function getCellData(key: OntimeEntryCommonKeys | 'blank', entry: OntimeEntry) {
return { userEnteredValue: { stringValue: entry.timerType } };
}
// all other data is not relevant for the group end entry
if (entry.id.startsWith('group-end')) {
return {};
}
// we need to flatten the milestones
if (key.startsWith('custom')) {
const customKey = key.split(':')[1];
return { userEnteredValue: { stringValue: entry.custom[customKey] } };
}
// typescript cannot guarantee that the key exists for every entry
// so we check for the key existence and assert the type
if (!(key in entry)) return {};
+21 -8
View File
@@ -1,5 +1,6 @@
import { MaybeNumber, TimerPhase } from 'ontime-types';
import { dayInMs, isPlaybackActive } from 'ontime-utils';
import { dayInMs, isPlaybackActive, MILLIS_PER_HOUR } from 'ontime-utils';
import type { RuntimeState } from '../stores/runtimeState.js';
/**
@@ -113,14 +114,14 @@ export function skippedOutOfEvent(state: RuntimeState, previousTime: number, ski
* Negative offset is under time / ahead of schedule
*/
export function getRuntimeOffset(state: RuntimeState): { absolute: number; relative: number } {
const { eventNow, clock } = state;
const { eventNow, clock, _startDayOffset } = state;
const { addedTime, current, startedAt } = state.timer;
// nothing to calculate if there are no loaded events or if we havent started
if (eventNow === null || startedAt === null) {
if (eventNow === null || startedAt === null || _startDayOffset === null) {
return { absolute: 0, relative: 0 };
}
const { countToEnd, timeStart } = eventNow;
const { countToEnd, timeStart, dayOffset } = eventNow;
const { plannedStart, actualStart } = state.rundown;
// eslint-disable-next-line no-unused-labels -- dev code path
@@ -131,8 +132,8 @@ export function getRuntimeOffset(state: RuntimeState): { absolute: number; relat
if (actualStart === null) throw new Error('timerUtils.getRuntimeOffset: state.rundown.plannedStart must be set');
}
// difference between planned event start and actual event start (will be positive if we stared behind )
const eventStartOffset = startedAt - timeStart;
// difference between planned event start and actual event start (will be positive if we started behind)
const eventStartOffset = startedAt + _startDayOffset * dayInMs - (timeStart + dayOffset * dayInMs);
// how long has the event been running over (is a negative number when in over timer so inverted before adding to offset)
const overtime = Math.abs(Math.min(current, 0));
@@ -140,10 +141,11 @@ export function getRuntimeOffset(state: RuntimeState): { absolute: number; relat
// time the playback was paused, the different from now to when we paused is added to the offset TODO: brakes when crossing midnight
const pausedTime = state._timer.pausedAt === null ? 0 : clock - state._timer.pausedAt;
// absolute offset is difference between schedule and playback time
const absolute = eventStartOffset + overtime + pausedTime + addedTime;
// the relative offset i the same as the absolute offset but adjusted relative to the actual start time
const relative = absolute + plannedStart - actualStart;
// the relative offset is the same as the absolute but adjusted relative to the actual start time
const relative = absolute + plannedStart - actualStart - _startDayOffset * dayInMs;
// in case of count to end, the absolute offset is just the overtime
return countToEnd ? { absolute: overtime, relative } : { absolute, relative };
@@ -180,3 +182,14 @@ export function getTimerPhase(state: RuntimeState): TimerPhase {
return TimerPhase.Default;
}
/**
* Finds the day offset relative to an event start
* used byt the runtimeState on first start to get correct offsets
*/
export function findDayOffset(plannedStart: number, clock: number): number {
const distance = clock - plannedStart;
if (distance >= 12 * MILLIS_PER_HOUR) return -1;
if (distance < -12 * MILLIS_PER_HOUR) return 1;
return 0;
}
@@ -14,6 +14,8 @@ const baseState: RuntimeState = {
plannedStart: 0,
plannedEnd: 0,
actualStart: null,
actualGroupStart: null,
currentDay: 0,
},
offset: {
absolute: 0,
@@ -46,6 +48,8 @@ const baseState: RuntimeState = {
_group: null,
_end: null,
_flag: null,
_startDayOffset: null,
_startEpoch: null,
};
export function makeRuntimeStateData(patch?: Partial<RuntimeState>): RuntimeState {
@@ -393,6 +393,7 @@ describe('loadGroupFlagAndEnd()', () => {
const state = {
groupNow: null,
eventNow: rundown.entries[11],
rundown: { actualGroupStart: null },
} as RuntimeState;
const metadata = { playableEventOrder: ['0', '11', '3'], flags: ['1'] } as RundownMetadata;
@@ -420,6 +421,7 @@ describe('loadGroupFlagAndEnd()', () => {
const state = {
groupNow: rundown.entries[1],
eventNow: rundown.entries[22],
rundown: { actualGroupStart: null },
} as RuntimeState;
const metadata = { playableEventOrder: ['0', '11', '22'], flags: ['1'] } as RundownMetadata;
@@ -447,6 +449,7 @@ describe('loadGroupFlagAndEnd()', () => {
const state = {
groupNow: rundown.entries[1],
eventNow: rundown.entries[0],
rundown: { actualGroupStart: null },
} as RuntimeState;
const metadata = { playableEventOrder: ['0', '11', '22'], flags: ['1'] } as RundownMetadata;
@@ -471,6 +474,7 @@ describe('loadGroupFlagAndEnd()', () => {
const state = {
groupNow: null,
eventNow: rundown.entries[0],
rundown: { actualGroupStart: null },
} as RuntimeState;
const metadata = { playableEventOrder: ['0', '1'], flags: ['1'] } as RundownMetadata;
+93 -23
View File
@@ -23,9 +23,15 @@ import {
isPlaybackActive,
} from 'ontime-utils';
import { timeNow } from '../utils/time.js';
import { getTimeObject, timeNow } from '../utils/time.js';
import type { RestorePoint } from '../services/restore-service/restore.type.js';
import { getCurrent, getExpectedFinish, getRuntimeOffset, getTimerPhase } from '../services/timerUtils.js';
import {
findDayOffset,
getCurrent,
getExpectedFinish,
getRuntimeOffset,
getTimerPhase,
} from '../services/timerUtils.js';
import { loadRoll, normaliseRollStart } from '../services/rollUtils.js';
import { timerConfig } from '../setup/config.js';
import { RundownMetadata } from '../api-data/rundown/rundown.types.js';
@@ -55,6 +61,8 @@ export type RuntimeState = {
_group: ExpectedMetadata;
_flag: ExpectedMetadata;
_end: ExpectedMetadata;
_startEpoch: MaybeNumber;
_startDayOffset: MaybeNumber;
};
const runtimeState: RuntimeState = {
@@ -78,6 +86,8 @@ const runtimeState: RuntimeState = {
_group: null,
_flag: null,
_end: null,
_startEpoch: null,
_startDayOffset: null,
};
export function getState(): Readonly<RuntimeState> {
@@ -152,6 +162,10 @@ export function clearState() {
runtimeState._timer.pausedAt = null;
runtimeState._timer.secondaryTarget = null;
runtimeState._timer.hasFinished = false;
runtimeState._startEpoch = null;
runtimeState._startDayOffset = null;
runtimeState.rundown.currentDay = null;
}
/**
@@ -190,7 +204,8 @@ export function updateRundownData(rundownData: {
runtimeState.rundown.plannedStart = rundownData.firstStart;
runtimeState.rundown.plannedEnd =
rundownData.firstStart === null ? null : rundownData.firstStart + rundownData.totalDuration;
getExpectedTimes();
if (isPlaybackActive(runtimeState.timer.playback)) getExpectedTimes();
}
/**
@@ -229,9 +244,14 @@ export function load(
// patch with potential provided data
if (initialData) {
patchTimer(initialData);
const startEpoch = initialData?.startEpoch;
const firstStart = initialData?.firstStart;
if (firstStart === null || typeof firstStart === 'number') {
if (
(firstStart === null || typeof firstStart === 'number') &&
(startEpoch === null || typeof startEpoch === 'number')
) {
runtimeState.rundown.actualStart = firstStart;
runtimeState._startEpoch = startEpoch;
const { absolute, relative } = getRuntimeOffset(runtimeState);
runtimeState.offset.absolute = absolute;
runtimeState.offset.relative = relative;
@@ -367,7 +387,8 @@ export function start(state: RuntimeState = runtimeState): boolean {
return false;
}
state.clock = timeNow();
const [epoch, now] = getTimeObject();
state.clock = now;
state.timer.secondaryTimer = null;
// add paused time if it exists
@@ -386,9 +407,16 @@ export function start(state: RuntimeState = runtimeState): boolean {
state.timer.elapsed = 0;
if (state.rundown.actualStart === null) {
state._startDayOffset = findDayOffset(state.eventNow.timeStart, state.clock);
state.rundown.currentDay = state._startDayOffset;
state._startEpoch = epoch;
state.rundown.actualStart = state.clock;
}
if (state.groupNow !== null && state.rundown.actualGroupStart === null) {
state.rundown.actualGroupStart = state.clock;
}
// update timer phase
runtimeState.timer.phase = getTimerPhase(runtimeState);
@@ -480,13 +508,20 @@ export type UpdateResult = {
export function update(): UpdateResult {
// 0. there are some things we always do
const previousClock = runtimeState.clock;
runtimeState.clock = timeNow(); // we update the clock on every update call
const [epoch, now] = getTimeObject();
runtimeState.clock = now; // we update the clock on every update call
// 1. is playback idle?
if (!isPlaybackActive(runtimeState.timer.playback)) {
return updateIfIdle();
}
// if we are playing and playback changes. we tick the current runtime day
if (runtimeState._startDayOffset !== null && runtimeState._startEpoch) {
runtimeState.rundown.currentDay =
runtimeState._startDayOffset + Math.floor((epoch - runtimeState._startEpoch) / dayInMs);
}
// 2. are we waiting to roll?
if (runtimeState.timer.playback === Playback.Roll && runtimeState.timer.secondaryTimer !== null) {
const hasCrossedMidnight = previousClock > runtimeState.clock;
@@ -568,6 +603,10 @@ export function roll(
return { eventId: runtimeState.eventNow?.id ?? null, didStart: false };
}
// we will need to do some calculations, update the time first
const [epoch, now] = getTimeObject();
runtimeState.clock = now;
// 2. if there is an event armed, we use it
if (runtimeState.timer.playback === Playback.Armed || runtimeState.timer.phase === TimerPhase.Pending) {
// eslint-disable-next-line no-unused-labels -- dev code path
@@ -598,12 +637,24 @@ export function roll(
// check if the event is ready to start or if needs to be pending
const isNow = checkIsNow(runtimeState.eventNow.timeStart, runtimeState.eventNow.timeEnd, offsetClock);
if (isNow) {
runtimeState.timer.startedAt = runtimeState.clock;
/**
* If we are starting an event in roll mode
* we backtrace all the start times to the supposed start time of the event
*/
const plannedStart = runtimeState.eventNow.timeStart;
runtimeState.timer.startedAt = plannedStart;
// reset the secondary timer to cancel any countdowns
runtimeState.timer.secondaryTimer = null;
if (runtimeState.groupNow !== null && runtimeState.rundown.actualGroupStart === null) {
runtimeState.rundown.actualGroupStart = plannedStart;
}
if (runtimeState.rundown.actualStart === null) {
runtimeState.rundown.actualStart = runtimeState.clock;
runtimeState.rundown.actualStart = plannedStart;
runtimeState._startDayOffset = 0;
runtimeState._startEpoch = epoch;
}
runtimeState.timer.secondaryTimer = null;
} else {
runtimeState._timer.secondaryTarget = normaliseRollStart(runtimeState.eventNow.timeStart, offsetClock);
runtimeState.timer.secondaryTimer = runtimeState._timer.secondaryTarget - offsetClock;
@@ -658,26 +709,37 @@ export function roll(
return { eventId: runtimeState.eventNow.id, didStart: false };
}
// there is something to run, load event
// event will finish on time
// account for event that finishes the day after
/**
* At this point we know that there is something to run and the event is loaded
* - ensure the event will finish ontime
* - account for events that finish the day after
*
* when we start in roll mode
* we need to backtrace all times to the supposed start time of the event
*/
const plannedStart = runtimeState.eventNow.timeStart;
const endTime =
runtimeState.eventNow.timeEnd < runtimeState.eventNow.timeStart
? runtimeState.eventNow.timeEnd + dayInMs
: runtimeState.eventNow.timeEnd;
runtimeState.timer.startedAt = runtimeState.clock;
runtimeState.timer.startedAt = plannedStart;
runtimeState.timer.expectedFinish = endTime;
// we add time to allow timer to catch up
runtimeState.timer.addedTime = -(runtimeState.clock - runtimeState.eventNow.timeStart);
// state catch up
runtimeState.timer.duration = calculateDuration(runtimeState.eventNow.timeStart, endTime);
runtimeState.timer.current = getCurrent(runtimeState);
runtimeState.timer.elapsed = 0;
// update runtime
runtimeState.rundown.actualStart = runtimeState.clock;
runtimeState.rundown.actualStart = plannedStart;
if (runtimeState.groupNow !== null && runtimeState.rundown.actualGroupStart === null) {
runtimeState.rundown.actualGroupStart = plannedStart;
}
// update metadata
runtimeState._startDayOffset = 0;
runtimeState._startEpoch = epoch;
return { eventId: runtimeState.eventNow.id, didStart: true };
}
@@ -704,7 +766,7 @@ function getExpectedTimes(state = runtimeState) {
if (_group !== null) {
const { event: lastEvent, accumulatedGap, isLinkedToLoaded } = _group;
const lastEventExpectedStart = getExpectedStart(lastEvent, {
currentDay: eventNow.dayOffset,
currentDay: state.rundown.currentDay!,
totalGap: accumulatedGap,
isLinkedToLoaded,
mode: offset.mode,
@@ -721,7 +783,7 @@ function getExpectedTimes(state = runtimeState) {
if (_flag) {
const { event, accumulatedGap, isLinkedToLoaded } = _flag;
const expectedStart = getExpectedStart(event, {
currentDay: eventNow.dayOffset,
currentDay: state.rundown.currentDay!,
totalGap: accumulatedGap,
isLinkedToLoaded,
mode: offset.mode,
@@ -736,7 +798,7 @@ function getExpectedTimes(state = runtimeState) {
if (state._end) {
const { event, accumulatedGap, isLinkedToLoaded } = state._end;
const expectedStart = getExpectedStart(event, {
currentDay: eventNow.dayOffset,
currentDay: state.rundown.currentDay!,
totalGap: accumulatedGap,
isLinkedToLoaded,
mode: offset.mode,
@@ -752,16 +814,19 @@ export function loadGroupFlagAndEnd(
rundown: Rundown,
metadata: RundownMetadata,
currentIndex: MaybeNumber,
state = runtimeState,
state = runtimeState, // used for testing
) {
const previousGroup = state.groupNow?.id;
state.groupNow = null;
state._group = null;
state.eventFlag = null;
state._flag = null;
state._end = null;
if (currentIndex == null) return;
if (state.eventNow === null) return;
if (currentIndex === null || state.eventNow === null) {
state.rundown.actualGroupStart = null;
return;
}
const currentGroupId = state.eventNow.parent;
const flagsPresent = metadata.flags.length !== 0;
@@ -773,6 +838,10 @@ export function loadGroupFlagAndEnd(
state.groupNow = currentGroupId ? (entries[currentGroupId] as OntimeGroup) : null;
const lastEventInGroup = orderInGroup ? getLastEventNormal(rundown.entries, orderInGroup).lastEvent : null;
if (previousGroup !== currentGroupId) {
state.rundown.actualGroupStart = null;
}
// if we don't have a any flags in the rundown then no need to look for it
let foundFlag = !flagsPresent;
// if we don't have a last event for the group there is no need to find its end time
@@ -814,4 +883,5 @@ export function loadGroupFlagAndEnd(
export function setOffsetMode(mode: OffsetMode) {
runtimeState.offset.mode = mode;
if (isPlaybackActive(runtimeState.timer.playback)) getExpectedTimes();
}
+15
View File
@@ -74,3 +74,18 @@ export function timeNow() {
elapsed += now.getMilliseconds();
return elapsed;
}
/**
* Get current time from system
* @returns [number, number] - [epoch time, milliseconds since midnight]
*/
export function getTimeObject(): [number, number] {
const now = new Date();
// extract milliseconds since midnight
let elapsed = now.getHours() * 3600000;
elapsed += now.getMinutes() * 60000;
elapsed += now.getSeconds() * 1000;
elapsed += now.getMilliseconds();
return [now.getTime(), elapsed];
}
@@ -72,4 +72,6 @@ test('time until relative', async ({ page }) => {
await expect(page.getByTestId('entry-2').getByTestId('rundown-event')).toContainText('30s');
await expect(page.getByTestId('entry-3').getByTestId('rundown-event')).toContainText('10m');
await expect(page.getByTestId('entry-4').getByTestId('rundown-event')).toContainText('20m');
await page.getByRole('button', { name: 'Absolute' }).click();
});
+4 -3
View File
@@ -1,6 +1,6 @@
{
"name": "ontime",
"version": "4.0.0-beta.2",
"version": "4.0.0-beta.3",
"description": "Time keeping for live events",
"keywords": [
"ontime",
@@ -33,7 +33,7 @@
"e2e": "pnpm clear-temp && cross-env DEBUG=pw:webserver npx playwright test -c playwright.config.ts",
"e2e:ui": "cross-env DEBUG=pw:webserver npx playwright test --ui -c playwright.config.ts",
"e2e:i": "npx playwright codegen",
"cleanup": "rm -rf node_modules && rm -rf **/node_modules && rm -rf **/**/node_modules",
"cleanup": "pnpm rimraf node_modules && rimraf **/node_modules && rimraf **/**/node_modules",
"clear-temp": "rm -rf e2e/tests/fixtures/tmp"
},
"devDependencies": {
@@ -47,7 +47,8 @@
"eslint-plugin-playwright": "^1.5.2",
"prettier": "catalog:",
"turbo": "^2.3.3",
"typescript": "catalog:"
"typescript": "catalog:",
"rimraf": "catalog:"
},
"packageManager": "pnpm@10.11.0+sha512.6540583f41cc5f628eb3d9773ecee802f4f9ef9923cc45b69890fb47991d4b092964694ec3a4f738a420c918a333062c8b925d312f42e4f0c263eb603551f977"
}
+5 -2
View File
@@ -1,4 +1,5 @@
{
"version": "4.0.0-beta.3",
"name": "ontime-types",
"type": "module",
"main": "./src/index.ts",
@@ -7,7 +8,8 @@
"description": "shared typings for ontime",
"scripts": {
"cleanup": "rm -rf .turbo && rm -rf node_modules",
"lint": "eslint . --quiet"
"lint": "eslint . --quiet",
"build": "tsc"
},
"keywords": [],
"author": "",
@@ -16,6 +18,7 @@
"@typescript-eslint/eslint-plugin": "catalog:",
"@typescript-eslint/parser": "catalog:",
"eslint": "catalog:",
"typescript": "catalog:"
"typescript": "catalog:",
"ts-essentials": "catalog:"
}
}
+173 -14
View File
@@ -1,15 +1,174 @@
import type { DeepPartial } from 'ts-essentials';
import type { OntimeEvent } from '../../definitions/core/OntimeEntry.js';
import type { SimpleDirection, SimplePlayback } from '../../definitions/runtime/AuxTimer.type.js';
import type { MessageState } from '../../definitions/runtime/MessageControl.type.js';
import type { OffsetMode } from '../../definitions/runtime/Offset.type.js';
import type { RuntimeStore } from '../../definitions/runtime/RuntimeStore.type.js';
export type VersionAction = {
tag: 'version';
payload: undefined;
};
export type VersionResponse = {
tag: 'version';
payload: string;
};
export type PollAction = {
tag: 'poll';
payload: undefined;
};
export type PollResponse = {
tag: 'poll';
payload: RuntimeStore;
};
export type ChangeAction = {
tag: 'change';
payload: { [x: string]: Partial<OntimeEvent> };
};
export type ChangeResponse = {
tag: 'change';
payload: 'success' | 'throttled';
};
export type MessageAction = {
tag: 'message';
payload: DeepPartial<MessageState>;
};
export type MessageResponse = {
tag: 'message';
payload: MessageState;
};
export type StartAction = {
tag: 'start';
payload: undefined | { index: number } | { id: string } | { cue: string } | 'next' | 'previous';
};
export type StartResponse = {
tag: 'start';
payload: 'success';
};
export type PauseAction = {
tag: 'pause';
payload: undefined;
};
export type PauseResponse = {
tag: 'pause';
payload: 'success';
};
export type StopAction = {
tag: 'stop';
payload: undefined;
};
export type StopResponse = {
tag: 'stop';
payload: 'success';
};
export type ReloadAction = {
tag: 'reload';
payload: undefined;
};
export type ReloadResponse = {
tag: 'reload';
payload: 'success';
};
export type RollAction = {
tag: 'roll';
payload: undefined;
};
export type RollResponse = {
tag: 'roll';
payload: 'success';
};
export type LoadAction = {
tag: 'load';
payload: { index: number } | { id: string } | { cue: string } | 'next' | 'previous';
};
export type LoadResponse = {
tag: 'load';
payload: 'success';
};
export type AddtimeAction = {
tag: 'addtime';
payload: { add: number } | { remove: number } | number;
};
export type AddtimeResponse = {
tag: 'addtime';
payload: 'success';
};
export type AuxtimerAction = {
tag: 'auxtimer';
payload:
| { '1': SimplePlayback }
| { '1': { duration?: number; addtime?: number; direction?: SimpleDirection } }
| { '2': SimplePlayback }
| { '2': { duration?: number; addtime?: number; direction?: SimpleDirection } }
| { '3': SimplePlayback }
| { '3': { duration?: number; addtime?: number; direction?: SimpleDirection } };
};
export type AuxtimerResponse = {
tag: 'auxtimer';
payload: 'success';
};
export type ClientAction = {
tag: 'client';
payload: { target: string } & ({ rename: string } | { redirect: string } | { identify: string });
};
export type ClientResponse = {
tag: 'client';
payload: 'success';
};
export type OffsetmodeAction = {
tag: 'offsetmode';
payload: OffsetMode;
};
export type OffsetmodeResponse = {
tag: 'offsetmode';
payload: 'success';
};
export type ApiAction =
| 'version'
| 'poll'
| 'change'
| 'message'
| 'start'
| 'pause'
| 'stop'
| 'reload'
| 'roll'
| 'load'
| 'addtime'
| 'auxtimer'
| 'client'
| 'offsetmode';
| VersionAction
| PollAction
| ChangeAction
| MessageAction
| StartAction
| PauseAction
| StopAction
| ReloadAction
| RollAction
| LoadAction
| AddtimeAction
| AuxtimerAction
| ClientAction
| OffsetmodeAction;
export type ApiResponse =
| VersionResponse
| PollResponse
| ChangeResponse
| MessageResponse
| StartResponse
| PauseResponse
| StopResponse
| ReloadResponse
| RollResponse
| LoadResponse
| AddtimeResponse
| AuxtimerResponse
| ClientResponse
| OffsetmodeResponse;
export type ApiActionTag = ApiAction['tag'];
@@ -1,12 +1,11 @@
import type {
AutomationSettings,
CustomFields,
ProjectData,
ProjectRundowns,
Settings,
URLPreset,
ViewSettings,
} from '../index.js';
import type { AutomationSettings } from './core/Automation.type.js';
import type { CustomFields } from './core/CustomFields.type.js';
import type { ProjectData } from './core/ProjectData.type.js';
import type { ProjectRundowns } from './core/Rundown.type.js';
import type { Settings } from './core/Settings.type.js';
import type { URLPreset } from './core/UrlPreset.type.js';
import type { ViewSettings } from './core/Views.type.js';
export type DatabaseModel = {
rundowns: ProjectRundowns;
@@ -1,4 +1,9 @@
import type { EndAction, EntryCustomFields, MaybeNumber, TimerType, TimeStrategy, Trigger } from '../../index.js';
import type { MaybeNumber } from '../../utils/utils.type.js';
import type { EndAction } from '../EndAction.type.js';
import type { TimerType } from '../TimerType.type.js';
import type { TimeStrategy } from '../TimeStrategy.type.js';
import type { Trigger } from './Automation.type.js';
import type { EntryCustomFields } from './CustomFields.type.js';
export type EntryId = string;
@@ -4,6 +4,8 @@ export type RundownState = {
selectedEventIndex: MaybeNumber;
numEvents: number;
plannedStart: MaybeNumber;
actualStart: MaybeNumber;
plannedEnd: MaybeNumber;
actualStart: MaybeNumber;
currentDay: MaybeNumber;
actualGroupStart: MaybeNumber;
};
@@ -33,6 +33,8 @@ export const runtimeStorePlaceholder: Readonly<RuntimeStore> = {
plannedStart: 0, // only changes if event changes
plannedEnd: 0, // only changes if event changes, overflows over dayInMs
actualStart: null, // set once we start the timer
actualGroupStart: null, // maybe set once we start the timer
currentDay: null,
},
offset: {
absolute: 0, // changes at runtime
@@ -1,4 +1,4 @@
import type { MaybeNumber } from '../../index.js';
import type { MaybeNumber } from '../../utils/utils.type.js';
import type { Playback } from './Playback.type.js';
export enum TimerPhase {
+1 -1
View File
@@ -90,7 +90,7 @@ export type { LinkOptions } from './api/session-controller/BackendResponse.type.
export { MessageTag } from './api/websocket/data.type.js';
export type { WsPacketToServer, WsPacketToClient } from './api/websocket/data.type.js';
export { RefetchKey } from './api/websocket/refetch.type.js';
export type { ApiAction } from './api/websocket/api.type.js';
export type { ApiAction, ApiActionTag, ApiResponse } from './api/websocket/api.type.js';
// SERVER RUNTIME
export { type Log, LogLevel, type LogMessage, LogOrigin } from './definitions/runtime/Logger.type.js';
export { Playback } from './definitions/runtime/Playback.type.js';
+33
View File
@@ -0,0 +1,33 @@
{
"compilerOptions": {
"target": "esnext",
"module": "esnext",
"moduleResolution": "node10",
"resolveJsonModule": false,
"isolatedModules": false,
"skipLibCheck": true,
"allowSyntheticDefaultImports": true,
"strict": true,
"forceConsistentCasingInFileNames": true,
"noImplicitAny": true,
"noImplicitReturns": true,
"allowUnreachableCode": false,
"allowUnusedLabels": false,
"noImplicitThis": true,
"strictNullChecks": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noFallthroughCasesInSwitch": true,
"removeComments": true,
"preserveConstEnums": true,
"allowJs": true,
"declaration": true,
"outDir": "dist",
},
"include": [
"src/index.ts",
],
"exclude": [
"node_modules", "dist"
]
}
@@ -1,6 +1,6 @@
import { OffsetMode } from 'ontime-types';
import { dayInMs } from './conversionUtils';
import { dayInMs, MILLIS_PER_HOUR } from './conversionUtils';
import { getExpectedStart } from './getExpectedStart';
describe('getExpectedStart()', () => {
@@ -277,4 +277,41 @@ describe('getExpectedStart()', () => {
// the overlap will be pushed out to the expected available time
expect(getExpectedStart(testEvent, { ...testState, totalGap: -5 })).toBe(110);
});
test('we started on the day before', () => {
const testEvent = {
timeStart: 5,
dayOffset: 0,
delay: 0,
};
const testState = {
currentDay: -1,
actualStart: 23 * MILLIS_PER_HOUR,
plannedStart: 0,
offset: -1 * MILLIS_PER_HOUR,
mode: OffsetMode.Absolute,
isLinkedToLoaded: true,
totalGap: 0,
};
expect(getExpectedStart(testEvent, { ...testState })).toBe(23 * MILLIS_PER_HOUR + 5);
});
test('next day in multi-day rundown', () => {
const testEvent = {
timeStart: 5,
dayOffset: 1,
delay: 0,
};
const testState = {
currentDay: -1,
actualStart: 23 * MILLIS_PER_HOUR,
plannedStart: 0,
offset: -1 * MILLIS_PER_HOUR,
mode: OffsetMode.Absolute,
isLinkedToLoaded: true,
totalGap: 0,
};
expect(getExpectedStart(testEvent, { ...testState })).toBe(23 * MILLIS_PER_HOUR + 5 + dayInMs);
expect(getExpectedStart(testEvent, { ...testState, currentDay: 0 })).toBe(23 * MILLIS_PER_HOUR + 5);
});
});
@@ -15,7 +15,7 @@ import { dayInMs } from './conversionUtils.js';
export function getExpectedStart(
event: Pick<OntimeEvent, 'timeStart' | 'dayOffset' | 'delay'>,
state: {
currentDay: number;
currentDay: number; // the current day from the rundown
totalGap: number;
isLinkedToLoaded: boolean;
offset: number;
@@ -38,7 +38,7 @@ export function getExpectedStart(
let relativeStartOffset = 0;
if (mode === OffsetMode.Relative) {
relativeStartOffset = (actualStart ?? 0) - (plannedStart ?? 0);
relativeStartOffset = (actualStart ?? 0) + currentDay * dayInMs - (plannedStart ?? 0);
}
const scheduledStartTime = normalisedTimeStart + relativeStartOffset;
@@ -14,7 +14,7 @@ export const defaultImportMap = {
title: 'title',
countToEnd: 'count to end',
skip: 'skip',
note: 'notes',
note: 'note',
colour: 'colour',
endAction: 'end action',
timerType: 'timer type',
+350 -13
View File
@@ -27,6 +27,12 @@ catalogs:
prettier:
specifier: 3.3.1
version: 3.3.1
rimraf:
specifier: 6.0.1
version: 6.0.1
ts-essentials:
specifier: 10.1.1
version: 10.1.1
typescript:
specifier: 5.5.3
version: 5.5.3
@@ -65,6 +71,9 @@ importers:
prettier:
specifier: 'catalog:'
version: 3.3.1
rimraf:
specifier: 'catalog:'
version: 6.0.1
turbo:
specifier: ^2.3.3
version: 2.5.6
@@ -271,6 +280,30 @@ importers:
specifier: ^7.2.0
version: 7.2.0
apps/resolver:
devDependencies:
'@sprout2000/esbuild-copy-plugin':
specifier: ^1.1.19
version: 1.1.19
'@typescript-eslint/parser':
specifier: 'catalog:'
version: 7.16.1(eslint@8.56.0)(typescript@5.5.3)
eslint:
specifier: 'catalog:'
version: 8.56.0
ontime-types:
specifier: workspace:^4.0.0
version: link:../../packages/types
rimraf:
specifier: 'catalog:'
version: 6.0.1
tsup:
specifier: ^8.5.0
version: 8.5.0(jiti@2.4.2)(postcss@8.5.4)(tsx@4.20.5)(typescript@5.5.3)
typescript:
specifier: 'catalog:'
version: 5.5.3
apps/server:
dependencies:
'@googleapis/sheets':
@@ -377,7 +410,7 @@ importers:
specifier: ^0.3.4
version: 0.3.4
ts-essentials:
specifier: ^10.0.3
specifier: 'catalog:'
version: 10.1.1(typescript@5.5.3)
tsx:
specifier: ^4.19.2
@@ -400,6 +433,9 @@ importers:
eslint:
specifier: 'catalog:'
version: 8.56.0
ts-essentials:
specifier: 'catalog:'
version: 10.1.1(typescript@5.5.3)
typescript:
specifier: 'catalog:'
version: 5.5.3
@@ -1575,6 +1611,10 @@ packages:
resolution: {integrity: sha512-t09vSN3MdfsyCHoFcTRCH/iUtG7OJ0CsjzB8cjAmKc/va/kIgeDI/TxsigdncE/4be734m0cvIYwNaV4i2XqAw==}
engines: {node: '>=10'}
'@sprout2000/esbuild-copy-plugin@1.1.19':
resolution: {integrity: sha512-KXI1nDBWKLubg5EJy+jkxhqvtg9wlshF+ycVMoad6j2pr/APQiDMwAv6JUzkgU6INxg40E4/7xfgefi/Gn+blA==}
engines: {node: '>=16.7'}
'@svgr/babel-plugin-add-jsx-attribute@8.0.0':
resolution: {integrity: sha512-b9MIk7yhdS1pMCZM8VeNfUlSKVRhsHZNMl5O9SfaX0l0t5wjdgu4IDzGB8bpnGBBOjGST3rRFVsaaEtI4W6f7g==}
engines: {node: '>=14'}
@@ -2030,6 +2070,9 @@ packages:
resolution: {integrity: sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==}
engines: {node: '>=12'}
any-promise@1.3.0:
resolution: {integrity: sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==}
anymatch@3.1.3:
resolution: {integrity: sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==}
engines: {node: '>= 8'}
@@ -2233,6 +2276,12 @@ packages:
builder-util@26.0.17:
resolution: {integrity: sha512-fym+vg0kegrHBSCmkYYql2EbsLvnlUhIUKRQJ7EHjyftwMz8mibpvTRll3pzK1rtWm/VRdjl7AB397jdtg/Jmw==}
bundle-require@5.1.0:
resolution: {integrity: sha512-3WrrOuZiyaaZPWiEt4G3+IffISVC9HYlWueJEBWED4ZH4aIAC2PnkdnuRrR94M+w6yGWn4AglWtJtBI8YqvgoA==}
engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0}
peerDependencies:
esbuild: '>=0.18'
busboy@1.6.0:
resolution: {integrity: sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA==}
engines: {node: '>=10.16.0'}
@@ -2377,6 +2426,10 @@ packages:
resolution: {integrity: sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==}
engines: {node: '>= 0.8'}
commander@4.1.1:
resolution: {integrity: sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==}
engines: {node: '>= 6'}
commander@5.1.0:
resolution: {integrity: sha512-P0CysNDQ7rtVw4QIQtm+MRxV66vKFSvlsQvGYXZWR3qFU0jlMKHZZZgw8e+8DSah4UDKMqnknRDQz+xuQXQ/Zg==}
engines: {node: '>= 6'}
@@ -2396,9 +2449,16 @@ packages:
resolution: {integrity: sha512-MWufYdFw53ccGjCA+Ol7XJYpAlW6/prSMzuPOTRnJGcGzuhLn4Scrz7qf6o8bROZ514ltazcIFJZevcfbo0x7A==}
engines: {'0': node >= 6.0}
confbox@0.1.8:
resolution: {integrity: sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w==}
config-file-ts@0.2.6:
resolution: {integrity: sha512-6boGVaglwblBgJqGyxm4+xCmEGcWgnWHSWHY5jad58awQhB6gftq0G8HbzU39YqCIYHMLAiL1yjwiZ36m/CL8w==}
consola@3.4.2:
resolution: {integrity: sha512-5IKcdX0nnYavi6G7TtOhwkYzyjfJlatbjMjuLSfE2kYT5pMDOilZ4OvMhi637CcDICTmz3wARPoyhqyX1Y+XvA==}
engines: {node: ^14.18.0 || >=16.10.0}
content-disposition@1.0.0:
resolution: {integrity: sha512-Au9nRL8VNUut/XSzbQA38+M78dzP4D+eqg3gfJHMIHHYa3bg067xj1KxMUWj+VULbiZMowKngFFbKczUrNJ1mg==}
engines: {node: '>= 0.6'}
@@ -2985,6 +3045,9 @@ packages:
resolution: {integrity: sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==}
engines: {node: '>=10'}
fix-dts-default-cjs-exports@1.0.1:
resolution: {integrity: sha512-pVIECanWFC61Hzl2+oOCtoJ3F17kglZC/6N94eRWycFgBH35hHx0Li604ZIzhseh97mf2p0cv7vVrOZGoqhlEg==}
flat-cache@3.1.1:
resolution: {integrity: sha512-/qM2b3LUIaIgviBQovTLvijfyOQXPtSRnRK26ksj2J7rzPIecePUIpJsZ4T02Qg+xiAEKIs5K8dsHEd+VaKa/Q==}
engines: {node: '>=12.0.0'}
@@ -3126,6 +3189,11 @@ packages:
resolution: {integrity: sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==}
hasBin: true
glob@11.0.3:
resolution: {integrity: sha512-2Nim7dha1KVkaiF4q6Dj+ngPPMdfvLJEOpZk/jKiUAkqKebpGAWQXAq9z1xu9HKu5lWfqw/FASuccEjyznjPaA==}
engines: {node: 20 || >=22}
hasBin: true
glob@7.2.3:
resolution: {integrity: sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==}
deprecated: Glob versions prior to v9 are no longer supported
@@ -3506,6 +3574,10 @@ packages:
jackspeak@3.4.3:
resolution: {integrity: sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==}
jackspeak@4.1.1:
resolution: {integrity: sha512-zptv57P3GpL+O0I7VdMJNBZCu+BPHVQUk55Ft8/QCJjTVxrnJHuVuX/0Bl2A6/+2oyR/ZMEuFKwmzqqZ/U5nPQ==}
engines: {node: 20 || >=22}
jake@10.9.2:
resolution: {integrity: sha512-2P4SQ0HrLQ+fw6llpLnOaGAvN2Zu6778SJMrCUwns4fOoG9ayrTiZk3VV8sCPkVZF8ab0zksVpS8FDY5pRCNBA==}
engines: {node: '>=10'}
@@ -3518,6 +3590,10 @@ packages:
joi@17.13.3:
resolution: {integrity: sha512-otDA4ldcIx+ZXsKHWmp0YizCweVRZG96J10b0FevjfuncLO1oX59THoAmHkNubYJ+9gWsYsp5k8v4ib6oDv1fA==}
joycon@3.1.1:
resolution: {integrity: sha512-34wB/Y7MW7bzjKRjUKTa46I2Z7eV62Rkhva+KkopW7Qvv/OSWBqvkSY7vusOPrNuZcUG3tApvdVgNB8POj3SPw==}
engines: {node: '>=10'}
js-tokens@4.0.0:
resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==}
@@ -3597,9 +3673,17 @@ packages:
resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==}
engines: {node: '>= 0.8.0'}
lilconfig@3.1.3:
resolution: {integrity: sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==}
engines: {node: '>=14'}
lines-and-columns@1.2.4:
resolution: {integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==}
load-tsconfig@0.2.5:
resolution: {integrity: sha512-IXO6OCs9yg8tMKzfPZ1YmheJbZCiEsnBdcB03l0OcfK9prKnJb96siuHCr5Fl37/yo9DnKU+TLpxzTUspw9shg==}
engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0}
locate-path@6.0.0:
resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==}
engines: {node: '>=10'}
@@ -3619,6 +3703,9 @@ packages:
lodash.merge@4.6.2:
resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==}
lodash.sortby@4.7.0:
resolution: {integrity: sha512-HDWXG8isMntAyRF5vZ7xKuEvOhT4AhlRt/3czTSjvGUxjYCBVRQY48ViDHyfYz9VIoBkW4TMGQNapx+l3RUwdA==}
lodash.union@4.6.0:
resolution: {integrity: sha512-c4pB2CdGrGdjMKYLA+XiRDO7Y0PRQbm/Gzg8qMj+QH+pFVAoTp5sBpO0odL3FjoPCGjK96p6qsP+yQoiLoOBcw==}
@@ -3650,6 +3737,10 @@ packages:
lru-cache@10.4.3:
resolution: {integrity: sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==}
lru-cache@11.2.1:
resolution: {integrity: sha512-r8LA6i4LP4EeWOhqBaZZjDWwehd1xUJPCJd9Sv300H0ZmcUER4+JPh7bqqZeqs1o5pgtgvXm+d9UGrB5zZGDiQ==}
engines: {node: 20 || >=22}
lru-cache@5.1.1:
resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==}
@@ -3808,6 +3899,9 @@ packages:
engines: {node: '>=10'}
hasBin: true
mlly@1.8.0:
resolution: {integrity: sha512-l8D9ODSRWLe2KHJSifWGwBqpTZXIXTeo8mlKjY+E2HAakaTeNpqAyBZ8GSqLzHgw4XmHmC8whvpjJNMbFZN7/g==}
ms@2.1.3:
resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==}
@@ -3815,6 +3909,9 @@ packages:
resolution: {integrity: sha512-Ug8bXeTIUlxurg8xLTEskKShvcKDZALo1THEX5E41pYCD2sCVub5/kIRIGqWNoqV6szyLyQKV6mD4QUrWE5GCQ==}
engines: {node: '>= 10.16.0'}
mz@2.7.0:
resolution: {integrity: sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==}
nanoid@3.3.11:
resolution: {integrity: sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==}
engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1}
@@ -3999,6 +4096,10 @@ packages:
resolution: {integrity: sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==}
engines: {node: '>=16 || 14 >=14.18'}
path-scurry@2.0.0:
resolution: {integrity: sha512-ypGJsmGtdXUOeM5u93TyeIEfEhM6s+ljAhrk5vAvSx8uyY/02OvrZnA0YNGUrPXfpJMgI1ODd3nwz8Npx4O4cg==}
engines: {node: 20 || >=22}
path-to-regexp@8.2.0:
resolution: {integrity: sha512-TdrF7fW9Rphjq4RjrW0Kp2AW0Ahwu9sRGTkS6bvDi0SCwZlEZYmcfDbEsTz8RVk0EHIS/Vd1bv3JhG+1xZuAyQ==}
engines: {node: '>=16'}
@@ -4021,9 +4122,6 @@ packages:
pend@1.2.0:
resolution: {integrity: sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==}
picocolors@1.0.1:
resolution: {integrity: sha512-anP1Z8qwhkbmu7MFP5iTt+wQKXgwzf7zTyGlcdzabySa9vd0Xt392U0rVmz9poOaBj0uHJKyyo9/upk0HrEQew==}
picocolors@1.1.1:
resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==}
@@ -4035,6 +4133,13 @@ packages:
resolution: {integrity: sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==}
engines: {node: '>=12'}
pirates@4.0.7:
resolution: {integrity: sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==}
engines: {node: '>= 6'}
pkg-types@1.3.1:
resolution: {integrity: sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ==}
playwright-core@1.55.0:
resolution: {integrity: sha512-GvZs4vU3U5ro2nZpeiwyb0zuFaqb9sUiAJuyrWpcGouD8y9/HLgGbNRjIph7zU9D3hnPaisMl9zG9CgFi/biIg==}
engines: {node: '>=18'}
@@ -4053,6 +4158,24 @@ packages:
resolution: {integrity: sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==}
engines: {node: '>= 0.4'}
postcss-load-config@6.0.1:
resolution: {integrity: sha512-oPtTM4oerL+UXmx+93ytZVN82RrlY/wPUV8IeDxFrzIjXOLF1pN+EmKPLbubvKHT2HC20xXsCAH2Z+CKV6Oz/g==}
engines: {node: '>= 18'}
peerDependencies:
jiti: '>=1.21.0'
postcss: '>=8.0.9'
tsx: ^4.8.1
yaml: ^2.4.2
peerDependenciesMeta:
jiti:
optional: true
postcss:
optional: true
tsx:
optional: true
yaml:
optional: true
postcss@8.5.4:
resolution: {integrity: sha512-QSa9EBe+uwlGTFmHsPKokv3B/oEMQZxfqW0QqNCyhpa6mB1afzulwn8hihglqAb2pOw+BJgNlmXQ8la2VeHB7w==}
engines: {node: ^10 || ^12 || >=14}
@@ -4268,6 +4391,10 @@ packages:
resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==}
engines: {node: '>=4'}
resolve-from@5.0.0:
resolution: {integrity: sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==}
engines: {node: '>=8'}
resolve-pkg-maps@1.0.0:
resolution: {integrity: sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==}
@@ -4300,6 +4427,11 @@ packages:
deprecated: Rimraf versions prior to v4 are no longer supported
hasBin: true
rimraf@6.0.1:
resolution: {integrity: sha512-9dkvaxAsk/xNXSJzMgFqqMCuFgt2+KsOFek3TMLfo8NCPfWpBmqwyNn5Y+NX56QUYfCtsyhF3ayiboEoUmJk/A==}
engines: {node: 20 || >=22}
hasBin: true
roarr@2.15.4:
resolution: {integrity: sha512-CHhPh+UNHD2GTXNYhPWLnU8ONHdI+5DI+4EYIAOaiD63rHeYlZvyh8P+in5999TTSFgUYuKUAjzRI4mdh/p+2A==}
engines: {node: '>=8.0'}
@@ -4498,6 +4630,11 @@ packages:
resolution: {integrity: sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==}
engines: {node: '>=0.10.0'}
source-map@0.8.0-beta.0:
resolution: {integrity: sha512-2ymg6oRBpebeZi9UUNsgQ89bhx01TcTkmNTGnNO88imTmbSgy4nfujrgVEFKWpMTEGA11EDkTt7mqObTPdigIA==}
engines: {node: '>= 8'}
deprecated: The work that was done in this beta branch won't be included in future versions
sprintf-js@1.1.3:
resolution: {integrity: sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA==}
@@ -4580,6 +4717,11 @@ packages:
resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==}
engines: {node: '>=8'}
sucrase@3.35.0:
resolution: {integrity: sha512-8EbVDiu9iN/nESwxeSxDKe0dunta1GOlHufmSSXxMD2z2/tMZpDMpvXQGsc+ajGo8y2uYUmixaSRUc/QPoQ0GA==}
engines: {node: '>=16 || 14 >=14.17'}
hasBin: true
sumchecker@3.0.1:
resolution: {integrity: sha512-MvjXzkz/BOfyVDkG0oFOtBxHX2u3gKbMHIF/dXblZsgD3BWOFLmHovIpZY7BykJdAjcqRCBi1WYBNdEC9yI7vg==}
engines: {node: '>= 8.0'}
@@ -4626,6 +4768,13 @@ packages:
text-table@0.2.0:
resolution: {integrity: sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==}
thenify-all@1.6.0:
resolution: {integrity: sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==}
engines: {node: '>=0.8'}
thenify@3.3.1:
resolution: {integrity: sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==}
tiny-async-pool@1.3.0:
resolution: {integrity: sha512-01EAw5EDrcVrdgyCLgoSPvqznC0sVxDSVeiOz09FUpjh71G79VCqneOr+xvt7T1r76CF6ZZfPjHorN2+d+3mqA==}
@@ -4677,10 +4826,17 @@ packages:
tr46@0.0.3:
resolution: {integrity: sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==}
tr46@1.0.1:
resolution: {integrity: sha512-dTpowEjclQ7Kgx5SdBkqRzVhERQXov8/l9Ft9dVM9fmg0W0KQSVaXX9T4i6twCPNtYiZM53lpSSUAwJbFPOHxA==}
tr46@3.0.0:
resolution: {integrity: sha512-l7FvfAHlcmulp8kr+flpQZmVwtu7nfRV7NZujtN0OqES8EL4O4e0qqzL0DC5gAvx/ZC/9lk6rhcUwYvkBnBnYA==}
engines: {node: '>=12'}
tree-kill@1.2.2:
resolution: {integrity: sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==}
hasBin: true
truncate-utf8-bytes@1.0.2:
resolution: {integrity: sha512-95Pu1QXQvruGEhv62XCMO3Mm90GscOCClvrIUwCM0PYOXK3kaF3l3sIHxx71ThJfcbM2O5Au6SO3AWCSEfW4mQ==}
@@ -4704,6 +4860,9 @@ packages:
typescript:
optional: true
ts-interface-checker@0.1.13:
resolution: {integrity: sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==}
tsconfck@3.1.6:
resolution: {integrity: sha512-ks6Vjr/jEw0P1gmOVwutM3B7fWxoWBL2KRDb1JfqGVawBmO5UsvmWOQFGHBPl5yxYz4eERr19E6L7NMv+Fej4w==}
engines: {node: ^18 || >=20}
@@ -4717,6 +4876,25 @@ packages:
tslib@2.6.2:
resolution: {integrity: sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==}
tsup@8.5.0:
resolution: {integrity: sha512-VmBp77lWNQq6PfuMqCHD3xWl22vEoWsKajkF8t+yMBawlUS8JzEI+vOVMeuNZIuMML8qXRizFKi9oD5glKQVcQ==}
engines: {node: '>=18'}
hasBin: true
peerDependencies:
'@microsoft/api-extractor': ^7.36.0
'@swc/core': ^1
postcss: ^8.4.12
typescript: '>=4.5.0'
peerDependenciesMeta:
'@microsoft/api-extractor':
optional: true
'@swc/core':
optional: true
postcss:
optional: true
typescript:
optional: true
tsx@4.20.5:
resolution: {integrity: sha512-+wKjMNU9w/EaQayHXb7WA7ZaHY6hN8WgfvHNQ3t1PnU91/7O8TcTnIhCDYTZwnt8JsO9IBqZ30Ln1r7pPF52Aw==}
engines: {node: '>=18.0.0'}
@@ -4805,6 +4983,9 @@ packages:
engines: {node: '>=14.17'}
hasBin: true
ufo@1.6.1:
resolution: {integrity: sha512-9a4/uxlTWJ4+a5i0ooc1rU7C7YOw3wT+UGqdeNNHWnOF9qcMBgLRS+4IYUqbczewFx4mLEig6gawh7X6mFlEkA==}
unbox-primitive@1.1.0:
resolution: {integrity: sha512-nWJ91DjeOkej/TA8pXQ3myruKpKEYgqvpw9lz4OPHj/NWFNluYrjbz9j01CJ8yKQd2g4jFoOkINCTW2I5LEEyw==}
engines: {node: '>= 0.4'}
@@ -4994,6 +5175,9 @@ packages:
webidl-conversions@3.0.1:
resolution: {integrity: sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==}
webidl-conversions@4.0.2:
resolution: {integrity: sha512-YQ+BmxuTgd6UXZW3+ICGfyqRyHXVlD5GtQr5+qjiNW7bF0cqrzX500HVXPBOvgXb5YnzDd+h0zqyv61KUD7+Sg==}
webidl-conversions@7.0.0:
resolution: {integrity: sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==}
engines: {node: '>=12'}
@@ -5020,6 +5204,9 @@ packages:
whatwg-url@5.0.0:
resolution: {integrity: sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==}
whatwg-url@7.1.0:
resolution: {integrity: sha512-WUu7Rg1DroM7oQvGWfOiAK21n74Gg+T4elXEQYkOhtyLeWiJFoOGLXPKI/9gzIie9CtwVLm8wtw6YJdKyxSjeg==}
which-boxed-primitive@1.1.1:
resolution: {integrity: sha512-TbX3mj8n0odCBFVlY8AxkqcHASw3L60jIuF8jFP78az3C2YhmGvqbHBpAjTRH2/xqYunrJ9g1jSyjCjpoWzIAA==}
engines: {node: '>= 0.4'}
@@ -5177,7 +5364,7 @@ snapshots:
'@babel/code-frame@7.24.2':
dependencies:
'@babel/highlight': 7.24.5
picocolors: 1.0.1
picocolors: 1.1.1
'@babel/code-frame@7.27.1':
dependencies:
@@ -5252,8 +5439,8 @@ snapshots:
'@babel/generator@7.23.6':
dependencies:
'@babel/types': 7.28.2
'@jridgewell/gen-mapping': 0.3.3
'@jridgewell/trace-mapping': 0.3.20
'@jridgewell/gen-mapping': 0.3.13
'@jridgewell/trace-mapping': 0.3.30
jsesc: 2.5.2
'@babel/generator@7.27.5':
@@ -5426,7 +5613,7 @@ snapshots:
'@babel/helper-validator-identifier': 7.27.1
chalk: 2.4.2
js-tokens: 4.0.0
picocolors: 1.0.1
picocolors: 1.1.1
'@babel/parser@7.23.6':
dependencies:
@@ -6302,6 +6489,8 @@ snapshots:
'@sindresorhus/is@4.6.0': {}
'@sprout2000/esbuild-copy-plugin@1.1.19': {}
'@svgr/babel-plugin-add-jsx-attribute@8.0.0(@babel/core@7.23.6)':
dependencies:
'@babel/core': 7.23.6
@@ -6780,8 +6969,7 @@ snapshots:
acorn@8.14.0: {}
acorn@8.15.0:
optional: true
acorn@8.15.0: {}
adler-32@1.3.1: {}
@@ -6831,6 +7019,8 @@ snapshots:
ansi-styles@6.2.1: {}
any-promise@1.3.0: {}
anymatch@3.1.3:
dependencies:
normalize-path: 3.0.0
@@ -7194,6 +7384,11 @@ snapshots:
transitivePeerDependencies:
- supports-color
bundle-require@5.1.0(esbuild@0.25.5):
dependencies:
esbuild: 0.25.5
load-tsconfig: 0.2.5
busboy@1.6.0:
dependencies:
streamsearch: 1.1.0
@@ -7361,6 +7556,8 @@ snapshots:
dependencies:
delayed-stream: 1.0.0
commander@4.1.1: {}
commander@5.1.0: {}
compare-version@0.1.2: {}
@@ -7381,11 +7578,15 @@ snapshots:
readable-stream: 3.6.2
typedarray: 0.0.6
confbox@0.1.8: {}
config-file-ts@0.2.6:
dependencies:
glob: 10.4.5
typescript: 5.9.2
consola@3.4.2: {}
content-disposition@1.0.0:
dependencies:
safe-buffer: 5.2.1
@@ -8191,6 +8392,12 @@ snapshots:
locate-path: 6.0.0
path-exists: 4.0.0
fix-dts-default-cjs-exports@1.0.1:
dependencies:
magic-string: 0.30.17
mlly: 1.8.0
rollup: 4.41.1
flat-cache@3.1.1:
dependencies:
flatted: 3.2.9
@@ -8362,6 +8569,15 @@ snapshots:
package-json-from-dist: 1.0.1
path-scurry: 1.11.1
glob@11.0.3:
dependencies:
foreground-child: 3.3.1
jackspeak: 4.1.1
minimatch: 10.0.3
minipass: 7.1.2
package-json-from-dist: 1.0.1
path-scurry: 2.0.0
glob@7.2.3:
dependencies:
fs.realpath: 1.0.0
@@ -8780,6 +8996,10 @@ snapshots:
optionalDependencies:
'@pkgjs/parseargs': 0.11.0
jackspeak@4.1.1:
dependencies:
'@isaacs/cliui': 8.0.2
jake@10.9.2:
dependencies:
async: 3.2.6
@@ -8797,6 +9017,8 @@ snapshots:
'@sideway/formula': 3.0.1
'@sideway/pinpoint': 2.0.0
joycon@3.1.1: {}
js-tokens@4.0.0: {}
js-yaml@4.1.0:
@@ -8899,8 +9121,12 @@ snapshots:
prelude-ls: 1.2.1
type-check: 0.4.0
lilconfig@3.1.3: {}
lines-and-columns@1.2.4: {}
load-tsconfig@0.2.5: {}
locate-path@6.0.0:
dependencies:
p-locate: 5.0.0
@@ -8915,6 +9141,8 @@ snapshots:
lodash.merge@4.6.2: {}
lodash.sortby@4.7.0: {}
lodash.union@4.6.0: {}
lodash@4.17.21: {}
@@ -8942,6 +9170,8 @@ snapshots:
lru-cache@10.4.3: {}
lru-cache@11.2.1: {}
lru-cache@5.1.1:
dependencies:
yallist: 3.1.1
@@ -9093,6 +9323,13 @@ snapshots:
mkdirp@1.0.4: {}
mlly@1.8.0:
dependencies:
acorn: 8.15.0
pathe: 2.0.3
pkg-types: 1.3.1
ufo: 1.6.1
ms@2.1.3: {}
multer@2.0.1:
@@ -9105,6 +9342,12 @@ snapshots:
type-is: 1.6.18
xtend: 4.0.2
mz@2.7.0:
dependencies:
any-promise: 1.3.0
object-assign: 4.1.1
thenify-all: 1.6.0
nanoid@3.3.11: {}
nanoid@5.1.5: {}
@@ -9288,6 +9531,11 @@ snapshots:
lru-cache: 10.4.3
minipass: 7.1.2
path-scurry@2.0.0:
dependencies:
lru-cache: 11.2.1
minipass: 7.1.2
path-to-regexp@8.2.0: {}
path-type@4.0.0: {}
@@ -9300,14 +9548,20 @@ snapshots:
pend@1.2.0: {}
picocolors@1.0.1: {}
picocolors@1.1.1: {}
picomatch@2.3.1: {}
picomatch@4.0.2: {}
pirates@4.0.7: {}
pkg-types@1.3.1:
dependencies:
confbox: 0.1.8
mlly: 1.8.0
pathe: 2.0.3
playwright-core@1.55.0: {}
playwright@1.55.0:
@@ -9324,6 +9578,14 @@ snapshots:
possible-typed-array-names@1.1.0: {}
postcss-load-config@6.0.1(jiti@2.4.2)(postcss@8.5.4)(tsx@4.20.5):
dependencies:
lilconfig: 3.1.3
optionalDependencies:
jiti: 2.4.2
postcss: 8.5.4
tsx: 4.20.5
postcss@8.5.4:
dependencies:
nanoid: 3.3.11
@@ -9534,6 +9796,8 @@ snapshots:
resolve-from@4.0.0: {}
resolve-from@5.0.0: {}
resolve-pkg-maps@1.0.0: {}
resolve@1.22.10:
@@ -9565,6 +9829,11 @@ snapshots:
dependencies:
glob: 7.2.3
rimraf@6.0.1:
dependencies:
glob: 11.0.3
package-json-from-dist: 1.0.1
roarr@2.15.4:
dependencies:
boolean: 3.2.0
@@ -9832,6 +10101,10 @@ snapshots:
source-map@0.6.1: {}
source-map@0.8.0-beta.0:
dependencies:
whatwg-url: 7.1.0
sprintf-js@1.1.3:
optional: true
@@ -9934,6 +10207,16 @@ snapshots:
strip-json-comments@3.1.1: {}
sucrase@3.35.0:
dependencies:
'@jridgewell/gen-mapping': 0.3.13
commander: 4.1.1
glob: 10.4.5
lines-and-columns: 1.2.4
mz: 2.7.0
pirates: 4.0.7
ts-interface-checker: 0.1.13
sumchecker@3.0.1:
dependencies:
debug: 4.4.1
@@ -9988,6 +10271,14 @@ snapshots:
text-table@0.2.0: {}
thenify-all@1.6.0:
dependencies:
thenify: 3.3.1
thenify@3.3.1:
dependencies:
any-promise: 1.3.0
tiny-async-pool@1.3.0:
dependencies:
semver: 5.7.2
@@ -10033,11 +10324,17 @@ snapshots:
tr46@0.0.3: {}
tr46@1.0.1:
dependencies:
punycode: 2.3.1
tr46@3.0.0:
dependencies:
punycode: 2.3.1
optional: true
tree-kill@1.2.2: {}
truncate-utf8-bytes@1.0.2:
dependencies:
utf8-byte-length: 1.0.4
@@ -10054,12 +10351,42 @@ snapshots:
optionalDependencies:
typescript: 5.5.3
ts-interface-checker@0.1.13: {}
tsconfck@3.1.6(typescript@5.5.3):
optionalDependencies:
typescript: 5.5.3
tslib@2.6.2: {}
tsup@8.5.0(jiti@2.4.2)(postcss@8.5.4)(tsx@4.20.5)(typescript@5.5.3):
dependencies:
bundle-require: 5.1.0(esbuild@0.25.5)
cac: 6.7.14
chokidar: 4.0.3
consola: 3.4.2
debug: 4.4.1
esbuild: 0.25.5
fix-dts-default-cjs-exports: 1.0.1
joycon: 3.1.1
picocolors: 1.1.1
postcss-load-config: 6.0.1(jiti@2.4.2)(postcss@8.5.4)(tsx@4.20.5)
resolve-from: 5.0.0
rollup: 4.41.1
source-map: 0.8.0-beta.0
sucrase: 3.35.0
tinyexec: 0.3.2
tinyglobby: 0.2.14
tree-kill: 1.2.2
optionalDependencies:
postcss: 8.5.4
typescript: 5.5.3
transitivePeerDependencies:
- jiti
- supports-color
- tsx
- yaml
tsx@4.20.5:
dependencies:
esbuild: 0.25.5
@@ -10153,6 +10480,8 @@ snapshots:
typescript@5.9.2: {}
ufo@1.6.1: {}
unbox-primitive@1.1.0:
dependencies:
call-bound: 1.0.4
@@ -10190,7 +10519,7 @@ snapshots:
dependencies:
browserslist: 4.22.2
escalade: 3.1.1
picocolors: 1.0.1
picocolors: 1.1.1
update-browserslist-db@1.1.3(browserslist@4.25.0):
dependencies:
@@ -10363,6 +10692,8 @@ snapshots:
webidl-conversions@3.0.1: {}
webidl-conversions@4.0.2: {}
webidl-conversions@7.0.0:
optional: true
@@ -10389,6 +10720,12 @@ snapshots:
tr46: 0.0.3
webidl-conversions: 3.0.1
whatwg-url@7.1.0:
dependencies:
lodash.sortby: 4.7.0
tr46: 1.0.1
webidl-conversions: 4.0.2
which-boxed-primitive@1.1.1:
dependencies:
is-bigint: 1.1.0
+2
View File
@@ -11,6 +11,8 @@ catalog:
eslint-config-prettier: 9.1.0
eslint-plugin-prettier: 5.1.3
vitest: 3.2.1
rimraf: 6.0.1
ts-essentials: 10.1.1
onlyBuiltDependencies:
- electron
- esbuild
+3
View File
@@ -26,6 +26,9 @@
"cache": false
},
"build": {
"dependsOn": [
"^build"
],
"env": ["SENTRY_AUTH_TOKEN"]
},
"build:local": {},