Compare commits

..

17 Commits

Author SHA1 Message Date
arc-alex 6397d33eaa feat: option to select all events for countdown 2025-09-16 16:59:29 +02:00
arc-alex 9288566a7f chore: remove unused code 2025-09-16 16:38:11 +02:00
Alex Christoffer Rasmussen 7fd9f83fb0 fix docker ignore (#1780) 2025-09-16 16:27:00 +02:00
Carlos Valente e621f1386e fix(cuesheet): infinite render loop on resizing columns 2025-09-16 06:43:19 +02:00
Carlos Valente b51e7cbd2d fix: maintain multiline in cuesheet cells 2025-09-14 14:19:18 +02:00
Carlos Valente 7cd92ce5f7 docs: add contribution guidelines 2025-09-14 14:19:18 +02:00
Carlos Valente f6a02abf36 fix: skip nested events 2025-09-14 14:19:18 +02:00
Carlos Valente e4b0df42cf feat: allow finding milestones 2025-09-14 14:19:18 +02:00
Carlos Valente ded8bddb2d refactor: improve automated following 2025-09-14 14:19:18 +02:00
Carlos Valente 8d3ce46c56 fix: allow moving a group after another 2025-09-14 14:19:18 +02:00
Carlos Valente 578cb2b244 refactor: improve pin styling 2025-09-14 14:19:18 +02:00
Carlos Valente c79ee193c9 fix: prevent layout reflow on different entry types 2025-09-14 14:19:18 +02:00
Carlos Valente 5810ae0d54 refactor: add default value to secondary source 2025-09-14 14:19:18 +02:00
Carlos Valente bfbf8574e3 fix: phase style overrides in timer 2025-09-14 14:19:18 +02:00
Alex Christoffer Rasmussen 490e429f44 cleanup (#1776)
* chore: cleanup leftover console log

* chore: prevent error in client tsconfig
2025-09-13 16:31:56 +02:00
Alex Christoffer Rasmussen 2e950965e0 fix: sheet default import map (#1775)
* fix: default import map

* fix: update test
2025-09-08 09:49:01 +02:00
arc-alex cc34db775a chore: bump version 2025-09-08 06:51:07 +02:00
89 changed files with 575 additions and 1605 deletions
-42
View File
@@ -1,42 +0,0 @@
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
-1
View File
@@ -8,4 +8,3 @@ playwright-report
**/*.toml
**/*.yml
**/*.json
!tsconfig.common.json
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@getontime/cli",
"version": "4.0.0-beta.4",
"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.4",
"version": "4.0.0-beta.3",
"private": true,
"type": "module",
"dependencies": {
-2
View File
@@ -8,7 +8,6 @@ import IdentifyOverlay from './common/components/identify-overlay/IdentifyOverla
import { AppContextProvider } from './common/context/AppContext';
import { ontimeQueryClient } from './common/queryClient';
import { connectSocket } from './common/utils/socket';
import KeepAwake from './features/keep-awake/KeepAwake';
import { TranslationProvider } from './translation/TranslationProvider';
import AppRouter from './AppRouter';
import { baseURI } from './externals';
@@ -25,7 +24,6 @@ function App() {
<ErrorBoundary>
<TranslationProvider>
<IdentifyOverlay />
<KeepAwake />
<AppRouter />
</TranslationProvider>
</ErrorBoundary>
@@ -1,11 +1,10 @@
import { memo } from 'react';
import { IoClose, IoContract, IoExpand, IoEye, IoLockClosedOutline, IoSwapVertical } from 'react-icons/io5';
import { IoClose, IoContract, IoExpand, IoLockClosedOutline, IoSwapVertical } from 'react-icons/io5';
import { useLocation } from 'react-router';
import { Dialog } from '@base-ui-components/react/dialog';
import { useDisclosure, useFullscreen } from '@mantine/hooks';
import { isLocalhost } from '../../../externals';
import { useKeepAwakeOptions } from '../../../features/keep-awake/KeepAwake';
import { navigatorConstants } from '../../../viewerConfig';
import { useClientStore } from '../../stores/clientStore';
import { useViewOptionsStore } from '../../stores/viewOptions';
@@ -32,7 +31,6 @@ function NavigationMenu({ isOpen, onClose }: NavigationMenuProps) {
const [isRenameOpen, handlers] = useDisclosure(false);
const { fullscreen, toggle } = useFullscreen();
const { mirror, toggleMirror } = useViewOptionsStore();
const { keepAwake, toggleKeepAwake } = useKeepAwakeOptions();
const location = useLocation();
return (
@@ -64,13 +62,6 @@ function NavigationMenu({ isOpen, onClose }: NavigationMenuProps) {
<IoSwapVertical />
{mirror && <span className={style.note}>Active</span>}
</NavigationMenuItem>
{window.isSecureContext && (
<NavigationMenuItem active={keepAwake} onClick={toggleKeepAwake}>
Keep Awake
<IoEye />
{keepAwake && <span className={style.note}>Active</span>}
</NavigationMenuItem>
)}
<NavigationMenuItem onClick={handlers.open}>Rename Client</NavigationMenuItem>
<hr className={style.separator} />
@@ -28,7 +28,6 @@
bottom: 0;
width: 40rem;
max-width: 100vw;
height: 100vh;
display: flex;
+19 -40
View File
@@ -164,6 +164,24 @@ 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,
@@ -172,7 +190,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.rundown.currentDay ?? 0,
currentDay: state.eventNow?.dayOffset ?? 0,
actualStart: state.rundown.actualStart,
plannedStart: state.rundown.plannedStart,
clock: state.clock,
@@ -209,45 +227,6 @@ 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) => ({
@@ -0,0 +1 @@
export type Size = 'xs' | 'sm' | 'md' | 'lg';
@@ -0,0 +1,89 @@
import { create } from 'zustand';
type Target = 'cuesheet' | 'timer' | 'clock' | 'countdown' | 'backstage' | 'studio';
interface SelectionState {
[key: string]: boolean;
}
interface ColumnPermissions {
read: string[];
write: string[];
}
interface CuesheetLinksState {
target: Target | null;
readSelected: SelectionState;
writeSelected: SelectionState;
setTarget: (target: Target | null) => void;
setField: (field: 'read' | 'write', key: string, value: boolean) => void;
toggleField: (field: 'read' | 'write', key: string) => void;
selectAll: (field: 'read' | 'write', keys: string[]) => void;
clearAll: (field: 'read' | 'write', keys: string[]) => void;
// Returns arrays of column keys that have read/write permissions if target is 'cuesheet'
getSelections: () => ColumnPermissions | null;
}
export const useCuesheetLinksStore = create<CuesheetLinksState>((set, get) => ({
target: null,
readSelected: {},
writeSelected: {},
setTarget: (target) => set({ target }),
setField: (field, key, value) =>
set((state) => ({
...(field === 'read'
? { readSelected: { ...state.readSelected, [key]: value } }
: { writeSelected: { ...state.writeSelected, [key]: value } }),
})),
toggleField: (field, key) =>
set((state) => ({
...(field === 'read'
? { readSelected: { ...state.readSelected, [key]: !state.readSelected[key] } }
: { writeSelected: { ...state.writeSelected, [key]: !state.writeSelected[key] } }),
})),
selectAll: (field, keys) =>
set((_state) => ({
...(field === 'read'
? {
readSelected: keys.reduce((acc, key) => {
acc[key] = true;
return acc;
}, {} as SelectionState),
}
: {
writeSelected: keys.reduce((acc, key) => {
acc[key] = true;
return acc;
}, {} as SelectionState),
}),
})),
clearAll: (field, keys) =>
set((_state) => ({
...(field === 'read'
? {
readSelected: keys.reduce((acc, key) => {
acc[key] = false;
return acc;
}, {} as SelectionState),
}
: {
writeSelected: keys.reduce((acc, key) => {
acc[key] = false;
return acc;
}, {} as SelectionState),
}),
})),
getSelections: () => {
const state = get();
if (state.target !== 'cuesheet') return null;
return {
read: Object.entries(state.readSelected)
.filter(([_, selected]) => selected)
.map(([key]) => key),
write: Object.entries(state.writeSelected)
.filter(([_, selected]) => selected)
.map(([key]) => key),
};
},
}));
@@ -0,0 +1,8 @@
import { MaybeString } from 'ontime-types';
export default function safeParseNumber(value: MaybeString, defaultValue: number = 0): number {
if (!value) return defaultValue;
const number = Number(value);
if (isNaN(number)) return defaultValue;
return number;
}
+2 -2
View File
@@ -1,5 +1,5 @@
import {
ApiActionTag,
ApiAction,
Log,
MessageTag,
RefetchKey,
@@ -199,7 +199,7 @@ export const connectSocket = () => {
};
};
export function sendSocket<T extends MessageTag | ApiActionTag>(
export function sendSocket<T extends MessageTag | ApiAction>(
tag: T,
payload: T extends MessageTag ? Pick<WsPacketToServer & { tag: T }, 'payload'>['payload'] : unknown,
): void {
+5 -5
View File
@@ -33,8 +33,8 @@ export function validateProjectFile(file: File) {
}
// Limit file size of a project file to around 1MB
if (file.size > 2_000_000) {
throw new Error('File size limit (2MB) exceeded');
if (file.size > 1_000_000) {
throw new Error('File size limit (1MB) exceeded');
}
}
@@ -56,8 +56,8 @@ export function validateLogo(file: File) {
throw new Error('File is empty');
}
// Limit file size of a project file to around 1.5MB
if (file.size > 1_500_000) {
throw new Error('File size limit (1.5MB) exceeded');
// Limit file size of a project file to around 1MB
if (file.size > 1_000_000) {
throw new Error('File size limit (1MB) exceeded');
}
}
@@ -0,0 +1,29 @@
/* eslint-disable react/display-name */
import { ComponentType, useEffect } from 'react';
import { useLocation, useNavigate } from 'react-router';
import useUrlPresets from '../common/hooks-query/useUrlPresets';
import { getRouteFromPreset } from '../common/utils/urlPresets';
const withPreset = <P extends object>(Component: ComponentType<P>) => {
return (props: Partial<P>) => {
const { data } = useUrlPresets();
const navigate = useNavigate();
const location = useLocation();
// navigate if is alias route
useEffect(() => {
if (!data) return;
const destination = getRouteFromPreset(location, data);
// navigate to this destination if its not null
if (destination) {
navigate(destination);
}
}, [data, navigate, location]);
return <Component {...(props as P)} />;
};
};
export default withPreset;
@@ -107,9 +107,6 @@ export default function AutomationsList(props: AutomationsListProps) {
</IconButton>
</Panel.InlineElements>
</tr>
</Fragment>
);
})}
{deleteError && (
<tr>
<td colSpan={5}>
@@ -117,6 +114,9 @@ export default function AutomationsList(props: AutomationsListProps) {
</td>
</tr>
)}
</Fragment>
);
})}
</tbody>
</Panel.Table>
</Panel.Card>
@@ -2,7 +2,6 @@ import { useState } from 'react';
import { CustomFields, Rundown } from 'ontime-types';
import Button from '../../../../../common/components/buttons/Button';
import useRundown from '../../../../../common/hooks-query/useRundown';
import * as Panel from '../../../panel-utils/PanelUtils';
import PreviewSpreadsheet from './preview/PreviewRundown';
@@ -18,7 +17,7 @@ interface ImportReviewProps {
export default function ImportReview(props: ImportReviewProps) {
const { rundown, customFields, onFinished, onCancel } = props;
const { data: currentRundown } = useRundown();
const [loading, setLoading] = useState(false);
const { importRundown } = useGoogleSheet();
const resetPreview = useSheetStore((state) => state.resetPreview);
@@ -30,12 +29,9 @@ export default function ImportReview(props: ImportReviewProps) {
const applyImport = async () => {
setLoading(true);
// we need to import on-top of the currently loaded rundown
// so the id needs to match
await importRundown(
{
[currentRundown.id]: { ...rundown, id: currentRundown.id, title: currentRundown.title },
[rundown.id]: rundown,
},
customFields,
);
@@ -27,7 +27,6 @@ export default function ProjectData() {
reset,
formState: { isSubmitting, isValid, isDirty, errors },
setError,
clearErrors,
watch,
control,
setValue,
@@ -54,7 +53,6 @@ export default function ProjectData() {
const handleUploadProjectLogo = async (event: ChangeEvent<HTMLInputElement>) => {
const file = event.target.files?.[0];
clearErrors('logo');
if (!file) {
return;
@@ -95,7 +93,6 @@ export default function ProjectData() {
const onSubmit = async (formData: ProjectData) => {
try {
clearErrors();
await updateProjectData(formData);
} catch (error) {
const message = maybeAxiosError(error);
@@ -1,84 +0,0 @@
import { use, useCallback, useEffect, useMemo, useState } from 'react';
import { useSearchParams } from 'react-router';
import { PresetContext } from '../../common/context/PresetContext';
/** @url https://developer.mozilla.org/en-US/docs/Web/API/WakeLock */
export default function KeepAwake() {
const { keepAwake } = useKeepAwakeOptions();
const [wakeLockSentinel, setWakeLockSentinel] = useState<WakeLockSentinel | null>(null);
const removeLock = () => {
if (wakeLockSentinel) wakeLockSentinel.release().finally(() => setWakeLockSentinel(null));
};
const acquireLock = () => {
if (!wakeLockSentinel || wakeLockSentinel.released) {
setWakeLockSentinel(null);
navigator.wakeLock
.request('screen')
.then((sentinel) => {
setWakeLockSentinel(sentinel);
})
.catch(console.error);
}
};
useEffect(() => {
const controller = new AbortController();
if (keepAwake) {
acquireLock();
document.addEventListener(
'visibilitychange',
() => {
if (wakeLockSentinel !== null && document.visibilityState === 'visible') {
acquireLock();
}
},
{ signal: controller.signal },
);
} else {
removeLock();
}
return () => {
controller.abort();
removeLock();
};
}, [keepAwake]);
return <></>;
}
const keepAwakeKey = 'keep-awake';
function getOptionsFromParams(searchParams: URLSearchParams, defaultValues?: URLSearchParams) {
// Helper to get value from either source, prioritizing defaultValues
return defaultValues?.has(keepAwakeKey) || searchParams.has(keepAwakeKey);
}
/**
* Hook exposes the keep awake options
*/
export function useKeepAwakeOptions() {
const [searchParams, setSearchParams] = useSearchParams();
const maybePreset = use(PresetContext);
const keepAwake = useMemo(() => {
const defaultValues = maybePreset ? new URLSearchParams(maybePreset.search) : undefined;
return getOptionsFromParams(searchParams, defaultValues);
}, [maybePreset, searchParams]);
const toggleKeepAwake = useCallback(() => {
setSearchParams((searchParams) => {
if (keepAwake) {
searchParams.delete(keepAwakeKey);
} else {
searchParams.set(keepAwakeKey, '1');
}
return searchParams;
});
}, [keepAwake]);
return { keepAwake, toggleKeepAwake };
}
@@ -47,10 +47,9 @@
.daySpan {
&::after {
content: "+"attr(data-day-offset);
content: '*';
vertical-align: super;
font-size: 0.6em;
letter-spacing: 0;
font-size: 0.75em;
color: $info-blue;
}
}
@@ -68,10 +67,3 @@
font-size: calc(1rem - 2px);
text-align: right;
}
.dueTime {
text-transform: capitalize;
font-size: 1rem;
letter-spacing: 0;
color: $playback-over;
}
@@ -8,26 +8,23 @@ import {
TbFolderPin,
TbFolderStar,
} from 'react-icons/tb';
import { OffsetMode, OntimeEvent, OntimeGroup, TimerPhase, TimerType } from 'ontime-types';
import { dayInMs, isPlaybackActive, millisToString } from 'ontime-utils';
import { OntimeEvent, OntimeGroup, TimerPhase, TimerType } from 'ontime-types';
import { 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, formatDueTime, formattedTime } from '../overview.utils';
import { calculateEndAndDaySpan, formattedTime } from '../overview.utils';
import { OverUnder, TimeColumn } from './TimeLayout';
@@ -61,8 +58,8 @@ export function StartTimes() {
<Tooltip text='Planned end time' render={<TbCalendarPin className={style.icon} />} />
{maybePlannedDaySpan > 0 ? (
<Tooltip
text={`Rundown spans over ${maybePlannedDaySpan + 1} days`}
render={<span className={cx([style.time, style.daySpan])} data-day-offset={maybePlannedDaySpan} />}
text={`Event spans over ${maybePlannedDaySpan + 1} days`}
render={<span className={cx([style.time, style.daySpan])} />}
>
{plannedEndText}
</Tooltip>
@@ -74,8 +71,8 @@ export function StartTimes() {
<Tooltip text='Expected end time' render={<TbCalendarStar className={style.icon} />} />
{maybeExpectedEnd !== null && maybeExpectedDaySpan > 0 ? (
<Tooltip
text={`Rundown spans over ${maybeExpectedDaySpan + 1} days`}
render={<span className={cx([style.time, style.daySpan])} data-day-offset={maybeExpectedDaySpan} />}
text={`Event spans over ${maybeExpectedDaySpan + 1} days`}
render={<span className={cx([style.time, style.daySpan])} />}
>
{formattedTime(maybeExpectedEnd)}
</Tooltip>
@@ -99,110 +96,61 @@ 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, mode, groupExpectedEnd, actualGroupStart, currentDay, playback } = useGroupTimerOverView();
const { clock, groupExpectedEnd } = useRuntimePlaybackOverview();
const { currentGroupId } = useCurrentGroupId();
const group = useEntry(currentGroupId) as OntimeGroup | null;
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);
// 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 expectedGroupEnd = groupExpectedEnd !== null ? groupExpectedEnd - clock : null;
const expectedTimeUntilGroupEnd = formatDueTime(expectedGroupEnd, 3, TimerType.CountDown);
const expectedTimeUntilGroupEnd = formattedTime(expectedGroupEnd, 3, TimerType.CountDown);
const groupTitle = group?.title ?? null;
return (
<div className={style.metadataRow}>
<span className={group?.title ? style.labelTitle : style.label}>{`${group?.title || 'Group'} `}</span>
<span className={groupTitle ? style.labelTitle : style.label}>{`${groupTitle ? groupTitle : 'Group'} `}</span>
<div className={style.labelledElement}>
<Tooltip text='Time to planned group end' render={<TbFolderPin className={style.icon} />} />
<span
className={cx([
style.time,
(!group || !active) && style.muted,
plannedTimeUntilGroupEnd === 'due' && style.dueTime,
])}
>
{plannedTimeUntilGroupEnd}
</span>
<span className={cx([style.time, !group && style.muted])}>{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 && style.muted,
expectedTimeUntilGroupEnd === 'due' && style.dueTime,
])}
>
{expectedTimeUntilGroupEnd}
</span>
<span className={cx([style.time, groupExpectedEnd === null && style.muted])}>{expectedTimeUntilGroupEnd}</span>
</div>
</div>
);
}
function FlagTimes() {
const { clock, mode, actualStart, plannedStart, playback, currentDay } = useFlagTimerOverView();
const { clock } = useClock();
const { id, expectedStart } = useNextFlag();
const entry = useEntry(id) as OntimeEvent | null;
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 plannedFlagStart = entry ? entry.timeStart - clock : null;
const plannedTimeUntilDisplay = formattedTime(plannedFlagStart, 3, TimerType.CountDown);
const expectedTimeUntil = expectedStart !== null ? expectedStart - clock : null;
const expectedTimeUntilDisplay = formatDueTime(expectedTimeUntil, 3, TimerType.CountDown);
const expectedTimeUntilDisplay = formattedTime(expectedTimeUntil, 3, TimerType.CountDown);
const title = entry?.title ?? null;
return (
<div className={style.metadataRow}>
<span className={title ? style.labelTitle : style.label}>{`${title || 'Flag'} `}</span>
<span className={title ? style.labelTitle : style.label}>{`${title ? 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 || !active) && style.muted,
plannedTimeUntilDisplay === 'due' && style.dueTime,
])}
>
<span data-testid='flag-plannedStart' className={cx([style.time, !entry && style.muted])}>
{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,
expectedTimeUntilDisplay === 'due' && style.dueTime,
])}
>
<span data-testid='flag-expectedStart' className={cx([style.time, expectedTimeUntil === null && style.muted])}>
{expectedTimeUntilDisplay}
</span>
</div>
@@ -211,7 +159,7 @@ function FlagTimes() {
}
export function ProgressOverview() {
const { numEvents, selectedEventIndex } = useProgressOverview();
const { numEvents, selectedEventIndex } = useRuntimePlaybackOverview();
const current = selectedEventIndex !== null ? selectedEventIndex + 1 : enDash;
const progressText = numEvents ? `${current} of ${numEvents || enDash}` : enDash;
@@ -220,7 +168,7 @@ export function ProgressOverview() {
}
export function OffsetOverview() {
const { offset, playback } = useOffsetOverview();
const { offset, playback } = useRuntimePlaybackOverview();
const isPlaying = isPlaybackActive(playback);
const offsetState = getOffsetState(isPlaying ? offset : null);
@@ -3,23 +3,6 @@ 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
*/
@@ -73,7 +73,9 @@ interface EventUntilProps {
isLinkedToLoaded: boolean;
}
function EventUntil({ timeStart, delay, dayOffset, totalGap, isLinkedToLoaded }: EventUntilProps) {
function EventUntil(props: EventUntilProps) {
const { timeStart, delay, dayOffset, totalGap, isLinkedToLoaded } = props;
const timeUntil = useTimeUntilExpectedStart({ timeStart, delay, dayOffset }, { totalGap, isLinkedToLoaded });
const isDue = timeUntil < MILLIS_PER_SECOND;
+1
View File
@@ -17,6 +17,7 @@ $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,14 +10,13 @@ import {
makeProjectDataOptions,
} from '../../common/components/view-params-editor/viewParams.utils';
import { PresetContext } from '../../common/context/PresetContext';
import { getScheduleOptions } from '../common/schedule/schedule.options';
import { scheduleOptions } 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' },
@@ -40,7 +39,7 @@ export const getBackstageOptions = (
},
],
},
getScheduleOptions(customFieldOptions),
scheduleOptions,
{
title: OptionTitle.ElementVisibility,
collapsible: true,
@@ -1,5 +1,14 @@
import { createContext, PropsWithChildren, RefObject, use, useEffect, useLayoutEffect, useRef, useState } from 'react';
import { EntryId, isOntimeEvent, OntimeEntry, OntimeEvent } from 'ontime-types';
import {
createContext,
PropsWithChildren,
RefObject,
useContext,
useEffect,
useLayoutEffect,
useRef,
useState,
} from 'react';
import { isOntimeEvent, OntimeEntry, OntimeEvent } from 'ontime-types';
import { usePartialRundown } from '../../../common/hooks-query/useRundown';
@@ -16,18 +25,13 @@ interface ScheduleContextState {
const ScheduleContext = createContext<ScheduleContextState | undefined>(undefined);
interface ScheduleProviderProps {
selectedEventId: EntryId | null;
selectedEventId: string | null;
}
export const ScheduleProvider = ({ children, selectedEventId }: PropsWithChildren<ScheduleProviderProps>) => {
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 { cycleInterval, stopCycle } = useScheduleOptions();
const { data: events } = usePartialRundown((event: OntimeEntry) => {
return isOntimeEvent(event);
});
const [firstIndex, setFirstIndex] = useState(-1);
@@ -131,7 +135,7 @@ export const ScheduleProvider = ({ children, selectedEventId }: PropsWithChildre
selectedEventIndex = 0;
return (
<ScheduleContext
<ScheduleContext.Provider
value={{
events: viewEvents as OntimeEvent[],
selectedEventId,
@@ -141,12 +145,12 @@ export const ScheduleProvider = ({ children, selectedEventId }: PropsWithChildre
}}
>
{children}
</ScheduleContext>
</ScheduleContext.Provider>
);
};
export const useSchedule = () => {
const context = use(ScheduleContext);
const context = useContext(ScheduleContext);
if (!context) {
throw new Error('useSchedule() can only be used inside a ScheduleContext');
}
@@ -22,7 +22,8 @@ interface ScheduleItemProps {
delay: number;
}
export default function ScheduleItem({ timeStart, timeEnd, title, colour, skip, delay }: ScheduleItemProps) {
export default function ScheduleItem(props: ScheduleItemProps) {
const { timeStart, timeEnd, title, colour, skip, delay } = props;
const { showExpected } = useScheduleOptions();
if (showExpected) {
@@ -66,7 +67,9 @@ export default function ScheduleItem({ timeStart, timeEnd, title, colour, skip,
);
}
function DelayedScheduleItem({ timeStart, timeEnd, title, colour, skip, delay }: ScheduleItemProps) {
function DelayedScheduleItem(props: ScheduleItemProps) {
const { timeStart, timeEnd, title, colour, skip, delay } = props;
const start = formatTime(timeStart, formatOptions);
const end = formatTime(timeEnd, formatOptions);
const delayedStart = formatTime(timeStart + delay, formatOptions);
@@ -92,7 +95,9 @@ function DelayedScheduleItem({ timeStart, timeEnd, title, colour, skip, delay }:
);
}
function ExpectedScheduleItem({ timeStart, timeEnd, title, colour, skip, delay }: ScheduleItemProps) {
function ExpectedScheduleItem(props: ScheduleItemProps) {
const { timeStart, timeEnd, title, colour, skip, delay } = props;
return (
<li className={cx(['entry', skip && 'entry--skip'])}>
<div className='entry-times'>
@@ -111,11 +116,12 @@ interface ExpectedTimeProps {
delay: number;
}
function ExpectedTime({ time, delay }: ExpectedTimeProps) {
function ExpectedTime(props: ExpectedTimeProps) {
const { time, delay } = props;
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,23 +1,14 @@
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 type { ViewOption } from '../../../common/components/view-params-editor/viewParams.types';
import { ViewOption } from '../../../common/components/view-params-editor/viewParams.types';
import { isStringBoolean } from '../../../features/viewers/common/viewUtils';
export const getScheduleOptions = (customFieldOptions: SelectOption[]): ViewOption => ({
export const scheduleOptions: 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',
@@ -40,10 +31,9 @@ export const getScheduleOptions = (customFieldOptions: SelectOption[]): ViewOpti
defaultValue: false,
},
],
});
};
type ScheduleOptions = {
filter: string | null;
cycleInterval: number;
stopCycle: boolean;
showExpected: boolean;
@@ -51,7 +41,6 @@ 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: $ontime-delay-text;
color: $delay-color;
}
.sub__schedule--strike {
@@ -1,6 +1,6 @@
import { useMemo, useState } from 'react';
import { IoAdd } from 'react-icons/io5';
import { EntryId, isOntimeEvent, isPlayableEvent, OntimeEvent, OntimeView } from 'ontime-types';
import { isOntimeEvent, isPlayableEvent, OntimeEvent, OntimeView } from 'ontime-types';
import Button from '../../common/components/buttons/Button';
import Empty from '../../common/components/state/Empty';
@@ -16,7 +16,7 @@ import { useTranslation } from '../../translation/TranslationProvider';
import Loader from '../common/loader/Loader';
import { getCountdownOptions, useCountdownOptions } from './countdown.options';
import { getOrderedSubscriptions } from './countdown.utils';
import { CountdownSubscription, getOrderedSubscriptions } from './countdown.utils';
import CountdownSelect from './CountdownSelect';
import CountdownSubscriptions from './CountdownSubscriptions';
import SingleEventCountdown from './SingleEventCountdown';
@@ -59,6 +59,8 @@ function Countdown({ customFields, rundownData, projectData, isMirrored, setting
[defaultFormat, customFields, subscriptions],
);
console.log(subscriptions)
return (
<div className={`countdown ${isMirrored ? 'mirror' : ''}`} data-testid='countdown-view'>
<ViewParamsEditor target={OntimeView.Countdown} viewOptions={countdownOptions} />
@@ -87,7 +89,7 @@ function Countdown({ customFields, rundownData, projectData, isMirrored, setting
interface CountdownContentsProps {
playableEvents: ExtendedEntry<OntimeEvent>[];
subscriptions: EntryId[];
subscriptions: CountdownSubscription;
goToEditMode: () => void;
}
@@ -1,5 +1,5 @@
import { useState } from 'react';
import { IoArrowBack, IoClose, IoSaveOutline } from 'react-icons/io5';
import { IoArrowBack, IoClose, IoSaveOutline, IoAlbumsOutline } from 'react-icons/io5';
import { useNavigate } from 'react-router';
import { EntryId, OntimeEvent } from 'ontime-types';
@@ -7,18 +7,19 @@ import Button from '../../common/components/buttons/Button';
import { cx } from '../../common/utils/styleUtils';
import ClockTime from '../../features/viewers/common/clock-time/ClockTime';
import { makeSubscriptionsUrl } from './countdown.utils';
import { CountdownSubscription, makeSubscriptionsUrl } from './countdown.utils';
import './Countdown.scss';
interface CountdownSelectProps {
events: OntimeEvent[];
subscriptions: EntryId[];
subscriptions: CountdownSubscription;
disableEdit: () => void;
}
export default function CountdownSelect({ events, subscriptions, disableEdit }: CountdownSelectProps) {
const [selected, setSelected] = useState<EntryId[]>(subscriptions);
const maybeAllSubscriptions: EntryId[] = subscriptions === 'all' ? events.map((event) => event.id) : subscriptions;
const [selected, setSelected] = useState<EntryId[]>(maybeAllSubscriptions);
const navigate = useNavigate();
/**
@@ -47,6 +48,18 @@ export default function CountdownSelect({ events, subscriptions, disableEdit }:
navigate(url.search.toString());
};
/**
* Creates a URL with all
* and navigates to it
*/
const applyAll = () => {
// we remove events that no longer exist to avoid stale subscriptions
const url = makeSubscriptionsUrl(window.location.href, 'all');
disableEdit();
setSelected([]);
navigate(url.search.toString());
};
// make a copy of the selected array for quick lookup
const selectedIds = new Set(selected);
@@ -86,6 +99,10 @@ export default function CountdownSelect({ events, subscriptions, disableEdit }:
<Button variant='subtle' size='xlarge' onClick={disableEdit}>
<IoArrowBack /> Go back
</Button>
<Button variant='subtle' size='xlarge' onClick={applyAll}>
{/* TODO: icon ??? */}
<IoAlbumsOutline /> Use All
</Button>
<Button variant='subtle' size='xlarge' onClick={() => setSelected([])} disabled={selected.length === 0}>
<IoClose /> Clear
</Button>
@@ -8,11 +8,12 @@ import { ViewOption } from '../../common/components/view-params-editor/viewParam
import { makeOptionsFromCustomFields } from '../../common/components/view-params-editor/viewParams.utils';
import { PresetContext } from '../../common/context/PresetContext';
import { isStringBoolean } from '../../features/viewers/common/viewUtils';
import { CountdownSubscription } from './countdown.utils';
export const getCountdownOptions = (
timeFormat: string,
customFields: CustomFields,
persistedSubscriptions: EntryId[],
persistedSubscriptions: CountdownSubscription,
): ViewOption[] => {
const secondaryOptions = makeOptionsFromCustomFields(customFields, [
{ value: 'none', label: 'None' },
@@ -55,7 +56,7 @@ export const getCountdownOptions = (
id: 'sub',
title: 'Event subscription',
description: 'The events to follow',
values: persistedSubscriptions,
values: persistedSubscriptions === 'all' ? ['all'] : persistedSubscriptions,
type: 'persist',
},
],
@@ -64,7 +65,7 @@ export const getCountdownOptions = (
};
type CountdownOptions = {
subscriptions: EntryId[];
subscriptions: CountdownSubscription;
secondarySource: keyof OntimeEvent | null;
showExpected: boolean;
};
@@ -85,8 +86,10 @@ function getOptionsFromParams(searchParams: URLSearchParams, defaultValues?: URL
return searchParams.getAll(key) as EntryId[];
};
const subscriptions = getArrayValues('sub');
return {
subscriptions: getArrayValues('sub'),
subscriptions: subscriptions.at(0) === 'all' ? 'all' : subscriptions,
secondarySource: getValue('secondary-src') as keyof OntimeEvent | null,
showExpected: isStringBoolean(getValue('showExpected')),
};
@@ -14,6 +14,8 @@ export function sanitiseTitle(title: string | null) {
return title ?? '{no title}';
}
export type CountdownSubscription = EntryId[] | 'all';
export const preferredFormat12 = 'h:mm a';
export const preferredFormat24 = 'HH:mm';
@@ -120,7 +122,7 @@ export function useSubscriptionDisplayData(
/**
* Adds a set of subscriptions to the URL parameters
*/
export function makeSubscriptionsUrl(urlRef: string, subscriptions: EntryId[]) {
export function makeSubscriptionsUrl(urlRef: string, subscriptions: CountdownSubscription) {
const url = new URL(urlRef);
const newParams = new URLSearchParams();
@@ -131,10 +133,14 @@ export function makeSubscriptionsUrl(urlRef: string, subscriptions: EntryId[]) {
}
}
if (subscriptions === 'all') {
newParams.append('sub', 'all');
} else {
// add new subscriptions
subscriptions.forEach((id) => {
newParams.append('sub', id);
});
}
url.search = newParams.toString();
@@ -146,38 +152,14 @@ export function makeSubscriptionsUrl(urlRef: string, subscriptions: EntryId[]) {
* Since the original array is already ordered, we simply filter out the events
* which are not in the subscriptions list.
*/
export function getOrderedSubscriptions<T extends OntimeEntry>(subscriptions: EntryId[], playableEvents: T[]): T[] {
export function getOrderedSubscriptions<T extends OntimeEntry>(
subscriptions: CountdownSubscription,
playableEvents: T[],
): T[] {
if (subscriptions === 'all') return playableEvents;
return playableEvents.filter((event) => subscriptions.includes(event.id));
}
/**
* Checks through the rundown whether the current event is linked to the loaded event
*/
export function isLinkedToLoadedEvent(events: OntimeEvent[], loadedId: EntryId | null, currentId: EntryId): boolean {
// if nothing is loaded, we return true to simplify the logic
if (!loadedId) {
return true;
}
const loadedIndex = events.findIndex((event) => event.id === loadedId);
if (loadedIndex === -1) {
return true;
}
for (let i = loadedIndex; i < events.length; i++) {
const event = events[i];
if (event.id === currentId) {
return true;
}
if (event.linkStart === null) {
return false;
}
}
return true;
}
export function isOutsideRange(a: number, b: number): boolean {
return Math.abs(a - b) > MILLIS_PER_MINUTE;
}
@@ -0,0 +1,51 @@
import { useVisibleRowsStore } from './visibleRowsStore';
let observer: IntersectionObserver | null = null;
function getObserver(): IntersectionObserver {
if (!observer) {
const options: IntersectionObserverInit = {
root: null,
rootMargin: '400px 0px', // prevent unmounting rows too early
threshold: 0.25,
};
const handleOnIntersect: IntersectionObserverCallback = (entries) => {
const visibleRows = useVisibleRowsStore.getState();
entries.forEach((entry) => {
const targetId = entry.target.id;
if (entry.isIntersecting) {
visibleRows.addVisibleRow(targetId);
} else {
visibleRows.removeVisibleRow(targetId);
}
});
};
observer = new IntersectionObserver(handleOnIntersect, options);
}
return observer;
}
/**
* register a row element in the observer
*/
export function observeRow(element: HTMLElement) {
getObserver().observe(element);
}
/**
* unregister a row element in the observer
*/
export function unobserveRow(element: HTMLElement) {
getObserver().unobserve(element);
}
/**
* cleanup observer, should be called when the table component unmounts
*/
export function cleanup() {
observer?.disconnect();
observer = null;
}
@@ -0,0 +1,18 @@
import { create } from 'zustand';
interface VisibleRowsStore {
visibleRows: Set<string>;
addVisibleRow: (id: string) => void;
removeVisibleRow: (id: string) => void;
}
export const useVisibleRowsStore = create<VisibleRowsStore>((set) => ({
visibleRows: new Set(),
addVisibleRow: (id) => set((state) => ({ visibleRows: new Set(state.visibleRows).add(id) })),
removeVisibleRow: (id) =>
set((state) => {
const newSet = new Set(state.visibleRows);
newSet.delete(id);
return { visibleRows: newSet };
}),
}));
@@ -105,7 +105,7 @@ $timeline-color: color-mix(in srgb, transparent 60%, var(--background-color-over
}
.delay {
color: $ontime-delay-text;
color: $delay-color;
}
.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: $ontime-delay-text;
text-decoration-color: $delay-color;
}
.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 {
+15 -2
View File
@@ -1,7 +1,6 @@
{
"extends": "../../tsconfig.common.json",
"compilerOptions": {
"target": "esnext",
"target": "ESNext",
"lib": [
"dom",
"dom.iterable",
@@ -14,7 +13,21 @@
],
"module": "esnext",
"moduleResolution": "node",
"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,
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "ontime-electron",
"version": "4.0.0-beta.4",
"version": "4.0.0-beta.3",
"author": "Carlos Valente",
"description": "Time keeping for live events",
"repository": "https://github.com/cpvalente/ontime",
-1
View File
@@ -1 +0,0 @@
*.tgz
-17
View File
@@ -1,17 +0,0 @@
# 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)
-28
View File
@@ -1,28 +0,0 @@
{
"name": "@getontime/resolver",
"version": "4.0.0-beta.4",
"type": "module",
"repository": "https://github.com/cpvalente/ontime",
"types": "./dist/main.d.ts",
"main": "./dist/main.js",
"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
@@ -1,20 +0,0 @@
// 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
@@ -1,18 +0,0 @@
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;
}
-22
View File
@@ -1,22 +0,0 @@
{
"extends": "../../tsconfig.common.json",
"compilerOptions": {
"target": "esnext",
"module": "preserve",
"moduleResolution": "bundler",
"allowUnusedLabels": false,
"removeComments": true,
"preserveConstEnums": true,
"allowJs": true,
"declaration": true,
"outDir": "dist",
"sourceMap": true,
"baseUrl": "src",
},
"include": [
"src",
],
"exclude": [
"node_modules",
]
}
-20
View File
@@ -1,20 +0,0 @@
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',
}),
],
});
+3 -2
View File
@@ -2,7 +2,7 @@
"name": "ontime-server",
"type": "module",
"main": "src/index.ts",
"version": "4.0.0-beta.4",
"version": "4.0.0-beta.3",
"exports": "./src/index.js",
"dependencies": {
"@googleapis/sheets": "^5.0.5",
@@ -20,6 +20,7 @@
"ontime-utils": "workspace:*",
"osc-min": "2.1.2",
"sanitize-filename": "^1.6.3",
"steno": "^4.0.2",
"ws": "^8.18.0",
"xlsx": "^0.18.5"
},
@@ -40,7 +41,7 @@
"prettier": "catalog:",
"server-timing": "^3.3.3",
"shx": "^0.3.4",
"ts-essentials": "catalog:",
"ts-essentials": "^10.0.3",
"tsx": "^4.19.2",
"typescript": "catalog:",
"vitest": "catalog:"
@@ -1,6 +1,6 @@
import { TriggerDTO, TimerLifeCycle, AutomationDTO, Automation, ProjectRundowns } from 'ontime-types';
import { TriggerDTO, TimerLifeCycle, AutomationDTO, Automation, EntryId } from 'ontime-types';
import { makeOntimeEvent } from '../../rundown/__mocks__/rundown.mocks.js';
import { makeRundown } from '../../rundown/__mocks__/rundown.mocks.js';
import {
addTrigger,
@@ -203,29 +203,10 @@ describe('deleteAutomation()', () => {
const automations = getAutomations();
expect(Object.keys(automations).length).toEqual(1);
const projectRundowns: ProjectRundowns = {
'rundown-1': {
id: 'rundown-1',
title: 'Rundown 1',
order: ['1'],
flatOrder: ['1'],
entries: {
'1': makeOntimeEvent({
id: '1',
triggers: [
{
id: 'trigger-1',
title: 'Trigger 1',
trigger: TimerLifeCycle.onClock,
automationId: 'test-automation',
},
],
}),
},
revision: 1,
},
};
await deleteAutomation(projectRundowns, Object.keys(automations)[0]);
const rundown = makeRundown({});
const timedEventOrder: EntryId[] = [];
await deleteAutomation(rundown, timedEventOrder, Object.keys(automations)[0]);
const removed = getAutomations();
expect(Object.keys(removed).length).toEqual(0);
});
@@ -1,7 +1,5 @@
import { ProjectRundowns, TimerLifeCycle } from 'ontime-types';
import { makeOntimeEvent } from '../../rundown/__mocks__/rundown.mocks.js';
import { TimerLifeCycle } from 'ontime-types';
import { makeOntimeEvent, makeRundown } from '../../rundown/__mocks__/rundown.mocks.js';
import { isAutomationUsed, parseTemplateNested, stringToOSCArgs } from '../automation.utils.js';
describe('parseTemplateNested()', () => {
@@ -252,12 +250,7 @@ describe('test stringToOSCArgs()', () => {
describe('isAutomationUsed()', () => {
it('returns the first event which uses an automation', () => {
const projectRundowns: ProjectRundowns = {
'rundown-1': {
id: 'rundown-1',
title: 'Rundown 1',
order: ['1'],
flatOrder: ['1'],
const rundown = makeRundown({
entries: {
'1': makeOntimeEvent({
id: '1',
@@ -271,71 +264,17 @@ describe('isAutomationUsed()', () => {
],
}),
},
revision: 1,
},
};
const automationId = 'test-automation';
const result = isAutomationUsed(projectRundowns, automationId);
expect(result).toStrictEqual(['Rundown 1', '1']);
});
it('finds usages in any rundown', () => {
const projectRundowns: ProjectRundowns = {
'rundown-1': {
id: 'rundown-1',
title: 'Rundown 1',
order: ['1'],
flatOrder: ['1'],
entries: {
'1': makeOntimeEvent({
id: '1',
triggers: [
{
id: 'trigger-1',
title: 'Trigger 1',
trigger: TimerLifeCycle.onClock,
automationId: 'test-automation',
},
],
}),
},
revision: 1,
},
'rundown-2': {
id: 'rundown-2',
title: 'Rundown 2',
order: ['1'],
flatOrder: ['1'],
entries: {
'1': makeOntimeEvent({
id: '1',
triggers: [
{
id: 'trigger-1',
title: 'Trigger 1',
trigger: TimerLifeCycle.onClock,
automationId: 'in-the-second-rundown',
},
],
}),
},
revision: 1,
},
};
const automationId = 'in-the-second-rundown';
const timedEventOrder = ['1'];
const automationId = 'test-automation';
const result = isAutomationUsed(projectRundowns, automationId);
expect(result).toStrictEqual(['Rundown 2', '1']);
const result = isAutomationUsed(rundown, timedEventOrder, automationId);
expect(result).toBe('1');
});
it('returns returns undefined if there are no matches', () => {
const projectRundowns: ProjectRundowns = {
'rundown-1': {
id: 'rundown-1',
title: 'Rundown 1',
order: ['1'],
flatOrder: ['1'],
const rundown = makeRundown({
entries: {
'1': makeOntimeEvent({
id: '1',
@@ -349,12 +288,12 @@ describe('isAutomationUsed()', () => {
],
}),
},
revision: 1,
},
};
});
const timedEventOrder = ['1'];
const automationId = 'does-not-exist';
const result = isAutomationUsed(projectRundowns, automationId);
const result = isAutomationUsed(rundown, timedEventOrder, automationId);
expect(result).toBeUndefined();
});
});
@@ -4,7 +4,8 @@ import { Automation, AutomationSettings, ErrorResponse, Trigger } from 'ontime-t
import type { Request, Response } from 'express';
import { oscServer } from '../../adapters/OscAdapter.js';
import { getDataProvider } from '../../classes/data-provider/DataProvider.js';
import { getCurrentRundown, getRundownMetadata } from '../rundown/rundown.dao.js';
import * as automationDao from './automation.dao.js';
import * as automationService from './automation.service.js';
@@ -107,8 +108,10 @@ export async function editAutomation(req: Request, res: Response<Automation | Er
export async function deleteAutomation(req: Request, res: Response<void | ErrorResponse>) {
try {
const projectRundowns = getDataProvider().getProjectRundowns();
await automationDao.deleteAutomation(projectRundowns, req.params.id);
const rundown = getCurrentRundown();
const { timedEventOrder } = getRundownMetadata();
await automationDao.deleteAutomation(rundown, timedEventOrder, req.params.id);
res.status(204).send();
} catch (error) {
const message = getErrorMessage(error);
@@ -2,8 +2,9 @@ import type {
Automation,
AutomationDTO,
AutomationSettings,
EntryId,
NormalisedAutomation,
ProjectRundowns,
Rundown,
Trigger,
TriggerDTO,
} from 'ontime-types';
@@ -135,15 +136,15 @@ export async function editAutomation(id: string, newAutomation: AutomationDTO):
/**
* Deletes a automation given its ID
*/
export async function deleteAutomation(projectRundowns: ProjectRundowns, automationId: string): Promise<void> {
export async function deleteAutomation(rundown: Rundown, timedEventOrder: EntryId[], id: string): Promise<void> {
const automations = getAutomations();
// ignore request if automation does not exist
if (!Object.hasOwn(automations, automationId)) {
if (!Object.hasOwn(automations, id)) {
return;
}
// prevent deleting a automation that is in use in triggers
const triggers = getAutomationTriggers().filter((trigger) => trigger.automationId === automationId);
const triggers = getAutomationTriggers().filter((trigger) => trigger.automationId === id);
if (triggers.length) {
throw new Error(
`Unable to delete automation used in trigger ${triggers[0].title}${triggers.length > 1 ? ` and ${triggers.length - 1} more` : ''}`,
@@ -151,12 +152,12 @@ export async function deleteAutomation(projectRundowns: ProjectRundowns, automat
}
// prevent deleting a automation that is in use in events
const isInUse = isAutomationUsed(projectRundowns, automationId);
const isInUse = isAutomationUsed(rundown, timedEventOrder, id);
if (isInUse) {
throw new Error(`Unable to delete automation used in rundown: ${isInUse[0]}, in event with ID: ${isInUse[1]}`);
throw new Error(`Unable to delete automation used in event with ID ${isInUse}`);
}
delete automations[automationId];
delete automations[id];
await saveChanges({ automations });
}
@@ -1,13 +1,4 @@
import {
EntryId,
FilterRule,
isOntimeEvent,
MaybeNumber,
OntimeAction,
ontimeActionKeyValues,
ProjectRundowns,
RundownEntries,
} from 'ontime-types';
import { EntryId, FilterRule, isOntimeEvent, MaybeNumber, OntimeAction, ontimeActionKeyValues, Rundown } from 'ontime-types';
import { millisToString, removeLeadingZero, splitWhitespace, getPropertyFromPath } from 'ontime-utils';
import type { OscArgOrArrayInput, OscArgInput } from 'osc-min';
@@ -207,40 +198,22 @@ export function isBooleanEquals(a: boolean, b: string): boolean {
/**
* Checks is an automation is used in a rundown
*/
function isAutomationUsedInRundown(
entries: RundownEntries,
flatOrder: EntryId[],
automationId: string,
): EntryId | undefined {
for (let i = 0; i < flatOrder.length; i++) {
const eventId = flatOrder[i];
const entry = entries[eventId];
// only ontime events can contain triggers
if (isOntimeEvent(entry) && entry.triggers) {
for (const trigger of entry.triggers) {
if (trigger.automationId === automationId) {
return entry.id;
}
}
}
}
}
/**
* Checks if an automation is used in any of the project rundowns
* TODO(v4): this currently only checks the current rundown, we will need to check all rundowns in the future
*/
export function isAutomationUsed(
projectRundowns: ProjectRundowns,
rundown: Rundown,
timedEventOrder: EntryId[],
automationId: string,
): [string, EntryId] | undefined {
for (const rundownId in projectRundowns) {
const rundown = projectRundowns[rundownId];
const usedInEvent = isAutomationUsedInRundown(rundown.entries, rundown.flatOrder, automationId);
if (usedInEvent) {
return [rundown.title, usedInEvent];
): EntryId | undefined {
for (let i = 0; i < timedEventOrder.length; i++) {
const eventId = timedEventOrder[i];
const event = rundown.entries[eventId];
if (isOntimeEvent(event) && event.triggers) {
for (const trigger of event.triggers) {
if (trigger.automationId === automationId) {
return eventId;
}
}
}
}
}
@@ -31,6 +31,7 @@ export async function patchPartialProjectFile(req: Request, res: Response<Databa
};
const newData = await projectService.patchCurrentProject(patchDb);
res.status(200).send(newData);
} catch (error) {
const message = getErrorMessage(error);
@@ -1,5 +1,5 @@
import {
ApiActionTag,
ApiAction,
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 ApiActionTag];
const handler = actionHandlers[action as ApiAction];
lastRequest = new Date();
if (handler) {
@@ -46,7 +46,7 @@ export function getLastRequest() {
type ActionHandler = (payload: unknown) => { payload: unknown };
const actionHandlers: Record<ApiActionTag, ActionHandler> = {
const actionHandlers: Record<ApiAction, ActionHandler> = {
/* General */
version: () => ({ payload: ONTIME_VERSION }),
poll: () => ({
@@ -1,8 +1,7 @@
import { dayInMs, MILLIS_PER_HOUR, millisToString } from 'ontime-utils';
import { dayInMs, millisToString } from 'ontime-utils';
import { EndAction, Playback, TimeStrategy, TimerPhase, TimerType } from 'ontime-types';
import {
findDayOffset,
getCurrent,
getExpectedFinish,
getRuntimeOffset,
@@ -727,7 +726,6 @@ describe('getRuntimeOffset()', () => {
eventNow: {
id: '1',
timeStart: 100,
dayOffset: 0,
},
timer: {
startedAt: 150,
@@ -740,10 +738,7 @@ describe('getRuntimeOffset()', () => {
rundown: {
actualStart: 150,
plannedStart: 100,
currentDay: 0,
},
clock: 150,
_startDayOffset: 0,
} as RuntimeState;
const { absolute } = getRuntimeOffset(state);
@@ -755,7 +750,6 @@ describe('getRuntimeOffset()', () => {
eventNow: {
id: '1',
timeStart: 100,
dayOffset: 0,
},
timer: {
startedAt: 150, // we started 50ms delayed
@@ -768,9 +762,7 @@ describe('getRuntimeOffset()', () => {
rundown: {
actualStart: 150,
plannedStart: 100,
currentDay: 0,
},
_startDayOffset: 0,
} as RuntimeState;
const { absolute } = getRuntimeOffset(state);
@@ -783,7 +775,6 @@ describe('getRuntimeOffset()', () => {
id: '1',
timeStart: 100,
timeEnd: 140,
dayOffset: 0,
},
timer: {
startedAt: 100, // we started ontime
@@ -796,9 +787,7 @@ describe('getRuntimeOffset()', () => {
rundown: {
actualStart: 100,
plannedStart: 100,
currentDay: 0,
},
_startDayOffset: 0,
} as RuntimeState;
const { absolute } = getRuntimeOffset(state);
@@ -811,7 +800,6 @@ describe('getRuntimeOffset()', () => {
id: '1',
timeStart: 100,
timeEnd: 150,
dayOffset: 0,
},
clock: 150,
timer: {
@@ -825,9 +813,7 @@ describe('getRuntimeOffset()', () => {
rundown: {
actualStart: 100,
plannedStart: 100,
currentDay: 0,
},
_startDayOffset: 0,
} as RuntimeState;
const { absolute } = getRuntimeOffset(state);
@@ -844,7 +830,6 @@ describe('getRuntimeOffset()', () => {
duration: 3600000,
timeStrategy: 'lock-duration',
linkStart: false,
dayOffset: 0,
},
rundown: {
selectedEventIndex: 0,
@@ -852,7 +837,6 @@ describe('getRuntimeOffset()', () => {
plannedStart: 77400000,
plannedEnd: 84600000,
actualStart: null,
currentDay: 0,
},
offset: {
absolute: -77400000,
@@ -868,7 +852,6 @@ describe('getRuntimeOffset()', () => {
secondaryTimer: null,
startedAt: null,
},
_startDayOffset: 0,
_timer: { pausedAt: null },
} as RuntimeState;
@@ -891,7 +874,6 @@ describe('getRuntimeOffset()', () => {
endAction: EndAction.None,
timerType: TimerType.CountDown,
countToEnd: true,
dayOffset: 0,
skip: false,
note: '',
colour: '',
@@ -908,7 +890,6 @@ describe('getRuntimeOffset()', () => {
plannedStart: 77400000, // 21:30:00
plannedEnd: 81000000, // 22:30:00
actualStart: 78000000, // 21:40:00
currentDay: 0,
},
offset: {
absolute: 0,
@@ -924,7 +905,6 @@ describe('getRuntimeOffset()', () => {
secondaryTimer: null,
startedAt: 78000000,
},
_startDayOffset: 0,
_timer: { pausedAt: null },
} as RuntimeState;
@@ -942,7 +922,6 @@ 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,
@@ -964,7 +943,6 @@ describe('getRuntimeOffset()', () => {
plannedStart: 77400000, // 21:30:00
plannedEnd: 81000000, // 22:30:00
actualStart: 78000000, // 21:40:00
currentDay: 0,
},
offset: {
absolute: 0,
@@ -980,7 +958,6 @@ describe('getRuntimeOffset()', () => {
secondaryTimer: null,
startedAt: 78000000,
},
_startDayOffset: 0,
_timer: { pausedAt: null },
} as RuntimeState;
@@ -1001,7 +978,6 @@ describe('getRuntimeOffset()', () => {
endAction: EndAction.None,
timerType: TimerType.CountDown,
countToEnd: true,
dayOffset: 0,
},
rundown: {
selectedEventIndex: 0,
@@ -1009,7 +985,6 @@ describe('getRuntimeOffset()', () => {
plannedStart: 77400000, // 21:30:00
plannedEnd: 81000000, // 22:30:00
actualStart: 82000000, // 22:46:40 <--- started now
currentDay: 0,
},
offset: {
absolute: 0,
@@ -1025,7 +1000,6 @@ describe('getRuntimeOffset()', () => {
secondaryTimer: null,
startedAt: 82000000, // <--- started now
},
_startDayOffset: 0,
_timer: { pausedAt: null },
} as RuntimeState;
@@ -1043,7 +1017,6 @@ describe('getRuntimeOffset() relative', () => {
eventNow: {
id: '1',
timeStart: 150,
dayOffset: 0,
},
timer: {
startedAt: 150,
@@ -1056,9 +1029,7 @@ describe('getRuntimeOffset() relative', () => {
rundown: {
actualStart: 150,
plannedStart: 150,
currentDay: 0,
},
_startDayOffset: 0,
} as RuntimeState;
const { absolute, relative } = getRuntimeOffset(state);
@@ -1070,7 +1041,6 @@ describe('getRuntimeOffset() relative', () => {
eventNow: {
id: '1',
timeStart: 100,
dayOffset: 0,
},
timer: {
startedAt: 150,
@@ -1083,9 +1053,7 @@ describe('getRuntimeOffset() relative', () => {
rundown: {
actualStart: 150,
plannedStart: 100,
currentDay: 0,
},
_startDayOffset: 0,
} as RuntimeState;
const { absolute, relative } = getRuntimeOffset(state);
@@ -1097,7 +1065,6 @@ describe('getRuntimeOffset() relative', () => {
eventNow: {
id: '1',
timeStart: 150,
dayOffset: 0,
},
timer: {
startedAt: 100,
@@ -1110,9 +1077,7 @@ describe('getRuntimeOffset() relative', () => {
rundown: {
actualStart: 100,
plannedStart: 150,
currentDay: 0,
},
_startDayOffset: 0,
} as RuntimeState;
const { absolute, relative } = getRuntimeOffset(state);
@@ -1293,21 +1258,3 @@ 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
});
});
@@ -323,14 +323,11 @@ export async function patchCurrentProject(data: Partial<DatabaseModel>) {
/**
* The user may have multiple rundowns
* so attempt to get the one that was used last
* otherwise just pick the first
* We currently ignore all other rundowns
*/
const last = await getLastLoaded();
const rundownToLoad =
last?.rundownId && last.rundownId in result ? result[last.rundownId] : getFirstRundown(result);
const firstRundown = getFirstRundown(result);
await initRundown(rundownToLoad, customFields, true);
await initRundown(firstRundown, customFields);
}
const updatedData = await getDataProvider().getData();
@@ -12,7 +12,6 @@ describe('isRestorePoint()', () => {
addedTime: 2,
pausedAt: 3,
firstStart: 1,
startEpoch: 1,
};
expect(isRestorePoint(restorePoint)).toBe(true);
@@ -23,7 +22,6 @@ describe('isRestorePoint()', () => {
addedTime: 0,
pausedAt: null,
firstStart: 1,
startEpoch: 1,
};
expect(isRestorePoint(restorePoint)).toBe(true);
});
@@ -37,7 +35,6 @@ describe('isRestorePoint()', () => {
addedTime: 0,
pausedAt: null,
groupStartAt: 10,
startEpoch: 1,
};
expect(isRestorePoint(restorePoint)).toBe(false);
});
@@ -59,7 +56,6 @@ describe('isRestorePoint()', () => {
addedTime: 0,
pausedAt: null,
groupStartAt: 10,
startEpoch: 1,
};
expect(isRestorePoint(restorePoint)).toBe(false);
});
@@ -16,7 +16,6 @@ describe('restoreService', () => {
addedTime: 5678,
pausedAt: 9087,
firstStart: 1234,
startEpoch: 1234,
};
const mockRead = vi.fn().mockResolvedValue(expected);
@@ -34,7 +33,6 @@ describe('restoreService', () => {
addedTime: 0,
pausedAt: null,
firstStart: 1234,
startEpoch: 1234,
};
const mockRead = vi.fn().mockResolvedValue(expected);
@@ -81,7 +79,6 @@ describe('restoreService', () => {
addedTime: 1234,
pausedAt: 1234,
firstStart: 1234,
startEpoch: 1234,
};
const mockWrite = vi.fn().mockResolvedValue(undefined);
@@ -98,7 +95,6 @@ describe('restoreService', () => {
addedTime: 5678,
pausedAt: 5678,
firstStart: 5678,
startEpoch: 5678,
};
const mockWrite = vi.fn().mockRejectedValue(new Error('Write failed'));
@@ -20,7 +20,6 @@ export function isRestorePoint(restorePoint: unknown): restorePoint is RestorePo
'addedTime',
'pausedAt',
'firstStart',
'startEpoch',
])
) {
return false;
@@ -50,9 +49,5 @@ export function isRestorePoint(restorePoint: unknown): restorePoint is RestorePo
return false;
}
if (!is.number(restorePoint.startEpoch) && restorePoint.startEpoch !== null) {
return false;
}
return true;
}
@@ -7,5 +7,4 @@ export type RestorePoint = {
addedTime: number;
pausedAt: MaybeNumber;
firstStart: MaybeNumber;
startEpoch: MaybeNumber;
};
@@ -743,7 +743,6 @@ 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,7 +412,6 @@ export async function upload(sheetId: string, options: ImportMap) {
},
});
try {
// update the corresponding row with event data
sheetOrder.forEach((entryId, index) => {
const isGroupEnd = entryId.startsWith('group-end-');
@@ -422,9 +421,6 @@ export async function upload(sheetId: string, options: ImportMap) {
: structuredClone(rundown.entries[id]);
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,6 +154,12 @@ 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))
@@ -162,17 +168,6 @@ 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 {};
+8 -21
View File
@@ -1,6 +1,5 @@
import { MaybeNumber, TimerPhase } from 'ontime-types';
import { dayInMs, isPlaybackActive, MILLIS_PER_HOUR } from 'ontime-utils';
import { dayInMs, isPlaybackActive } from 'ontime-utils';
import type { RuntimeState } from '../stores/runtimeState.js';
/**
@@ -114,14 +113,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, _startDayOffset } = state;
const { eventNow, clock } = 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 || _startDayOffset === null) {
if (eventNow === null || startedAt === null) {
return { absolute: 0, relative: 0 };
}
const { countToEnd, timeStart, dayOffset } = eventNow;
const { countToEnd, timeStart } = eventNow;
const { plannedStart, actualStart } = state.rundown;
// eslint-disable-next-line no-unused-labels -- dev code path
@@ -132,8 +131,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 started behind)
const eventStartOffset = startedAt + _startDayOffset * dayInMs - (timeStart + dayOffset * dayInMs);
// difference between planned event start and actual event start (will be positive if we stared behind )
const eventStartOffset = startedAt - timeStart;
// 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));
@@ -141,11 +140,10 @@ 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 is the same as the absolute but adjusted relative to the actual start time
const relative = absolute + plannedStart - actualStart - _startDayOffset * dayInMs;
// the relative offset i the same as the absolute offset but adjusted relative to the actual start time
const relative = absolute + plannedStart - actualStart;
// in case of count to end, the absolute offset is just the overtime
return countToEnd ? { absolute: overtime, relative } : { absolute, relative };
@@ -182,14 +180,3 @@ 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,8 +14,6 @@ const baseState: RuntimeState = {
plannedStart: 0,
plannedEnd: 0,
actualStart: null,
actualGroupStart: null,
currentDay: 0,
},
offset: {
absolute: 0,
@@ -48,8 +46,6 @@ const baseState: RuntimeState = {
_group: null,
_end: null,
_flag: null,
_startDayOffset: null,
_startEpoch: null,
};
export function makeRuntimeStateData(patch?: Partial<RuntimeState>): RuntimeState {
@@ -393,7 +393,6 @@ 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;
@@ -421,7 +420,6 @@ 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;
@@ -449,7 +447,6 @@ 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;
@@ -474,7 +471,6 @@ describe('loadGroupFlagAndEnd()', () => {
const state = {
groupNow: null,
eventNow: rundown.entries[0],
rundown: { actualGroupStart: null },
} as RuntimeState;
const metadata = { playableEventOrder: ['0', '1'], flags: ['1'] } as RundownMetadata;
+23 -93
View File
@@ -23,15 +23,9 @@ import {
isPlaybackActive,
} from 'ontime-utils';
import { getTimeObject, timeNow } from '../utils/time.js';
import { timeNow } from '../utils/time.js';
import type { RestorePoint } from '../services/restore-service/restore.type.js';
import {
findDayOffset,
getCurrent,
getExpectedFinish,
getRuntimeOffset,
getTimerPhase,
} from '../services/timerUtils.js';
import { 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';
@@ -61,8 +55,6 @@ export type RuntimeState = {
_group: ExpectedMetadata;
_flag: ExpectedMetadata;
_end: ExpectedMetadata;
_startEpoch: MaybeNumber;
_startDayOffset: MaybeNumber;
};
const runtimeState: RuntimeState = {
@@ -86,8 +78,6 @@ const runtimeState: RuntimeState = {
_group: null,
_flag: null,
_end: null,
_startEpoch: null,
_startDayOffset: null,
};
export function getState(): Readonly<RuntimeState> {
@@ -162,10 +152,6 @@ 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;
}
/**
@@ -204,8 +190,7 @@ export function updateRundownData(rundownData: {
runtimeState.rundown.plannedStart = rundownData.firstStart;
runtimeState.rundown.plannedEnd =
rundownData.firstStart === null ? null : rundownData.firstStart + rundownData.totalDuration;
if (isPlaybackActive(runtimeState.timer.playback)) getExpectedTimes();
getExpectedTimes();
}
/**
@@ -244,14 +229,9 @@ 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') &&
(startEpoch === null || typeof startEpoch === 'number')
) {
if (firstStart === null || typeof firstStart === 'number') {
runtimeState.rundown.actualStart = firstStart;
runtimeState._startEpoch = startEpoch;
const { absolute, relative } = getRuntimeOffset(runtimeState);
runtimeState.offset.absolute = absolute;
runtimeState.offset.relative = relative;
@@ -387,8 +367,7 @@ export function start(state: RuntimeState = runtimeState): boolean {
return false;
}
const [epoch, now] = getTimeObject();
state.clock = now;
state.clock = timeNow();
state.timer.secondaryTimer = null;
// add paused time if it exists
@@ -407,16 +386,9 @@ 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);
@@ -508,20 +480,13 @@ export type UpdateResult = {
export function update(): UpdateResult {
// 0. there are some things we always do
const previousClock = runtimeState.clock;
const [epoch, now] = getTimeObject();
runtimeState.clock = now; // we update the clock on every update call
runtimeState.clock = timeNow(); // 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;
@@ -603,10 +568,6 @@ 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
@@ -637,24 +598,12 @@ 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) {
/**
* 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;
}
runtimeState.timer.startedAt = runtimeState.clock;
if (runtimeState.rundown.actualStart === null) {
runtimeState.rundown.actualStart = plannedStart;
runtimeState._startDayOffset = 0;
runtimeState._startEpoch = epoch;
runtimeState.rundown.actualStart = runtimeState.clock;
}
runtimeState.timer.secondaryTimer = null;
} else {
runtimeState._timer.secondaryTarget = normaliseRollStart(runtimeState.eventNow.timeStart, offsetClock);
runtimeState.timer.secondaryTimer = runtimeState._timer.secondaryTarget - offsetClock;
@@ -709,37 +658,26 @@ export function roll(
return { eventId: runtimeState.eventNow.id, didStart: false };
}
/**
* 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;
// there is something to run, load event
// event will finish on time
// account for event that finishes the day after
const endTime =
runtimeState.eventNow.timeEnd < runtimeState.eventNow.timeStart
? runtimeState.eventNow.timeEnd + dayInMs
: runtimeState.eventNow.timeEnd;
runtimeState.timer.startedAt = plannedStart;
runtimeState.timer.startedAt = runtimeState.clock;
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 = plannedStart;
if (runtimeState.groupNow !== null && runtimeState.rundown.actualGroupStart === null) {
runtimeState.rundown.actualGroupStart = plannedStart;
}
// update metadata
runtimeState._startDayOffset = 0;
runtimeState._startEpoch = epoch;
runtimeState.rundown.actualStart = runtimeState.clock;
return { eventId: runtimeState.eventNow.id, didStart: true };
}
@@ -766,7 +704,7 @@ function getExpectedTimes(state = runtimeState) {
if (_group !== null) {
const { event: lastEvent, accumulatedGap, isLinkedToLoaded } = _group;
const lastEventExpectedStart = getExpectedStart(lastEvent, {
currentDay: state.rundown.currentDay!,
currentDay: eventNow.dayOffset,
totalGap: accumulatedGap,
isLinkedToLoaded,
mode: offset.mode,
@@ -783,7 +721,7 @@ function getExpectedTimes(state = runtimeState) {
if (_flag) {
const { event, accumulatedGap, isLinkedToLoaded } = _flag;
const expectedStart = getExpectedStart(event, {
currentDay: state.rundown.currentDay!,
currentDay: eventNow.dayOffset,
totalGap: accumulatedGap,
isLinkedToLoaded,
mode: offset.mode,
@@ -798,7 +736,7 @@ function getExpectedTimes(state = runtimeState) {
if (state._end) {
const { event, accumulatedGap, isLinkedToLoaded } = state._end;
const expectedStart = getExpectedStart(event, {
currentDay: state.rundown.currentDay!,
currentDay: eventNow.dayOffset,
totalGap: accumulatedGap,
isLinkedToLoaded,
mode: offset.mode,
@@ -814,19 +752,16 @@ export function loadGroupFlagAndEnd(
rundown: Rundown,
metadata: RundownMetadata,
currentIndex: MaybeNumber,
state = runtimeState, // used for testing
state = runtimeState,
) {
const previousGroup = state.groupNow?.id;
state.groupNow = null;
state._group = null;
state.eventFlag = null;
state._flag = null;
state._end = null;
if (currentIndex === null || state.eventNow === null) {
state.rundown.actualGroupStart = null;
return;
}
if (currentIndex == null) return;
if (state.eventNow === null) return;
const currentGroupId = state.eventNow.parent;
const flagsPresent = metadata.flags.length !== 0;
@@ -838,10 +773,6 @@ 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
@@ -883,5 +814,4 @@ export function loadGroupFlagAndEnd(
export function setOffsetMode(mode: OffsetMode) {
runtimeState.offset.mode = mode;
if (isPlaybackActive(runtimeState.timer.playback)) getExpectedTimes();
}
@@ -145,25 +145,16 @@ describe('coerce unknown value to a boolean', () => {
expect(coerceBoolean('')).toStrictEqual(false);
});
test('invalid strings', () => {
expect(() => coerceBoolean('bla')).toThrowError('Invalid value received');
expect(() => coerceBoolean(' ')).toThrowError('Invalid value received');
});
test('true numbers', () => {
expect(coerceBoolean(1)).toStrictEqual(true);
expect(coerceBoolean(2)).toStrictEqual(true);
expect(coerceBoolean(100000)).toStrictEqual(true);
});
test('false numbers', () => {
test.todo('false numbers', () => {
expect(coerceBoolean(0)).toStrictEqual(false);
});
test('invalid numbers', () => {
expect(() => coerceBoolean(0.5)).toThrowError('Invalid value received');
expect(() => coerceBoolean(-1)).toThrowError('Invalid value received');
expect(() => coerceBoolean(2)).toThrowError('Invalid value received');
expect(() => coerceBoolean(NaN)).toThrowError('Invalid value received');
expect(() => coerceBoolean(Infinity)).toThrowError('Invalid value received');
expect(coerceBoolean(-1)).toStrictEqual(false);
expect(coerceBoolean(-10000)).toStrictEqual(false);
});
test('booleans', () => {
+3 -7
View File
@@ -33,8 +33,8 @@ export function coerceString(value: unknown): string {
* @throws {Error} Throws an error if the value is null or undefined.
*/
export function coerceBoolean(value: unknown): boolean {
if (typeof value === 'boolean') {
return value;
if (value === undefined || typeof value === 'object') {
throw new Error('Invalid value received');
}
if (typeof value === 'string') {
const lowerCaseValue = value.toLocaleLowerCase();
@@ -52,11 +52,7 @@ export function coerceBoolean(value: unknown): boolean {
throw new Error('Invalid value received');
}
}
if (typeof value === 'number') {
if (value === 0) return false;
if (value === 1) return true;
}
throw new Error('Invalid value received');
return Boolean(value);
}
/**
-15
View File
@@ -74,18 +74,3 @@ 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];
}
+6 -5
View File
@@ -1,12 +1,13 @@
{
"extends": "../../tsconfig.common.json",
"compilerOptions": {
"target": "esnext",
"module": "node16",
"moduleResolution": "node16",
"noImplicitReturns": false, //TODO: fix this
"strict": true,
"target": "ESNext",
"module": "Node16",
"moduleResolution": "Node16",
"allowSyntheticDefaultImports": true,
"allowJs": true,
"esModuleInterop": true,
"skipLibCheck": true,
"types": ["vitest/globals"],
"outDir": "dist",
"experimentalDecorators": true,
@@ -72,6 +72,4 @@ 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();
});
+3 -4
View File
@@ -1,6 +1,6 @@
{
"name": "ontime",
"version": "4.0.0-beta.4",
"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": "pnpm rimraf node_modules && rimraf **/node_modules && rimraf **/**/node_modules",
"cleanup": "rm -rf node_modules && rm -rf **/node_modules && rm -rf **/**/node_modules",
"clear-temp": "rm -rf e2e/tests/fixtures/tmp"
},
"devDependencies": {
@@ -47,8 +47,7 @@
"eslint-plugin-playwright": "^1.5.2",
"prettier": "catalog:",
"turbo": "^2.3.3",
"typescript": "catalog:",
"rimraf": "catalog:"
"typescript": "catalog:"
},
"packageManager": "pnpm@10.11.0+sha512.6540583f41cc5f628eb3d9773ecee802f4f9ef9923cc45b69890fb47991d4b092964694ec3a4f738a420c918a333062c8b925d312f42e4f0c263eb603551f977"
}
+2 -5
View File
@@ -1,5 +1,4 @@
{
"version": "4.0.0-beta.4",
"name": "ontime-types",
"type": "module",
"main": "./src/index.ts",
@@ -8,8 +7,7 @@
"description": "shared typings for ontime",
"scripts": {
"cleanup": "rm -rf .turbo && rm -rf node_modules",
"lint": "eslint . --quiet",
"build": "tsc"
"lint": "eslint . --quiet"
},
"keywords": [],
"author": "",
@@ -18,7 +16,6 @@
"@typescript-eslint/eslint-plugin": "catalog:",
"@typescript-eslint/parser": "catalog:",
"eslint": "catalog:",
"typescript": "catalog:",
"ts-essentials": "catalog:"
"typescript": "catalog:"
}
}
+14 -176
View File
@@ -1,177 +1,15 @@
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 | { duration?: number; addtime?: number; direction?: SimpleDirection };
}
| {
['2']?: SimplePlayback | { duration?: number; addtime?: number; direction?: SimpleDirection };
}
| {
['3']?: SimplePlayback | { 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 =
| 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'];
| 'version'
| 'poll'
| 'change'
| 'message'
| 'start'
| 'pause'
| 'stop'
| 'reload'
| 'roll'
| 'load'
| 'addtime'
| 'auxtimer'
| 'client'
| 'offsetmode';
@@ -1,11 +1,12 @@
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';
import type {
AutomationSettings,
CustomFields,
ProjectData,
ProjectRundowns,
Settings,
URLPreset,
ViewSettings,
} from '../index.js';
export type DatabaseModel = {
rundowns: ProjectRundowns;
@@ -1,9 +1,4 @@
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';
import type { EndAction, EntryCustomFields, MaybeNumber, TimerType, TimeStrategy, Trigger } from '../../index.js';
export type EntryId = string;
@@ -4,8 +4,6 @@ export type RundownState = {
selectedEventIndex: MaybeNumber;
numEvents: number;
plannedStart: MaybeNumber;
plannedEnd: MaybeNumber;
actualStart: MaybeNumber;
currentDay: MaybeNumber;
actualGroupStart: MaybeNumber;
plannedEnd: MaybeNumber;
};
@@ -33,8 +33,6 @@ 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 '../../utils/utils.type.js';
import type { MaybeNumber } from '../../index.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, ApiActionTag, ApiResponse } from './api/websocket/api.type.js';
export type { ApiAction } 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';
-20
View File
@@ -1,20 +0,0 @@
{
"extends": "../../tsconfig.common.json",
"compilerOptions": {
"target": "esnext",
"module": "esnext",
"moduleResolution": "node10",
"allowUnusedLabels": false,
"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, MILLIS_PER_HOUR } from './conversionUtils';
import { dayInMs } from './conversionUtils';
import { getExpectedStart } from './getExpectedStart';
describe('getExpectedStart()', () => {
@@ -277,41 +277,4 @@ 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; // the current day from the rundown
currentDay: number;
totalGap: number;
isLinkedToLoaded: boolean;
offset: number;
@@ -38,7 +38,7 @@ export function getExpectedStart(
let relativeStartOffset = 0;
if (mode === OffsetMode.Relative) {
relativeStartOffset = (actualStart ?? 0) + currentDay * dayInMs - (plannedStart ?? 0);
relativeStartOffset = (actualStart ?? 0) - (plannedStart ?? 0);
}
const scheduledStartTime = normalisedTimeStart + relativeStartOffset;
+15 -2
View File
@@ -1,10 +1,23 @@
{
"extends": "../../tsconfig.common.json",
"compilerOptions": {
"target": "esnext",
"target": "ESNext",
"module": "esnext",
"moduleResolution": "node",
"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,
+16 -350
View File
@@ -27,12 +27,6 @@ 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
@@ -71,9 +65,6 @@ importers:
prettier:
specifier: 'catalog:'
version: 3.3.1
rimraf:
specifier: 'catalog:'
version: 6.0.1
turbo:
specifier: ^2.3.3
version: 2.5.6
@@ -280,30 +271,6 @@ 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':
@@ -351,6 +318,9 @@ importers:
sanitize-filename:
specifier: ^1.6.3
version: 1.6.3
steno:
specifier: ^4.0.2
version: 4.0.2
ws:
specifier: ^8.18.0
version: 8.18.3
@@ -407,7 +377,7 @@ importers:
specifier: ^0.3.4
version: 0.3.4
ts-essentials:
specifier: 'catalog:'
specifier: ^10.0.3
version: 10.1.1(typescript@5.5.3)
tsx:
specifier: ^4.19.2
@@ -430,9 +400,6 @@ 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
@@ -1608,10 +1575,6 @@ 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'}
@@ -2067,9 +2030,6 @@ 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'}
@@ -2273,12 +2233,6 @@ 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'}
@@ -2423,10 +2377,6 @@ 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'}
@@ -2446,16 +2396,9 @@ 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'}
@@ -3042,9 +2985,6 @@ 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'}
@@ -3186,11 +3126,6 @@ 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
@@ -3571,10 +3506,6 @@ 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'}
@@ -3587,10 +3518,6 @@ 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==}
@@ -3670,17 +3597,9 @@ 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'}
@@ -3700,9 +3619,6 @@ 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==}
@@ -3734,10 +3650,6 @@ 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==}
@@ -3896,9 +3808,6 @@ 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==}
@@ -3906,9 +3815,6 @@ 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}
@@ -4093,10 +3999,6 @@ 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'}
@@ -4119,6 +4021,9 @@ 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==}
@@ -4130,13 +4035,6 @@ 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'}
@@ -4155,24 +4053,6 @@ 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}
@@ -4388,10 +4268,6 @@ 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==}
@@ -4424,11 +4300,6 @@ 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'}
@@ -4627,11 +4498,6 @@ 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==}
@@ -4714,11 +4580,6 @@ 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'}
@@ -4765,13 +4626,6 @@ 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==}
@@ -4823,17 +4677,10 @@ 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==}
@@ -4857,9 +4704,6 @@ 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}
@@ -4873,25 +4717,6 @@ 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'}
@@ -4980,9 +4805,6 @@ 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'}
@@ -5172,9 +4994,6 @@ 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'}
@@ -5201,9 +5020,6 @@ 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'}
@@ -5361,7 +5177,7 @@ snapshots:
'@babel/code-frame@7.24.2':
dependencies:
'@babel/highlight': 7.24.5
picocolors: 1.1.1
picocolors: 1.0.1
'@babel/code-frame@7.27.1':
dependencies:
@@ -5436,8 +5252,8 @@ snapshots:
'@babel/generator@7.23.6':
dependencies:
'@babel/types': 7.28.2
'@jridgewell/gen-mapping': 0.3.13
'@jridgewell/trace-mapping': 0.3.30
'@jridgewell/gen-mapping': 0.3.3
'@jridgewell/trace-mapping': 0.3.20
jsesc: 2.5.2
'@babel/generator@7.27.5':
@@ -5610,7 +5426,7 @@ snapshots:
'@babel/helper-validator-identifier': 7.27.1
chalk: 2.4.2
js-tokens: 4.0.0
picocolors: 1.1.1
picocolors: 1.0.1
'@babel/parser@7.23.6':
dependencies:
@@ -6486,8 +6302,6 @@ 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
@@ -6966,7 +6780,8 @@ snapshots:
acorn@8.14.0: {}
acorn@8.15.0: {}
acorn@8.15.0:
optional: true
adler-32@1.3.1: {}
@@ -7016,8 +6831,6 @@ snapshots:
ansi-styles@6.2.1: {}
any-promise@1.3.0: {}
anymatch@3.1.3:
dependencies:
normalize-path: 3.0.0
@@ -7381,11 +7194,6 @@ 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
@@ -7553,8 +7361,6 @@ snapshots:
dependencies:
delayed-stream: 1.0.0
commander@4.1.1: {}
commander@5.1.0: {}
compare-version@0.1.2: {}
@@ -7575,15 +7381,11 @@ 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
@@ -8389,12 +8191,6 @@ 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
@@ -8566,15 +8362,6 @@ 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
@@ -8993,10 +8780,6 @@ 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
@@ -9014,8 +8797,6 @@ 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:
@@ -9118,12 +8899,8 @@ 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
@@ -9138,8 +8915,6 @@ snapshots:
lodash.merge@4.6.2: {}
lodash.sortby@4.7.0: {}
lodash.union@4.6.0: {}
lodash@4.17.21: {}
@@ -9167,8 +8942,6 @@ snapshots:
lru-cache@10.4.3: {}
lru-cache@11.2.1: {}
lru-cache@5.1.1:
dependencies:
yallist: 3.1.1
@@ -9320,13 +9093,6 @@ 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:
@@ -9339,12 +9105,6 @@ 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: {}
@@ -9528,11 +9288,6 @@ 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: {}
@@ -9545,20 +9300,14 @@ 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:
@@ -9575,14 +9324,6 @@ 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
@@ -9793,8 +9534,6 @@ snapshots:
resolve-from@4.0.0: {}
resolve-from@5.0.0: {}
resolve-pkg-maps@1.0.0: {}
resolve@1.22.10:
@@ -9826,11 +9565,6 @@ 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
@@ -10098,10 +9832,6 @@ 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
@@ -10204,16 +9934,6 @@ 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
@@ -10268,14 +9988,6 @@ 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
@@ -10321,17 +10033,11 @@ 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
@@ -10348,42 +10054,12 @@ 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
@@ -10477,8 +10153,6 @@ snapshots:
typescript@5.9.2: {}
ufo@1.6.1: {}
unbox-primitive@1.1.0:
dependencies:
call-bound: 1.0.4
@@ -10516,7 +10190,7 @@ snapshots:
dependencies:
browserslist: 4.22.2
escalade: 3.1.1
picocolors: 1.1.1
picocolors: 1.0.1
update-browserslist-db@1.1.3(browserslist@4.25.0):
dependencies:
@@ -10689,8 +10363,6 @@ snapshots:
webidl-conversions@3.0.1: {}
webidl-conversions@4.0.2: {}
webidl-conversions@7.0.0:
optional: true
@@ -10717,12 +10389,6 @@ 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,8 +11,6 @@ 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
-16
View File
@@ -1,16 +0,0 @@
{
"$schema": "https://json.schemastore.org/tsconfig",
"compilerOptions": {
"strict": true,
"skipLibCheck": true,
"allowSyntheticDefaultImports": true,
"resolveJsonModule": true,
"forceConsistentCasingInFileNames": true,
"isolatedModules": true,
"noImplicitReturns": true,
"noFallthroughCasesInSwitch": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"allowUnreachableCode": false
}
}
-3
View File
@@ -26,9 +26,6 @@
"cache": false
},
"build": {
"dependsOn": [
"^build"
],
"env": ["SENTRY_AUTH_TOKEN"]
},
"build:local": {},