mirror of
https://github.com/cpvalente/ontime.git
synced 2026-08-09 09:23:51 +00:00
Compare commits
12 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 99a2f58502 | |||
| 535717fa90 | |||
| 477777552c | |||
| df2d083a16 | |||
| 079a2ea96d | |||
| c50c84f50b | |||
| 58dce0a18c | |||
| b2f9b812bd | |||
| 35d2474797 | |||
| b9f5a0a82b | |||
| 26a58a9241 | |||
| 588b04c46b |
@@ -1,15 +1,15 @@
|
|||||||
import { PropsWithChildren, createContext, useContext } from 'react';
|
import { PropsWithChildren, createContext, useContext } from 'react';
|
||||||
|
|
||||||
import { useEntryActions } from '../hooks/useEntryAction';
|
import { useEntryActions, useScopedEntryActions } from '../hooks/useEntryAction';
|
||||||
|
import { useRundownSelectionContext } from './RundownSelectionContext';
|
||||||
|
|
||||||
type EntryActionsContextValue = ReturnType<typeof useEntryActions>;
|
type EntryActionsContextValue = ReturnType<typeof useEntryActions>;
|
||||||
const EntryActionsContext = createContext<EntryActionsContextValue | null>(null);
|
const EntryActionsContext = createContext<EntryActionsContextValue | null>(null);
|
||||||
|
|
||||||
interface EntryActionsProviderProps extends PropsWithChildren {
|
export function EntryActionsProvider({ children }: PropsWithChildren) {
|
||||||
actions: EntryActionsContextValue;
|
const { effectiveRundownId } = useRundownSelectionContext();
|
||||||
}
|
const actions = useScopedEntryActions(effectiveRundownId);
|
||||||
|
|
||||||
export function EntryActionsProvider({ children, actions }: EntryActionsProviderProps) {
|
|
||||||
return <EntryActionsContext.Provider value={actions}>{children}</EntryActionsContext.Provider>;
|
return <EntryActionsContext.Provider value={actions}>{children}</EntryActionsContext.Provider>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,120 @@
|
|||||||
|
import { Maybe, ProjectRundown } from 'ontime-types';
|
||||||
|
import { PropsWithChildren, createContext, startTransition, useCallback, useContext, useEffect, useMemo } from 'react';
|
||||||
|
import { useSearchParams } from 'react-router';
|
||||||
|
import { useNavigate } from 'react-router';
|
||||||
|
|
||||||
|
import { useProjectRundowns } from '../hooks-query/useProjectRundowns';
|
||||||
|
|
||||||
|
export type RundownScopeValue = {
|
||||||
|
loadedRundownId: string;
|
||||||
|
selectedRundownId: Maybe<string>;
|
||||||
|
isLoadedRundown: boolean;
|
||||||
|
effectiveRundownId: string;
|
||||||
|
selectRundownId: (val: Maybe<string>) => void;
|
||||||
|
rundowns: ProjectRundown[];
|
||||||
|
};
|
||||||
|
|
||||||
|
const RundownScopeContext = createContext<RundownScopeValue | null>(null);
|
||||||
|
|
||||||
|
export function RundownSelectionContextProvider({ children }: PropsWithChildren) {
|
||||||
|
'use memo';
|
||||||
|
const { data } = useProjectRundowns();
|
||||||
|
const { loaded, rundowns } = data;
|
||||||
|
|
||||||
|
const [selectedRundownId, setSelectedRundownId] = useSelectRundownFromParams();
|
||||||
|
|
||||||
|
const selectRundownId = useCallback(
|
||||||
|
(rundownId: Maybe<string>) => {
|
||||||
|
startTransition(() => {
|
||||||
|
if (rundowns.find((entry) => entry.id === rundownId)) setSelectedRundownId(rundownId);
|
||||||
|
else setSelectedRundownId(null);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
[rundowns, setSelectedRundownId],
|
||||||
|
);
|
||||||
|
|
||||||
|
const effectiveRundownId = selectedRundownId ? selectedRundownId : loaded;
|
||||||
|
const isLoadedRundown = effectiveRundownId === loaded;
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!rundowns.find((entry) => entry.id === effectiveRundownId)) setSelectedRundownId(null);
|
||||||
|
}, [rundowns, effectiveRundownId, setSelectedRundownId]);
|
||||||
|
|
||||||
|
const value = useMemo(
|
||||||
|
(): RundownScopeValue => ({
|
||||||
|
loadedRundownId: loaded,
|
||||||
|
isLoadedRundown,
|
||||||
|
selectedRundownId,
|
||||||
|
effectiveRundownId,
|
||||||
|
selectRundownId,
|
||||||
|
rundowns,
|
||||||
|
}),
|
||||||
|
[loaded, isLoadedRundown, selectedRundownId, effectiveRundownId, selectRundownId, rundowns],
|
||||||
|
);
|
||||||
|
|
||||||
|
return <RundownScopeContext.Provider value={value}>{children}</RundownScopeContext.Provider>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useRundownSelectionContext() {
|
||||||
|
const context = useContext(RundownScopeContext);
|
||||||
|
|
||||||
|
if (!context) {
|
||||||
|
throw new Error('useRundownScopeSelection requires a RundownSelectionContextProvider');
|
||||||
|
}
|
||||||
|
|
||||||
|
return context;
|
||||||
|
}
|
||||||
|
|
||||||
|
const rundownParam = 'rundownId';
|
||||||
|
|
||||||
|
export function useSelectRundownFromParams(): [Maybe<string>, (id: Maybe<string>) => void] {
|
||||||
|
'use memo';
|
||||||
|
const [searchParams, setSearchParams] = useSearchParams();
|
||||||
|
|
||||||
|
const selectedRundownId = searchParams.get(rundownParam);
|
||||||
|
const setSelectedRundownId = useCallback(
|
||||||
|
(id: Maybe<string>) => {
|
||||||
|
if (id === null) {
|
||||||
|
setSearchParams((searchParams) => {
|
||||||
|
searchParams.delete(rundownParam);
|
||||||
|
return searchParams;
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
setSearchParams((searchParams) => {
|
||||||
|
searchParams.set(rundownParam, id);
|
||||||
|
return searchParams;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[setSearchParams],
|
||||||
|
);
|
||||||
|
|
||||||
|
return [selectedRundownId, setSelectedRundownId];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* mutates the provided `searchParams`
|
||||||
|
*/
|
||||||
|
export function setSelectRundownInParams(id: Maybe<string>, searchParams: URLSearchParams): void {
|
||||||
|
if (id === null) {
|
||||||
|
searchParams.delete(rundownParam);
|
||||||
|
} else {
|
||||||
|
searchParams.set(rundownParam, id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useDirectLinkToBackgroundEdit() {
|
||||||
|
const navigate = useNavigate();
|
||||||
|
const [search] = useSearchParams();
|
||||||
|
return useCallback(
|
||||||
|
async (rundownId: string) => {
|
||||||
|
setSelectRundownInParams(rundownId, search);
|
||||||
|
navigate({
|
||||||
|
pathname: '/cuesheet',
|
||||||
|
search: search.toString(),
|
||||||
|
});
|
||||||
|
},
|
||||||
|
[navigate, search],
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,101 @@
|
|||||||
|
import { isOntimeEvent, OntimeEvent } from 'ontime-types';
|
||||||
|
import { useMemo } from 'react';
|
||||||
|
|
||||||
|
import { useRundownSelectionContext } from '../context/RundownSelectionContext';
|
||||||
|
import { useSelectedEventId } from '../hooks/useSocket';
|
||||||
|
import { ExtendedEntry, getFlatRundownMetadata, getRundownMetadata } from '../utils/rundownMetadata';
|
||||||
|
import { useFlatRundown, useRundown } from './useRundown';
|
||||||
|
|
||||||
|
export function useContextRundownEditModal() {
|
||||||
|
'use memo';
|
||||||
|
const { effectiveRundownId } = useRundownSelectionContext();
|
||||||
|
const { data: rundown } = useRundown(effectiveRundownId);
|
||||||
|
return { rundown };
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useContextRundownCueRenumberModal() {
|
||||||
|
'use memo';
|
||||||
|
const { effectiveRundownId } = useRundownSelectionContext();
|
||||||
|
const { data } = useRundown(effectiveRundownId);
|
||||||
|
const { flatOrder } = data;
|
||||||
|
return { flatOrder };
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useContextRundownList() {
|
||||||
|
'use memo';
|
||||||
|
const { effectiveRundownId, isLoadedRundown } = useRundownSelectionContext();
|
||||||
|
const loadedEventId = useSelectedEventId();
|
||||||
|
const effectiveSelectedEventId = isLoadedRundown ? loadedEventId : null;
|
||||||
|
const { data: rundown, status } = useRundown(effectiveRundownId);
|
||||||
|
const rundownMetadata = useMemo(
|
||||||
|
() => getRundownMetadata(rundown, effectiveSelectedEventId),
|
||||||
|
[effectiveSelectedEventId, rundown],
|
||||||
|
);
|
||||||
|
|
||||||
|
return useMemo(
|
||||||
|
() => ({
|
||||||
|
rundown,
|
||||||
|
rundownMetadata,
|
||||||
|
status,
|
||||||
|
isLoadedRundown,
|
||||||
|
}),
|
||||||
|
[rundown, rundownMetadata, status, isLoadedRundown],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useContextRundownTitleList() {
|
||||||
|
'use memo';
|
||||||
|
const { effectiveRundownId, isLoadedRundown } = useRundownSelectionContext();
|
||||||
|
const loadedEventId = useSelectedEventId();
|
||||||
|
const effectiveSelectedEventId = isLoadedRundown ? loadedEventId : null;
|
||||||
|
const { data: rundown, status } = useRundown(effectiveRundownId);
|
||||||
|
const flatRundown = useMemo(() => {
|
||||||
|
const flatData = getFlatRundownMetadata(rundown, effectiveSelectedEventId);
|
||||||
|
return flatData.filter(isOntimeEvent) as ExtendedEntry<OntimeEvent>[];
|
||||||
|
}, [effectiveSelectedEventId, rundown]);
|
||||||
|
|
||||||
|
return useMemo(
|
||||||
|
() => ({
|
||||||
|
flatRundown,
|
||||||
|
status,
|
||||||
|
isLoadedRundown,
|
||||||
|
}),
|
||||||
|
[status, isLoadedRundown, flatRundown],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useContextRundownTable() {
|
||||||
|
'use memo';
|
||||||
|
const { effectiveRundownId, isLoadedRundown } = useRundownSelectionContext();
|
||||||
|
const loadedEventId = useSelectedEventId();
|
||||||
|
const effectiveSelectedEventId = isLoadedRundown ? loadedEventId : null;
|
||||||
|
const { data: rundown, status } = useRundown(effectiveRundownId);
|
||||||
|
const flatRundown = useMemo(
|
||||||
|
() => getFlatRundownMetadata(rundown, effectiveSelectedEventId),
|
||||||
|
[effectiveSelectedEventId, rundown],
|
||||||
|
);
|
||||||
|
|
||||||
|
return useMemo(
|
||||||
|
() => ({
|
||||||
|
flatRundown,
|
||||||
|
status,
|
||||||
|
loadedEventId,
|
||||||
|
}),
|
||||||
|
[flatRundown, status, loadedEventId],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useContextRundownFinder() {
|
||||||
|
'use memo';
|
||||||
|
const { effectiveRundownId } = useRundownSelectionContext();
|
||||||
|
const { data: rundown, status } = useFlatRundown(effectiveRundownId);
|
||||||
|
|
||||||
|
return useMemo(
|
||||||
|
() => ({
|
||||||
|
rundown,
|
||||||
|
rundownId: effectiveRundownId,
|
||||||
|
status,
|
||||||
|
}),
|
||||||
|
[rundown, status, effectiveRundownId],
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,24 +1,18 @@
|
|||||||
import { useQuery } from '@tanstack/react-query';
|
import { useSuspenseQuery } from '@tanstack/react-query';
|
||||||
import { ProjectFile, ProjectFileList, ProjectFileListResponse } from 'ontime-types';
|
import { ProjectFile, ProjectFileList } from 'ontime-types';
|
||||||
|
import { MILLIS_PER_HOUR } from 'ontime-utils';
|
||||||
import { useMemo } from 'react';
|
import { useMemo } from 'react';
|
||||||
|
|
||||||
import { queryRefetchIntervalSlow } from '../../ontimeConfig';
|
|
||||||
import { PROJECT_LIST } from '../api/constants';
|
import { PROJECT_LIST } from '../api/constants';
|
||||||
import { getProjects } from '../api/db';
|
import { getProjects } from '../api/db';
|
||||||
|
|
||||||
const placeholderProjectList: ProjectFileListResponse = {
|
export function useProjectList() {
|
||||||
files: [],
|
const { data, status, refetch } = useSuspenseQuery({
|
||||||
lastLoadedProject: '',
|
|
||||||
};
|
|
||||||
|
|
||||||
function useProjectList() {
|
|
||||||
const { data, status, refetch } = useQuery({
|
|
||||||
queryKey: PROJECT_LIST,
|
queryKey: PROJECT_LIST,
|
||||||
queryFn: ({ signal }) => getProjects({ signal }),
|
queryFn: ({ signal }) => getProjects({ signal }),
|
||||||
placeholderData: (previousData, _previousQuery) => previousData,
|
staleTime: MILLIS_PER_HOUR,
|
||||||
refetchInterval: queryRefetchIntervalSlow,
|
|
||||||
});
|
});
|
||||||
return { data: data ?? placeholderProjectList, status, refetch };
|
return { data, status, refetch };
|
||||||
}
|
}
|
||||||
|
|
||||||
export type ProjectSortMode = 'alphabetical-asc' | 'alphabetical-desc' | 'modified-asc' | 'modified-desc';
|
export type ProjectSortMode = 'alphabetical-asc' | 'alphabetical-desc' | 'modified-asc' | 'modified-desc';
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
|
import { useMutation, useSuspenseQuery } from '@tanstack/react-query';
|
||||||
import { ProjectRundownsList } from 'ontime-types';
|
import { ProjectRundownsList } from 'ontime-types';
|
||||||
|
import { MILLIS_PER_HOUR } from 'ontime-utils';
|
||||||
|
|
||||||
import { queryRefetchIntervalSlow } from '../../ontimeConfig';
|
|
||||||
import { PROJECT_RUNDOWNS } from '../api/constants';
|
import { PROJECT_RUNDOWNS } from '../api/constants';
|
||||||
import {
|
import {
|
||||||
createRundown,
|
createRundown,
|
||||||
@@ -11,24 +11,21 @@ import {
|
|||||||
loadRundown,
|
loadRundown,
|
||||||
renameRundown,
|
renameRundown,
|
||||||
} from '../api/rundown';
|
} from '../api/rundown';
|
||||||
|
import { ontimeQueryClient } from '../queryClient';
|
||||||
|
|
||||||
//TODO: make suspends so we don't have to deal with no value all over
|
|
||||||
/**
|
/**
|
||||||
* Project rundowns
|
* Project rundowns
|
||||||
*/
|
*/
|
||||||
export function useProjectRundowns() {
|
export function useProjectRundowns() {
|
||||||
const { data, status, isError, refetch, isFetching } = useQuery<ProjectRundownsList>({
|
const { data, status, isError, refetch, isFetching } = useSuspenseQuery<ProjectRundownsList>({
|
||||||
queryKey: PROJECT_RUNDOWNS,
|
queryKey: PROJECT_RUNDOWNS,
|
||||||
queryFn: ({ signal }) => fetchProjectRundownList({ signal }),
|
queryFn: ({ signal }) => fetchProjectRundownList({ signal }),
|
||||||
placeholderData: (previousData, _previousQuery) => previousData,
|
staleTime: MILLIS_PER_HOUR,
|
||||||
refetchInterval: queryRefetchIntervalSlow,
|
|
||||||
});
|
});
|
||||||
return { data: data ?? { loaded: '', rundowns: [] }, status, isError, refetch, isFetching };
|
return { data, status, isError, refetch, isFetching };
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useMutateProjectRundowns() {
|
export function useMutateProjectRundowns() {
|
||||||
const ontimeQueryClient = useQueryClient();
|
|
||||||
|
|
||||||
const { mutateAsync: create } = useMutation({
|
const { mutateAsync: create } = useMutation({
|
||||||
mutationFn: createRundown,
|
mutationFn: createRundown,
|
||||||
onMutate: () => {
|
onMutate: () => {
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
import { useQuery, useQueryClient } from '@tanstack/react-query';
|
import { useQuery } from '@tanstack/react-query';
|
||||||
import { EntryId, OntimeEntry, Rundown } from 'ontime-types';
|
import { EntryId, Maybe, OntimeEntry, Rundown } from 'ontime-types';
|
||||||
import { useEffect, useMemo } from 'react';
|
import { useMemo } from 'react';
|
||||||
|
|
||||||
import { queryRefetchIntervalSlow } from '../../ontimeConfig';
|
import { queryRefetchIntervalSlow } from '../../ontimeConfig';
|
||||||
import { CURRENT_RUNDOWN_QUERY_KEY, getRundownQueryKey } from '../api/constants';
|
import { getRundownQueryKey } from '../api/constants';
|
||||||
import { fetchCurrentRundown, fetchRundown } from '../api/rundown';
|
import { fetchRundown } from '../api/rundown';
|
||||||
import { useSelectedEventId } from '../hooks/useSocket';
|
import { useSelectedEventId } from '../hooks/useSocket';
|
||||||
import { ExtendedEntry, getFlatRundownMetadata, getRundownMetadata } from '../utils/rundownMetadata';
|
import { ExtendedEntry, getFlatRundownMetadata, getRundownMetadata } from '../utils/rundownMetadata';
|
||||||
import { useProjectRundowns } from './useProjectRundowns';
|
import { useProjectRundowns } from './useProjectRundowns';
|
||||||
@@ -20,43 +20,36 @@ const cachedRundownPlaceholder: Rundown = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Normalised rundown data for the currently loaded rundown.
|
* Provides access to a specific rundown by ID.
|
||||||
*
|
* When rundownId is not provided the loaded rundown is provided
|
||||||
* Bootstraps via the `/current` alias so the first paint is a single round-trip,
|
|
||||||
* independent of the project rundown list. Once the loaded id is known, the
|
|
||||||
* query key swaps to the id-keyed cache that is shared with `useRundownById`.
|
|
||||||
*/
|
*/
|
||||||
export default function useRundown() {
|
export function useRundown(rundownId: Maybe<string>) {
|
||||||
const queryClient = useQueryClient();
|
'use memo';
|
||||||
|
|
||||||
const {
|
const {
|
||||||
data: { loaded: loadedRundownId },
|
data: { loaded: loadedRundownId },
|
||||||
} = useProjectRundowns();
|
} = useProjectRundowns();
|
||||||
|
|
||||||
|
const effectiveRundownId = rundownId !== null ? rundownId : loadedRundownId;
|
||||||
|
const isLoadedRundown = effectiveRundownId === loadedRundownId;
|
||||||
|
|
||||||
const { data, status, isError, refetch, isFetching } = useQuery<Rundown>({
|
const { data, status, isError, refetch, isFetching } = useQuery<Rundown>({
|
||||||
queryKey: loadedRundownId ? getRundownQueryKey(loadedRundownId) : CURRENT_RUNDOWN_QUERY_KEY,
|
queryKey: getRundownQueryKey(effectiveRundownId),
|
||||||
queryFn: ({ signal }) => fetchCurrentRundown({ signal }),
|
queryFn: ({ signal }) => fetchRundown(effectiveRundownId, { signal }),
|
||||||
|
placeholderData: (previousData, _previousQuery) => previousData,
|
||||||
refetchInterval: queryRefetchIntervalSlow,
|
refetchInterval: queryRefetchIntervalSlow,
|
||||||
});
|
});
|
||||||
|
|
||||||
// Seed the id-keyed cache when fetching via the bootstrap alias
|
return { data: data ?? cachedRundownPlaceholder, status, isError, refetch, isFetching, isLoadedRundown };
|
||||||
useEffect(() => {
|
|
||||||
if (!data || loadedRundownId) return;
|
|
||||||
queryClient.setQueryData(getRundownQueryKey(data.id), data);
|
|
||||||
}, [data, loadedRundownId, queryClient]);
|
|
||||||
|
|
||||||
// Once we have the ID, drop the temporary current cache
|
|
||||||
useEffect(() => {
|
|
||||||
if (!loadedRundownId) return;
|
|
||||||
queryClient.removeQueries({ queryKey: CURRENT_RUNDOWN_QUERY_KEY, exact: true });
|
|
||||||
}, [loadedRundownId, queryClient]);
|
|
||||||
|
|
||||||
return { data: data ?? cachedRundownPlaceholder, status, isError, refetch, isFetching };
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useRundownWithMetadata() {
|
export function useRundownWithMetadata(rundownId: Maybe<string>) {
|
||||||
const { data, status } = useRundown();
|
'use memo';
|
||||||
|
|
||||||
|
const { data, status, isLoadedRundown } = useRundown(rundownId);
|
||||||
const selectedEventId = useSelectedEventId();
|
const selectedEventId = useSelectedEventId();
|
||||||
const rundownMetadata = useMemo(() => getRundownMetadata(data, selectedEventId), [data, selectedEventId]);
|
const effectiveSelectedEventId = isLoadedRundown ? selectedEventId : null;
|
||||||
|
const rundownMetadata = getRundownMetadata(data, effectiveSelectedEventId);
|
||||||
return { data, status, rundownMetadata };
|
return { data, status, rundownMetadata };
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -64,8 +57,8 @@ export function useRundownWithMetadata() {
|
|||||||
* Provides access to a flat rundown
|
* Provides access to a flat rundown
|
||||||
* built from the order and rundown fields
|
* built from the order and rundown fields
|
||||||
*/
|
*/
|
||||||
export function useFlatRundown() {
|
export function useFlatRundown(rundownId: Maybe<string>) {
|
||||||
const { data, status } = useRundown();
|
const { data, status } = useRundown(rundownId);
|
||||||
|
|
||||||
const flatRundown = useMemo(() => {
|
const flatRundown = useMemo(() => {
|
||||||
if (data.revision === -1) {
|
if (data.revision === -1) {
|
||||||
@@ -77,11 +70,13 @@ export function useFlatRundown() {
|
|||||||
return { data: flatRundown, rundownId: data.id, status };
|
return { data: flatRundown, rundownId: data.id, status };
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useFlatRundownWithMetadata() {
|
export function useFlatRundownWithMetadata(rundownId: Maybe<string>) {
|
||||||
const { data, status } = useRundown();
|
'use memo';
|
||||||
const selectedEventId = useSelectedEventId();
|
|
||||||
|
|
||||||
const rundownWithMetadata = useMemo(() => getFlatRundownMetadata(data, selectedEventId), [data, selectedEventId]);
|
const { data, status, isLoadedRundown } = useRundown(rundownId);
|
||||||
|
const selectedEventId = useSelectedEventId();
|
||||||
|
const effectiveSelectedEventId = isLoadedRundown ? selectedEventId : null;
|
||||||
|
const rundownWithMetadata = getFlatRundownMetadata(data, effectiveSelectedEventId);
|
||||||
return { data: rundownWithMetadata, status };
|
return { data: rundownWithMetadata, status };
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -92,8 +87,8 @@ export function useFlatRundownWithMetadata() {
|
|||||||
* re-filtering on every render.
|
* re-filtering on every render.
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
export function usePartialRundown(cb: (event: ExtendedEntry<OntimeEntry>) => boolean) {
|
export function usePartialRundown(rundownId: Maybe<string>, cb: (event: ExtendedEntry<OntimeEntry>) => boolean) {
|
||||||
const { data, status } = useFlatRundownWithMetadata();
|
const { data, status } = useFlatRundownWithMetadata(rundownId);
|
||||||
const filteredData = useMemo(() => {
|
const filteredData = useMemo(() => {
|
||||||
return data.filter(cb);
|
return data.filter(cb);
|
||||||
}, [data, cb]);
|
}, [data, cb]);
|
||||||
@@ -103,37 +98,20 @@ export function usePartialRundown(cb: (event: ExtendedEntry<OntimeEntry>) => boo
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Hook to get a specific entry by ID from the rundown
|
* Hook to get a specific entry by ID from the rundown
|
||||||
|
* @deprecated
|
||||||
*/
|
*/
|
||||||
export function useEntry(entryId: EntryId | null): OntimeEntry | null {
|
export function useEntry(rundownId: Maybe<string>, entryId: EntryId | null): OntimeEntry | null {
|
||||||
const { data: rundown } = useRundown();
|
const { data: rundown } = useRundown(rundownId);
|
||||||
|
|
||||||
if (entryId === null) return null;
|
if (entryId === null) return null;
|
||||||
return rundown.entries[entryId] ?? null;
|
return rundown.entries[entryId] ?? null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useRundownAuxData() {
|
export function useRundownAuxData(rundownId: Maybe<string>) {
|
||||||
const { data, status } = useRundown();
|
const { data, status } = useRundown(rundownId);
|
||||||
const filteredData = useMemo(() => {
|
const filteredData = useMemo(() => {
|
||||||
const { title, id } = data;
|
const { title, id } = data;
|
||||||
return { title, id };
|
return { title, id };
|
||||||
}, [data]);
|
}, [data]);
|
||||||
return { data: filteredData, status };
|
return { data: filteredData, status };
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Provides access to a specific rundown by ID.
|
|
||||||
* When rundownId is null/undefined the query is disabled and returns the placeholder.
|
|
||||||
*/
|
|
||||||
export function useRundownById(rundownId: string | null | undefined) {
|
|
||||||
const enabled = Boolean(rundownId);
|
|
||||||
|
|
||||||
const { data, status, isError, refetch, isFetching } = useQuery<Rundown>({
|
|
||||||
queryKey: getRundownQueryKey(rundownId ?? ''),
|
|
||||||
queryFn: ({ signal }) => fetchRundown(rundownId!, { signal }),
|
|
||||||
enabled,
|
|
||||||
placeholderData: (previousData, _previousQuery) => previousData,
|
|
||||||
refetchInterval: queryRefetchIntervalSlow,
|
|
||||||
});
|
|
||||||
|
|
||||||
return { data: data ?? cachedRundownPlaceholder, status, isError, refetch, isFetching };
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,54 +0,0 @@
|
|||||||
import { EntryId, Rundown } from 'ontime-types';
|
|
||||||
import { useMemo } from 'react';
|
|
||||||
|
|
||||||
import { useSelectedEventId } from '../hooks/useSocket';
|
|
||||||
import { getFlatRundownMetadata, type ExtendedEntry } from '../utils/rundownMetadata';
|
|
||||||
import { useProjectRundowns } from './useProjectRundowns';
|
|
||||||
import { useRundownById } from './useRundown';
|
|
||||||
|
|
||||||
export type RundownSource = {
|
|
||||||
rundownId: string | null;
|
|
||||||
rundown: Rundown;
|
|
||||||
flatRundown: ExtendedEntry[];
|
|
||||||
status: string;
|
|
||||||
selectedEventId: EntryId | null;
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Explicitly scoped rundown data for views that may operate on a non-loaded rundown.
|
|
||||||
*/
|
|
||||||
export function useScopedRundown(rundownId: string | null): RundownSource {
|
|
||||||
const { data: projectRundowns } = useProjectRundowns();
|
|
||||||
return useRundownSource(rundownId, projectRundowns.loaded || null);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Loaded-rundown source for views that must follow the active runtime rundown.
|
|
||||||
*/
|
|
||||||
export function useLoadedRundownSource(): RundownSource {
|
|
||||||
const { data: projectRundowns } = useProjectRundowns();
|
|
||||||
const loadedRundownId = projectRundowns.loaded || null;
|
|
||||||
return useRundownSource(loadedRundownId, loadedRundownId);
|
|
||||||
}
|
|
||||||
|
|
||||||
function useRundownSource(rundownId: string | null, loadedRundownId: string | null): RundownSource {
|
|
||||||
const isLoadedTarget = rundownId !== null && rundownId === loadedRundownId;
|
|
||||||
const runtimeSelectedEventId = useSelectedEventId();
|
|
||||||
const effectiveSelectedEventId = isLoadedTarget ? runtimeSelectedEventId : null;
|
|
||||||
const { data: rundown, status } = useRundownById(rundownId);
|
|
||||||
const flatRundown = useMemo(
|
|
||||||
() => getFlatRundownMetadata(rundown, effectiveSelectedEventId),
|
|
||||||
[effectiveSelectedEventId, rundown],
|
|
||||||
);
|
|
||||||
|
|
||||||
return useMemo(
|
|
||||||
() => ({
|
|
||||||
rundownId,
|
|
||||||
rundown,
|
|
||||||
flatRundown,
|
|
||||||
status,
|
|
||||||
selectedEventId: effectiveSelectedEventId,
|
|
||||||
}),
|
|
||||||
[effectiveSelectedEventId, flatRundown, rundown, rundownId, status],
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -90,6 +90,7 @@ describe('initRundownMetadata()', () => {
|
|||||||
eventIndex: 0,
|
eventIndex: 0,
|
||||||
isPast: true,
|
isPast: true,
|
||||||
isNextDay: false,
|
isNextDay: false,
|
||||||
|
isParentToLoaded: false,
|
||||||
totalGap: 0,
|
totalGap: 0,
|
||||||
isLinkedToLoaded: false,
|
isLinkedToLoaded: false,
|
||||||
isLoaded: false,
|
isLoaded: false,
|
||||||
@@ -107,6 +108,7 @@ describe('initRundownMetadata()', () => {
|
|||||||
eventIndex: 1, // UI indexes are 1 based
|
eventIndex: 1, // UI indexes are 1 based
|
||||||
isPast: true,
|
isPast: true,
|
||||||
isNextDay: false,
|
isNextDay: false,
|
||||||
|
isParentToLoaded: false,
|
||||||
totalGap: 0,
|
totalGap: 0,
|
||||||
isLinkedToLoaded: false,
|
isLinkedToLoaded: false,
|
||||||
isLoaded: false,
|
isLoaded: false,
|
||||||
@@ -124,6 +126,7 @@ describe('initRundownMetadata()', () => {
|
|||||||
eventIndex: 1,
|
eventIndex: 1,
|
||||||
isPast: true,
|
isPast: true,
|
||||||
isNextDay: false,
|
isNextDay: false,
|
||||||
|
isParentToLoaded: true,
|
||||||
totalGap: 0,
|
totalGap: 0,
|
||||||
isLinkedToLoaded: false,
|
isLinkedToLoaded: false,
|
||||||
isLoaded: false,
|
isLoaded: false,
|
||||||
@@ -140,6 +143,7 @@ describe('initRundownMetadata()', () => {
|
|||||||
eventIndex: 2,
|
eventIndex: 2,
|
||||||
isPast: true,
|
isPast: true,
|
||||||
isNextDay: false,
|
isNextDay: false,
|
||||||
|
isParentToLoaded: false,
|
||||||
totalGap: 0,
|
totalGap: 0,
|
||||||
isLinkedToLoaded: false,
|
isLinkedToLoaded: false,
|
||||||
isLoaded: false,
|
isLoaded: false,
|
||||||
@@ -156,6 +160,7 @@ describe('initRundownMetadata()', () => {
|
|||||||
eventIndex: 2,
|
eventIndex: 2,
|
||||||
isPast: true,
|
isPast: true,
|
||||||
isNextDay: false,
|
isNextDay: false,
|
||||||
|
isParentToLoaded: false,
|
||||||
totalGap: 0,
|
totalGap: 0,
|
||||||
isLinkedToLoaded: false,
|
isLinkedToLoaded: false,
|
||||||
isLoaded: false,
|
isLoaded: false,
|
||||||
@@ -172,6 +177,7 @@ describe('initRundownMetadata()', () => {
|
|||||||
eventIndex: 3,
|
eventIndex: 3,
|
||||||
isPast: false,
|
isPast: false,
|
||||||
isNextDay: false,
|
isNextDay: false,
|
||||||
|
isParentToLoaded: false,
|
||||||
totalGap: 0,
|
totalGap: 0,
|
||||||
isLinkedToLoaded: true,
|
isLinkedToLoaded: true,
|
||||||
isLoaded: true,
|
isLoaded: true,
|
||||||
@@ -188,6 +194,7 @@ describe('initRundownMetadata()', () => {
|
|||||||
eventIndex: 4,
|
eventIndex: 4,
|
||||||
isPast: false,
|
isPast: false,
|
||||||
isNextDay: false,
|
isNextDay: false,
|
||||||
|
isParentToLoaded: false,
|
||||||
totalGap: 0,
|
totalGap: 0,
|
||||||
isLinkedToLoaded: true,
|
isLinkedToLoaded: true,
|
||||||
isLoaded: false,
|
isLoaded: false,
|
||||||
@@ -204,6 +211,7 @@ describe('initRundownMetadata()', () => {
|
|||||||
eventIndex: 5,
|
eventIndex: 5,
|
||||||
isPast: false,
|
isPast: false,
|
||||||
isNextDay: false,
|
isNextDay: false,
|
||||||
|
isParentToLoaded: false,
|
||||||
totalGap: 7,
|
totalGap: 7,
|
||||||
isLinkedToLoaded: false,
|
isLinkedToLoaded: false,
|
||||||
isLoaded: false,
|
isLoaded: false,
|
||||||
@@ -256,6 +264,7 @@ describe('initRundownMetadata()', () => {
|
|||||||
eventIndex: 0,
|
eventIndex: 0,
|
||||||
isPast: false,
|
isPast: false,
|
||||||
isNextDay: false,
|
isNextDay: false,
|
||||||
|
isParentToLoaded: false,
|
||||||
totalGap: 0,
|
totalGap: 0,
|
||||||
isLinkedToLoaded: false,
|
isLinkedToLoaded: false,
|
||||||
isLoaded: false,
|
isLoaded: false,
|
||||||
@@ -273,6 +282,7 @@ describe('initRundownMetadata()', () => {
|
|||||||
eventIndex: 1,
|
eventIndex: 1,
|
||||||
isPast: false,
|
isPast: false,
|
||||||
isNextDay: false,
|
isNextDay: false,
|
||||||
|
isParentToLoaded: false,
|
||||||
totalGap: 0,
|
totalGap: 0,
|
||||||
isLinkedToLoaded: false,
|
isLinkedToLoaded: false,
|
||||||
isLoaded: false,
|
isLoaded: false,
|
||||||
@@ -290,6 +300,7 @@ describe('initRundownMetadata()', () => {
|
|||||||
eventIndex: 2,
|
eventIndex: 2,
|
||||||
isPast: false,
|
isPast: false,
|
||||||
isNextDay: false,
|
isNextDay: false,
|
||||||
|
isParentToLoaded: false,
|
||||||
totalGap: 0,
|
totalGap: 0,
|
||||||
isLinkedToLoaded: false,
|
isLinkedToLoaded: false,
|
||||||
isLoaded: false,
|
isLoaded: false,
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ export type RundownMetadata = {
|
|||||||
groupColour: string | undefined;
|
groupColour: string | undefined;
|
||||||
groupEntries: number | undefined;
|
groupEntries: number | undefined;
|
||||||
isFirstAfterGroup: boolean;
|
isFirstAfterGroup: boolean;
|
||||||
|
isParentToLoaded: boolean; // if the group contains the loaded event
|
||||||
};
|
};
|
||||||
|
|
||||||
export type ExtendedEntry<T extends OntimeEntry = OntimeEntry> = T & RundownMetadata;
|
export type ExtendedEntry<T extends OntimeEntry = OntimeEntry> = T & RundownMetadata;
|
||||||
@@ -94,6 +95,7 @@ export function initRundownMetadata(selectedEventId: MaybeString) {
|
|||||||
groupColour: undefined,
|
groupColour: undefined,
|
||||||
groupEntries: undefined,
|
groupEntries: undefined,
|
||||||
isFirstAfterGroup: false,
|
isFirstAfterGroup: false,
|
||||||
|
isParentToLoaded: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
function process(entry: OntimeEntry): Readonly<RundownMetadata> {
|
function process(entry: OntimeEntry): Readonly<RundownMetadata> {
|
||||||
@@ -117,6 +119,7 @@ function processEntry(
|
|||||||
// initialise data to be overridden below
|
// initialise data to be overridden below
|
||||||
processedData.isNextDay = false;
|
processedData.isNextDay = false;
|
||||||
processedData.isLoaded = false;
|
processedData.isLoaded = false;
|
||||||
|
processedData.isParentToLoaded = false;
|
||||||
|
|
||||||
processedData.previousEntryId = processedData.thisId; // thisId comes from the previous iteration
|
processedData.previousEntryId = processedData.thisId; // thisId comes from the previous iteration
|
||||||
processedData.thisId = entry.id; // we reassign thisId
|
processedData.thisId = entry.id; // we reassign thisId
|
||||||
@@ -132,6 +135,7 @@ function processEntry(
|
|||||||
processedData.groupId = entry.id;
|
processedData.groupId = entry.id;
|
||||||
processedData.groupColour = entry.colour;
|
processedData.groupColour = entry.colour;
|
||||||
processedData.groupEntries = entry.entries.length;
|
processedData.groupEntries = entry.entries.length;
|
||||||
|
processedData.isParentToLoaded = selectedEventId ? entry.entries.includes(selectedEventId) : false;
|
||||||
} else {
|
} else {
|
||||||
// for delays and groups, we insert the group metadata
|
// for delays and groups, we insert the group metadata
|
||||||
if ((entry as OntimeEvent | OntimeDelay | OntimeMilestone).parent !== processedData.groupId) {
|
if ((entry as OntimeEvent | OntimeDelay | OntimeMilestone).parent !== processedData.groupId) {
|
||||||
|
|||||||
@@ -14,7 +14,6 @@ import {
|
|||||||
APP_SETTINGS,
|
APP_SETTINGS,
|
||||||
CLIENT_LIST,
|
CLIENT_LIST,
|
||||||
CSS_OVERRIDE,
|
CSS_OVERRIDE,
|
||||||
CURRENT_RUNDOWN_QUERY_KEY,
|
|
||||||
CUSTOM_FIELDS,
|
CUSTOM_FIELDS,
|
||||||
PROJECT_DATA,
|
PROJECT_DATA,
|
||||||
REPORT,
|
REPORT,
|
||||||
@@ -25,6 +24,7 @@ import {
|
|||||||
VIEW_SETTINGS,
|
VIEW_SETTINGS,
|
||||||
getRundownQueryKey,
|
getRundownQueryKey,
|
||||||
PROJECT_RUNDOWNS,
|
PROJECT_RUNDOWNS,
|
||||||
|
PROJECT_LIST,
|
||||||
} from '../api/constants';
|
} from '../api/constants';
|
||||||
import { invalidateAllCaches } from '../api/utils';
|
import { invalidateAllCaches } from '../api/utils';
|
||||||
import { ontimeQueryClient } from '../queryClient';
|
import { ontimeQueryClient } from '../queryClient';
|
||||||
@@ -216,6 +216,9 @@ export const connectSocket = () => {
|
|||||||
case RefetchKey.ProjectRundowns:
|
case RefetchKey.ProjectRundowns:
|
||||||
ontimeQueryClient.invalidateQueries({ queryKey: PROJECT_RUNDOWNS });
|
ontimeQueryClient.invalidateQueries({ queryKey: PROJECT_RUNDOWNS });
|
||||||
break;
|
break;
|
||||||
|
case RefetchKey.ProjectFiles:
|
||||||
|
ontimeQueryClient.invalidateQueries({ queryKey: PROJECT_LIST });
|
||||||
|
break;
|
||||||
default: {
|
default: {
|
||||||
target satisfies never;
|
target satisfies never;
|
||||||
break;
|
break;
|
||||||
@@ -238,7 +241,6 @@ export function maybeInvalidateRundownCache(revision: MaybeNumber, rundownId?: s
|
|||||||
if (!rundownId) {
|
if (!rundownId) {
|
||||||
// we omit rundownId to signify invalidate all rundowns
|
// we omit rundownId to signify invalidate all rundowns
|
||||||
ontimeQueryClient.invalidateQueries({ queryKey: RUNDOWN });
|
ontimeQueryClient.invalidateQueries({ queryKey: RUNDOWN });
|
||||||
ontimeQueryClient.invalidateQueries({ queryKey: CURRENT_RUNDOWN_QUERY_KEY, exact: true });
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -252,12 +254,6 @@ export function maybeInvalidateRundownCache(revision: MaybeNumber, rundownId?: s
|
|||||||
}
|
}
|
||||||
|
|
||||||
ontimeQueryClient.invalidateQueries({ queryKey, exact: true });
|
ontimeQueryClient.invalidateQueries({ queryKey, exact: true });
|
||||||
|
|
||||||
// keep current alias in sync with the ID-based cache
|
|
||||||
const loadedRundownId = ontimeQueryClient.getQueryData<{ loaded: string }>(PROJECT_RUNDOWNS)?.loaded;
|
|
||||||
if (!loadedRundownId || loadedRundownId === rundownId) {
|
|
||||||
ontimeQueryClient.invalidateQueries({ queryKey: CURRENT_RUNDOWN_QUERY_KEY, exact: true });
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function sendSocket<T extends MessageTag | ApiActionTag>(
|
export function sendSocket<T extends MessageTag | ApiActionTag>(
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import { deleteAllReport } from '../../../../common/api/report';
|
|||||||
import { createBlob, downloadBlob } from '../../../../common/api/utils';
|
import { createBlob, downloadBlob } from '../../../../common/api/utils';
|
||||||
import Button from '../../../../common/components/buttons/Button';
|
import Button from '../../../../common/components/buttons/Button';
|
||||||
import useReport from '../../../../common/hooks-query/useReport';
|
import useReport from '../../../../common/hooks-query/useReport';
|
||||||
import useRundown from '../../../../common/hooks-query/useRundown';
|
import { useRundown } from '../../../../common/hooks-query/useRundown';
|
||||||
import { cx } from '../../../../common/utils/styleUtils';
|
import { cx } from '../../../../common/utils/styleUtils';
|
||||||
import { formatTime } from '../../../../common/utils/time';
|
import { formatTime } from '../../../../common/utils/time';
|
||||||
import * as Panel from '../../panel-utils/PanelUtils';
|
import * as Panel from '../../panel-utils/PanelUtils';
|
||||||
@@ -15,7 +15,7 @@ import style from './ReportSettings.module.scss';
|
|||||||
|
|
||||||
export default function ReportSettings() {
|
export default function ReportSettings() {
|
||||||
const { data: reportData } = useReport();
|
const { data: reportData } = useReport();
|
||||||
const { data } = useRundown();
|
const { data } = useRundown(null);
|
||||||
|
|
||||||
const clearReport = async () => await deleteAllReport();
|
const clearReport = async () => await deleteAllReport();
|
||||||
const downloadCSV = (combinedReport: CombinedReport[]) => {
|
const downloadCSV = (combinedReport: CombinedReport[]) => {
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { useDisclosure } from '@mantine/hooks';
|
import { useDisclosure } from '@mantine/hooks';
|
||||||
import { useState } from 'react';
|
import { Suspense, useState } from 'react';
|
||||||
import {
|
import {
|
||||||
IoAdd,
|
IoAdd,
|
||||||
IoDocumentOutline,
|
IoDocumentOutline,
|
||||||
@@ -18,9 +18,9 @@ import IconButton from '../../../../common/components/buttons/IconButton';
|
|||||||
import Dialog from '../../../../common/components/dialog/Dialog';
|
import Dialog from '../../../../common/components/dialog/Dialog';
|
||||||
import { DropdownMenu } from '../../../../common/components/dropdown-menu/DropdownMenu';
|
import { DropdownMenu } from '../../../../common/components/dropdown-menu/DropdownMenu';
|
||||||
import Tag from '../../../../common/components/tag/Tag';
|
import Tag from '../../../../common/components/tag/Tag';
|
||||||
|
import { useDirectLinkToBackgroundEdit } from '../../../../common/context/RundownSelectionContext';
|
||||||
import { useMutateProjectRundowns, useProjectRundowns } from '../../../../common/hooks-query/useProjectRundowns';
|
import { useMutateProjectRundowns, useProjectRundowns } from '../../../../common/hooks-query/useProjectRundowns';
|
||||||
import { cx } from '../../../../common/utils/styleUtils';
|
import { cx } from '../../../../common/utils/styleUtils';
|
||||||
import { useDirectLinkToBackgroundEdit } from '../../../../views/cuesheet/useCuesheetRundownSelection';
|
|
||||||
import * as Panel from '../../panel-utils/PanelUtils';
|
import * as Panel from '../../panel-utils/PanelUtils';
|
||||||
import RundownRenameForm from './composite/RundownRenameForm';
|
import RundownRenameForm from './composite/RundownRenameForm';
|
||||||
import { ManageRundownForm } from './ManageRundownForm';
|
import { ManageRundownForm } from './ManageRundownForm';
|
||||||
@@ -28,6 +28,20 @@ import { ManageRundownForm } from './ManageRundownForm';
|
|||||||
import style from './ManagePanel.module.scss';
|
import style from './ManagePanel.module.scss';
|
||||||
|
|
||||||
export default function ManageRundowns() {
|
export default function ManageRundowns() {
|
||||||
|
return (
|
||||||
|
<Suspense
|
||||||
|
fallback={
|
||||||
|
<div className={style.empty}>
|
||||||
|
<Panel.Loader isLoading />
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<ManageRundownsSuspense />
|
||||||
|
</Suspense>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function ManageRundownsSuspense() {
|
||||||
const { data } = useProjectRundowns();
|
const { data } = useProjectRundowns();
|
||||||
const { duplicate, remove, load, rename } = useMutateProjectRundowns();
|
const { duplicate, remove, load, rename } = useMutateProjectRundowns();
|
||||||
const [isOpenDelete, deleteHandlers] = useDisclosure();
|
const [isOpenDelete, deleteHandlers] = useDisclosure();
|
||||||
|
|||||||
+2
-2
@@ -24,7 +24,7 @@ import Button from '../../../../../common/components/buttons/Button';
|
|||||||
import Info from '../../../../../common/components/info/Info';
|
import Info from '../../../../../common/components/info/Info';
|
||||||
import ExternalLink from '../../../../../common/components/link/external-link/ExternalLink';
|
import ExternalLink from '../../../../../common/components/link/external-link/ExternalLink';
|
||||||
import Modal from '../../../../../common/components/modal/Modal';
|
import Modal from '../../../../../common/components/modal/Modal';
|
||||||
import useRundown from '../../../../../common/hooks-query/useRundown';
|
import { useRundown } from '../../../../../common/hooks-query/useRundown';
|
||||||
import { removeFileExtension, validateExcelImport } from '../../../../../common/utils/uploadUtils';
|
import { removeFileExtension, validateExcelImport } from '../../../../../common/utils/uploadUtils';
|
||||||
import * as Panel from '../../../panel-utils/PanelUtils';
|
import * as Panel from '../../../panel-utils/PanelUtils';
|
||||||
import GSheetSetup from './GSheetSetup';
|
import GSheetSetup from './GSheetSetup';
|
||||||
@@ -59,7 +59,7 @@ export default function SourcesPanel() {
|
|||||||
const [activeSource, setActiveSource] = useState<ActiveSource | null>(null);
|
const [activeSource, setActiveSource] = useState<ActiveSource | null>(null);
|
||||||
const [completedRundownTitle, setCompletedRundownTitle] = useState('');
|
const [completedRundownTitle, setCompletedRundownTitle] = useState('');
|
||||||
|
|
||||||
const { data: currentRundown } = useRundown();
|
const { data: currentRundown } = useRundown(null);
|
||||||
const { applyImport } = useSpreadsheetImport();
|
const { applyImport } = useSpreadsheetImport();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { useState } from 'react';
|
import { Suspense, useState } from 'react';
|
||||||
import { IoArrowDown, IoArrowUp } from 'react-icons/io5';
|
import { IoArrowDown, IoArrowUp } from 'react-icons/io5';
|
||||||
|
|
||||||
import Info from '../../../../common/components/info/Info';
|
import Info from '../../../../common/components/info/Info';
|
||||||
@@ -11,11 +11,25 @@ import style from './ProjectPanel.module.scss';
|
|||||||
type SortParameter = 'alphabetical' | 'modified';
|
type SortParameter = 'alphabetical' | 'modified';
|
||||||
|
|
||||||
export default function ProjectList() {
|
export default function ProjectList() {
|
||||||
|
return (
|
||||||
|
<Suspense
|
||||||
|
fallback={
|
||||||
|
<div className={style.empty}>
|
||||||
|
<Panel.Loader isLoading />
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<ProjectListSuspend />
|
||||||
|
</Suspense>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function ProjectListSuspend() {
|
||||||
const [editingMode, setEditingMode] = useState<EditMode | null>(null);
|
const [editingMode, setEditingMode] = useState<EditMode | null>(null);
|
||||||
const [editingFilename, setEditingFilename] = useState<string | null>(null);
|
const [editingFilename, setEditingFilename] = useState<string | null>(null);
|
||||||
const [sortMode, setSortMode] = useState<ProjectSortMode>('modified-desc');
|
const [sortMode, setSortMode] = useState<ProjectSortMode>('modified-desc');
|
||||||
|
|
||||||
const { data, refetch, status } = useOrderedProjectList(sortMode);
|
const { data, refetch } = useOrderedProjectList(sortMode);
|
||||||
|
|
||||||
const handleToggleEditMode = (editMode: EditMode, filename: string | null) => {
|
const handleToggleEditMode = (editMode: EditMode, filename: string | null) => {
|
||||||
setEditingMode((prev) => (prev === editMode && filename === editingFilename ? null : editMode));
|
setEditingMode((prev) => (prev === editMode && filename === editingFilename ? null : editMode));
|
||||||
@@ -38,14 +52,6 @@ export default function ProjectList() {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
if (status === 'pending') {
|
|
||||||
return (
|
|
||||||
<div className={style.empty}>
|
|
||||||
<Panel.Loader isLoading />
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const numProjects = data.reorderedProjectFiles.length;
|
const numProjects = data.reorderedProjectFiles.length;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ export interface OperatorData {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function useOperatorData(): ViewData<OperatorData> {
|
export function useOperatorData(): ViewData<OperatorData> {
|
||||||
const { data: rundown, rundownMetadata, status: rundownStatus } = useRundownWithMetadata();
|
const { data: rundown, rundownMetadata, status: rundownStatus } = useRundownWithMetadata(null);
|
||||||
const { data: customFields, status: customFieldStatus } = useCustomFields();
|
const { data: customFields, status: customFieldStatus } = useCustomFields();
|
||||||
const { data: settings, status: settingsStatus } = useSettings();
|
const { data: settings, status: settingsStatus } = useSettings();
|
||||||
|
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
import { ErrorBoundary } from '@sentry/react';
|
import { ErrorBoundary } from '@sentry/react';
|
||||||
import { PropsWithChildren, ReactNode } from 'react';
|
import { PropsWithChildren, ReactNode, Suspense } from 'react';
|
||||||
|
|
||||||
import ScrollArea from '../../common/components/scroll-area/ScrollArea';
|
import ScrollArea from '../../common/components/scroll-area/ScrollArea';
|
||||||
import { useIsOnline } from '../../common/hooks/useSocket';
|
import { useIsOnline } from '../../common/hooks/useSocket';
|
||||||
import { cx } from '../../common/utils/styleUtils';
|
import { cx } from '../../common/utils/styleUtils';
|
||||||
|
import Loader from '../../views/common/loader/Loader';
|
||||||
|
|
||||||
import style from './Overview.module.scss';
|
import style from './Overview.module.scss';
|
||||||
|
|
||||||
@@ -15,18 +16,37 @@ export function OverviewWrapper({ navElements, children }: PropsWithChildren<Ove
|
|||||||
const isOnline = useIsOnline();
|
const isOnline = useIsOnline();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={cx([style.overview, !isOnline && style.isOffline])}>
|
<Suspense fallback={<OverviewFallback navElements={navElements} />}>
|
||||||
<ErrorBoundary>
|
<div className={cx([style.overview, !isOnline && style.isOffline])}>
|
||||||
<div className={style.nav}>{navElements}</div>
|
<ErrorBoundary>
|
||||||
<ScrollArea
|
<div className={style.nav}>{navElements}</div>
|
||||||
className={style.infoScroll}
|
<ScrollArea
|
||||||
contentClassName={style.info}
|
className={style.infoScroll}
|
||||||
contentStyle={{ minWidth: '100%' }}
|
contentClassName={style.info}
|
||||||
orientation='horizontal'
|
contentStyle={{ minWidth: '100%' }}
|
||||||
>
|
orientation='horizontal'
|
||||||
{children}
|
>
|
||||||
</ScrollArea>
|
{children}
|
||||||
</ErrorBoundary>
|
</ScrollArea>
|
||||||
|
</ErrorBoundary>
|
||||||
|
</div>
|
||||||
|
</Suspense>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function OverviewFallback({ navElements }: OverviewWrapperProps) {
|
||||||
|
return (
|
||||||
|
<div className={style.overview}>
|
||||||
|
<div className={style.nav}>{navElements}</div>
|
||||||
|
<ScrollArea
|
||||||
|
className={style.infoScroll}
|
||||||
|
contentClassName={style.info}
|
||||||
|
contentStyle={{ minWidth: '100%' }}
|
||||||
|
orientation='horizontal'
|
||||||
|
>
|
||||||
|
{/* TODO: this could be alined in a nicer way */}
|
||||||
|
<Loader />
|
||||||
|
</ScrollArea>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -210,7 +210,7 @@ export function MetadataTimes() {
|
|||||||
function GroupTimes() {
|
function GroupTimes() {
|
||||||
const { clock, mode, groupExpectedEnd, actualGroupStart, currentDay, playback, phase } = useGroupTimerOverView();
|
const { clock, mode, groupExpectedEnd, actualGroupStart, currentDay, playback, phase } = useGroupTimerOverView();
|
||||||
const currentGroupId = useCurrentGroupId();
|
const currentGroupId = useCurrentGroupId();
|
||||||
const group = useEntry(currentGroupId) as OntimeGroup | null;
|
const group = useEntry(null, currentGroupId) as OntimeGroup | null;
|
||||||
|
|
||||||
const hasRunningTimer = phase !== TimerPhase.Pending && isPlaybackActive(playback);
|
const hasRunningTimer = phase !== TimerPhase.Pending && isPlaybackActive(playback);
|
||||||
|
|
||||||
@@ -266,7 +266,7 @@ function GroupTimes() {
|
|||||||
function FlagTimes() {
|
function FlagTimes() {
|
||||||
const { clock, mode, actualStart, plannedStart, playback, currentDay, phase } = useFlagTimerOverView();
|
const { clock, mode, actualStart, plannedStart, playback, currentDay, phase } = useFlagTimerOverView();
|
||||||
const { id, expectedStart } = useNextFlag();
|
const { id, expectedStart } = useNextFlag();
|
||||||
const entry = useEntry(id) as OntimeEvent | null;
|
const entry = useEntry(null, id) as OntimeEvent | null;
|
||||||
|
|
||||||
const hasRunningTimer = phase !== TimerPhase.Pending && isPlaybackActive(playback);
|
const hasRunningTimer = phase !== TimerPhase.Pending && isPlaybackActive(playback);
|
||||||
|
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import style from './TitleOverview.module.scss';
|
|||||||
export default function TitleOverview() {
|
export default function TitleOverview() {
|
||||||
'use memo';
|
'use memo';
|
||||||
const { data: projectData } = useProjectData();
|
const { data: projectData } = useProjectData();
|
||||||
const { data: rundownData } = useRundownAuxData();
|
const { data: rundownData } = useRundownAuxData(null);
|
||||||
|
|
||||||
if (!projectData.title && !rundownData.title) {
|
if (!projectData.title && !rundownData.title) {
|
||||||
return null;
|
return null;
|
||||||
|
|||||||
@@ -6,8 +6,7 @@ import ErrorBoundary from '../../common/components/error-boundary/ErrorBoundary'
|
|||||||
import ViewNavigationMenu from '../../common/components/navigation-menu/ViewNavigationMenu';
|
import ViewNavigationMenu from '../../common/components/navigation-menu/ViewNavigationMenu';
|
||||||
import ProtectRoute from '../../common/components/protect-route/ProtectRoute';
|
import ProtectRoute from '../../common/components/protect-route/ProtectRoute';
|
||||||
import { EntryActionsProvider } from '../../common/context/EntryActionsContext';
|
import { EntryActionsProvider } from '../../common/context/EntryActionsContext';
|
||||||
import { useLoadedRundownSource } from '../../common/hooks-query/useScopedRundown';
|
import { RundownSelectionContextProvider } from '../../common/context/RundownSelectionContext';
|
||||||
import { useEntryActions } from '../../common/hooks/useEntryAction';
|
|
||||||
import { useIsSmallDevice } from '../../common/hooks/useIsSmallDevice';
|
import { useIsSmallDevice } from '../../common/hooks/useIsSmallDevice';
|
||||||
import { handleLinks } from '../../common/utils/linkUtils';
|
import { handleLinks } from '../../common/utils/linkUtils';
|
||||||
import { cx } from '../../common/utils/styleUtils';
|
import { cx } from '../../common/utils/styleUtils';
|
||||||
@@ -39,28 +38,29 @@ function RundownExport() {
|
|||||||
defaultValue: RundownViewMode.List,
|
defaultValue: RundownViewMode.List,
|
||||||
});
|
});
|
||||||
const isSmallDevice = useIsSmallDevice();
|
const isSmallDevice = useIsSmallDevice();
|
||||||
const entryActions = useEntryActions();
|
|
||||||
|
|
||||||
if (isSmallDevice && isExtracted) {
|
if (isSmallDevice && isExtracted) {
|
||||||
return (
|
return (
|
||||||
<EntryActionsProvider actions={entryActions}>
|
<RundownSelectionContextProvider>
|
||||||
<ProtectRoute permission='editor'>
|
<EntryActionsProvider>
|
||||||
<div
|
<ProtectRoute permission='editor'>
|
||||||
className={cx([style.rundownExport, style.extracted])}
|
<div
|
||||||
data-target='small-device'
|
className={cx([style.rundownExport, style.extracted])}
|
||||||
data-testid='panel-rundown'
|
data-target='small-device'
|
||||||
>
|
data-testid='panel-rundown'
|
||||||
<FinderPlacement />
|
>
|
||||||
<ViewNavigationMenu suppressSettings />
|
<FinderPlacement />
|
||||||
<div className={style.rundown}>
|
<ViewNavigationMenu suppressSettings />
|
||||||
<ErrorBoundary>
|
<div className={style.rundown}>
|
||||||
<RundownRoot isSmallDevice isExtracted viewMode={viewMode} setViewMode={setViewMode} />
|
<ErrorBoundary>
|
||||||
<RundownContextMenu />
|
<RundownRoot isSmallDevice isExtracted viewMode={viewMode} setViewMode={setViewMode} />
|
||||||
</ErrorBoundary>
|
<RundownContextMenu />
|
||||||
|
</ErrorBoundary>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</ProtectRoute>
|
||||||
</ProtectRoute>
|
</EntryActionsProvider>
|
||||||
</EntryActionsProvider>
|
</RundownSelectionContextProvider>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -70,30 +70,32 @@ function RundownExport() {
|
|||||||
viewMode === RundownViewMode.Table;
|
viewMode === RundownViewMode.Table;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<EntryActionsProvider actions={entryActions}>
|
<RundownSelectionContextProvider>
|
||||||
<ProtectRoute permission='editor'>
|
<EntryActionsProvider>
|
||||||
<div className={cx([style.rundownExport, isExtracted && style.extracted])} data-testid='panel-rundown'>
|
<ProtectRoute permission='editor'>
|
||||||
<FinderPlacement />
|
<div className={cx([style.rundownExport, isExtracted && style.extracted])} data-testid='panel-rundown'>
|
||||||
{isExtracted && <ViewNavigationMenu suppressSettings isNavigationLocked={getIsNavigationLocked()} />}
|
<FinderPlacement />
|
||||||
<div className={style.rundown}>
|
{isExtracted && <ViewNavigationMenu suppressSettings isNavigationLocked={getIsNavigationLocked()} />}
|
||||||
<Editor.Panel className={style.list}>
|
<div className={style.rundown}>
|
||||||
<ErrorBoundary>
|
<Editor.Panel className={style.list}>
|
||||||
{!isExtracted && <Editor.CornerExtract onClick={(event) => handleLinks('rundown', event)} />}
|
|
||||||
<RundownRoot isExtracted={isExtracted} viewMode={viewMode} setViewMode={setViewMode} />
|
|
||||||
<RundownContextMenu />
|
|
||||||
</ErrorBoundary>
|
|
||||||
</Editor.Panel>
|
|
||||||
{!hideSideBar && (
|
|
||||||
<div className={style.side}>
|
|
||||||
<ErrorBoundary>
|
<ErrorBoundary>
|
||||||
<RundownEntryEditor />
|
{!isExtracted && <Editor.CornerExtract onClick={(event) => handleLinks('rundown', event)} />}
|
||||||
|
<RundownRoot isExtracted={isExtracted} viewMode={viewMode} setViewMode={setViewMode} />
|
||||||
|
<RundownContextMenu />
|
||||||
</ErrorBoundary>
|
</ErrorBoundary>
|
||||||
</div>
|
</Editor.Panel>
|
||||||
)}
|
{!hideSideBar && (
|
||||||
|
<div className={style.side}>
|
||||||
|
<ErrorBoundary>
|
||||||
|
<RundownEntryEditor />
|
||||||
|
</ErrorBoundary>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</ProtectRoute>
|
||||||
</ProtectRoute>
|
</EntryActionsProvider>
|
||||||
</EntryActionsProvider>
|
</RundownSelectionContextProvider>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -105,17 +107,16 @@ interface RundownRootProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function RundownRoot({ isSmallDevice, isExtracted, viewMode, setViewMode }: RundownRootProps) {
|
function RundownRoot({ isSmallDevice, isExtracted, viewMode, setViewMode }: RundownRootProps) {
|
||||||
const source = useLoadedRundownSource();
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={style.rundownRoot}>
|
<div className={style.rundownRoot}>
|
||||||
{isSmallDevice ? (
|
{isSmallDevice ? (
|
||||||
<RundownHeaderMobile viewMode={viewMode} setViewMode={setViewMode} />
|
<RundownHeaderMobile viewMode={viewMode} setViewMode={setViewMode} />
|
||||||
) : (
|
) : (
|
||||||
|
// TODO: add data-background-rundown={!isLoadedRundown} styling
|
||||||
<RundownHeader isExtracted={isExtracted} viewMode={viewMode} setViewMode={setViewMode} />
|
<RundownHeader isExtracted={isExtracted} viewMode={viewMode} setViewMode={setViewMode} />
|
||||||
)}
|
)}
|
||||||
{viewMode === RundownViewMode.List ? <RundownList /> : <RundownTable />}
|
{viewMode === RundownViewMode.List ? <RundownList /> : <RundownTable />}
|
||||||
{viewMode === RundownViewMode.Table && <EntryEditModal rundown={source.rundown} />}
|
{viewMode === RundownViewMode.Table && <EntryEditModal />}
|
||||||
<RenumberCuesDialog />
|
<RenumberCuesDialog />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,16 +1,23 @@
|
|||||||
|
import { Playback } from 'ontime-types';
|
||||||
import { memo } from 'react';
|
import { memo } from 'react';
|
||||||
|
|
||||||
import Empty from '../../common/components/state/Empty';
|
import Empty from '../../common/components/state/Empty';
|
||||||
import { useRundownWithMetadata } from '../../common/hooks-query/useRundown';
|
import { useContextRundownList } from '../../common/hooks-query/useContextRundown';
|
||||||
import { useRundownEditor } from '../../common/hooks/useSocket';
|
import { useRundownEditor } from '../../common/hooks/useSocket';
|
||||||
import Rundown from './Rundown';
|
import Rundown from './Rundown';
|
||||||
|
|
||||||
|
const backgroundFeatureData = {
|
||||||
|
playback: Playback.Stop,
|
||||||
|
selectedEventId: null,
|
||||||
|
nextEventId: null,
|
||||||
|
};
|
||||||
|
|
||||||
export default memo(RundownList);
|
export default memo(RundownList);
|
||||||
function RundownList() {
|
function RundownList() {
|
||||||
const { data, status, rundownMetadata } = useRundownWithMetadata();
|
const { rundown, status, rundownMetadata, isLoadedRundown } = useContextRundownList();
|
||||||
const featureData = useRundownEditor();
|
const featureData = useRundownEditor();
|
||||||
|
|
||||||
const isLoading = status !== 'success' || !data || !rundownMetadata;
|
const isLoading = status !== 'success' || !rundown || !rundownMetadata;
|
||||||
|
|
||||||
if (isLoading) {
|
if (isLoading) {
|
||||||
return <Empty text='Connecting to server' />;
|
return <Empty text='Connecting to server' />;
|
||||||
@@ -18,12 +25,12 @@ function RundownList() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Rundown
|
<Rundown
|
||||||
order={data.order}
|
order={rundown.order}
|
||||||
flatOrder={data.flatOrder}
|
flatOrder={rundown.flatOrder}
|
||||||
entries={data.entries}
|
entries={rundown.entries}
|
||||||
id={data.id}
|
id={rundown.id}
|
||||||
rundownMetadata={rundownMetadata}
|
rundownMetadata={rundownMetadata}
|
||||||
featureData={featureData}
|
featureData={isLoadedRundown ? featureData : backgroundFeatureData}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,3 @@
|
|||||||
|
.rundownSelect {
|
||||||
|
min-width: min(20rem, calc(100vw - 6rem));
|
||||||
|
}
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
import Select from '../../../common/components/select/Select';
|
||||||
|
import { useRundownSelectionContext } from '../../../common/context/RundownSelectionContext';
|
||||||
|
import { AppMode } from '../../../ontimeConfig';
|
||||||
|
|
||||||
|
import styles from './RundownSelect.module.scss';
|
||||||
|
|
||||||
|
const FOLLOW = '___null___';
|
||||||
|
|
||||||
|
interface RundownSelectProps {
|
||||||
|
appMode: AppMode;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function RundownSelect({ appMode }: RundownSelectProps) {
|
||||||
|
'use memo';
|
||||||
|
const { selectRundownId, rundowns, loadedRundownId, selectedRundownId } = useRundownSelectionContext();
|
||||||
|
|
||||||
|
const options = rundowns.map(({ id, title }) => ({
|
||||||
|
value: id,
|
||||||
|
label: loadedRundownId === id ? `${title} (loaded)` : title,
|
||||||
|
}));
|
||||||
|
|
||||||
|
// add a follow option
|
||||||
|
options.unshift({
|
||||||
|
value: FOLLOW,
|
||||||
|
label: 'Follow loaded',
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={styles.rundownSelect}>
|
||||||
|
<Select
|
||||||
|
value={selectedRundownId ?? FOLLOW}
|
||||||
|
options={options}
|
||||||
|
onValueChange={(value) => {
|
||||||
|
if (value === FOLLOW) selectRundownId(null);
|
||||||
|
else selectRundownId(value);
|
||||||
|
}}
|
||||||
|
disabled={appMode === AppMode.Run}
|
||||||
|
fluid
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
import { OntimeEntry, isOntimeEvent, isOntimeGroup, isOntimeMilestone } from 'ontime-types';
|
import { OntimeEntry, isOntimeEvent, isOntimeGroup, isOntimeMilestone } from 'ontime-types';
|
||||||
import { useMemo } from 'react';
|
import { useMemo } from 'react';
|
||||||
|
|
||||||
import useRundown from '../../../common/hooks-query/useRundown';
|
import { useRundown } from '../../../common/hooks-query/useRundown';
|
||||||
import { useEventSelection } from '../useEventSelection';
|
import { useEventSelection } from '../useEventSelection';
|
||||||
import EventEditorFooter from './composite/EventEditorFooter';
|
import EventEditorFooter from './composite/EventEditorFooter';
|
||||||
import EventEditor from './EventEditor';
|
import EventEditor from './EventEditor';
|
||||||
@@ -13,7 +13,7 @@ import style from './EntryEditor.module.scss';
|
|||||||
|
|
||||||
export default function RundownEntryEditor() {
|
export default function RundownEntryEditor() {
|
||||||
const selectedEvents = useEventSelection((state) => state.selectedEvents);
|
const selectedEvents = useEventSelection((state) => state.selectedEvents);
|
||||||
const { data } = useRundown();
|
const { data } = useRundown(null);
|
||||||
|
|
||||||
const entry = useMemo<OntimeEntry | null>(() => {
|
const entry = useMemo<OntimeEntry | null>(() => {
|
||||||
if (data.order.length === 0) {
|
if (data.order.length === 0) {
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import Button from '../../../common/components/buttons/Button';
|
|||||||
import Dialog from '../../../common/components/dialog/Dialog';
|
import Dialog from '../../../common/components/dialog/Dialog';
|
||||||
import Input from '../../../common/components/input/input/Input';
|
import Input from '../../../common/components/input/input/Input';
|
||||||
import { useEntryActionsContext } from '../../../common/context/EntryActionsContext';
|
import { useEntryActionsContext } from '../../../common/context/EntryActionsContext';
|
||||||
import useRundown from '../../../common/hooks-query/useRundown';
|
import { useContextRundownCueRenumberModal } from '../../../common/hooks-query/useContextRundown';
|
||||||
import { orderEntries } from '../rundown.utils';
|
import { orderEntries } from '../rundown.utils';
|
||||||
import { useEventSelection } from '../useEventSelection';
|
import { useEventSelection } from '../useEventSelection';
|
||||||
|
|
||||||
@@ -17,8 +17,7 @@ type RenumberCueData = Pick<RenumberCues, 'increment' | 'prefix' | 'start'>;
|
|||||||
|
|
||||||
export default function RenumberCuesDialog() {
|
export default function RenumberCuesDialog() {
|
||||||
'use memo';
|
'use memo';
|
||||||
const { data } = useRundown();
|
const { flatOrder } = useContextRundownCueRenumberModal();
|
||||||
const { flatOrder } = data;
|
|
||||||
const { onClose, isOpen } = useRenumberCuesDialogStore();
|
const { onClose, isOpen } = useRenumberCuesDialogStore();
|
||||||
const { renumberCues } = useEntryActionsContext();
|
const { renumberCues } = useEntryActionsContext();
|
||||||
const selectedEvents = useEventSelection((state) => state.selectedEvents);
|
const selectedEvents = useEventSelection((state) => state.selectedEvents);
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import Tooltip from '../../../common/components/tooltip/Tooltip';
|
|||||||
import { setOffsetMode, useOffsetMode } from '../../../common/hooks/useSocket';
|
import { setOffsetMode, useOffsetMode } from '../../../common/hooks/useSocket';
|
||||||
import { AppMode } from '../../../ontimeConfig';
|
import { AppMode } from '../../../ontimeConfig';
|
||||||
import { EditorLayoutMode, useEditorLayout } from '../../../views/editor/useEditorLayout';
|
import { EditorLayoutMode, useEditorLayout } from '../../../views/editor/useEditorLayout';
|
||||||
|
import { RundownSelect } from '../common/RundownSelect';
|
||||||
import { RundownViewMode } from '../rundown.options';
|
import { RundownViewMode } from '../rundown.options';
|
||||||
import { useEditorFollowMode } from '../useEditorFollowMode';
|
import { useEditorFollowMode } from '../useEditorFollowMode';
|
||||||
import RundownMenu from './RundownMenu';
|
import RundownMenu from './RundownMenu';
|
||||||
@@ -24,6 +25,7 @@ interface HeaderControlsConfig {
|
|||||||
showRunEditToggle: boolean;
|
showRunEditToggle: boolean;
|
||||||
showOffsetToggle: boolean;
|
showOffsetToggle: boolean;
|
||||||
showOverflowMenu: boolean;
|
showOverflowMenu: boolean;
|
||||||
|
showRundownSelect: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const HEADER_CONTROLS_CONFIG: Record<EditorLayoutMode, HeaderControlsConfig> = {
|
export const HEADER_CONTROLS_CONFIG: Record<EditorLayoutMode, HeaderControlsConfig> = {
|
||||||
@@ -31,16 +33,19 @@ export const HEADER_CONTROLS_CONFIG: Record<EditorLayoutMode, HeaderControlsConf
|
|||||||
showRunEditToggle: true,
|
showRunEditToggle: true,
|
||||||
showOffsetToggle: true,
|
showOffsetToggle: true,
|
||||||
showOverflowMenu: true,
|
showOverflowMenu: true,
|
||||||
|
showRundownSelect: false,
|
||||||
},
|
},
|
||||||
[EditorLayoutMode.PLANNING]: {
|
[EditorLayoutMode.PLANNING]: {
|
||||||
showRunEditToggle: false,
|
showRunEditToggle: false,
|
||||||
showOffsetToggle: false,
|
showOffsetToggle: false,
|
||||||
showOverflowMenu: true,
|
showOverflowMenu: true,
|
||||||
|
showRundownSelect: true,
|
||||||
},
|
},
|
||||||
[EditorLayoutMode.TRACKING]: {
|
[EditorLayoutMode.TRACKING]: {
|
||||||
showRunEditToggle: false,
|
showRunEditToggle: false,
|
||||||
showOffsetToggle: true,
|
showOffsetToggle: true,
|
||||||
showOverflowMenu: false,
|
showOverflowMenu: false,
|
||||||
|
showRundownSelect: false,
|
||||||
},
|
},
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
@@ -50,7 +55,8 @@ function RundownHeader({ isExtracted, viewMode, setViewMode }: RundownHeaderProp
|
|||||||
const offsetMode = useOffsetMode();
|
const offsetMode = useOffsetMode();
|
||||||
const { layoutMode } = useEditorLayout();
|
const { layoutMode } = useEditorLayout();
|
||||||
|
|
||||||
const { showRunEditToggle, showOffsetToggle, showOverflowMenu } = HEADER_CONTROLS_CONFIG[layoutMode];
|
const { showRunEditToggle, showOffsetToggle, showOverflowMenu, showRundownSelect } =
|
||||||
|
HEADER_CONTROLS_CONFIG[layoutMode];
|
||||||
|
|
||||||
const toggleAppMode = (mode: AppMode[]) => {
|
const toggleAppMode = (mode: AppMode[]) => {
|
||||||
// we need to stop user from deselecting a mode
|
// we need to stop user from deselecting a mode
|
||||||
@@ -122,6 +128,8 @@ function RundownHeader({ isExtracted, viewMode, setViewMode }: RundownHeaderProp
|
|||||||
</ToggleGroup>
|
</ToggleGroup>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{showRundownSelect && <RundownSelect appMode={editorMode} />}
|
||||||
|
|
||||||
{showOverflowMenu && <RundownMenu allowNavigation={!isExtracted} />}
|
{showOverflowMenu && <RundownMenu allowNavigation={!isExtracted} />}
|
||||||
</Toolbar.Root>
|
</Toolbar.Root>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,10 +1,7 @@
|
|||||||
import { memo, useEffect, useMemo } from 'react';
|
import { memo, useEffect, useMemo } from 'react';
|
||||||
|
|
||||||
import EmptyPage from '../../../common/components/state/EmptyPage';
|
import EmptyPage from '../../../common/components/state/EmptyPage';
|
||||||
import { EntryActionsProvider } from '../../../common/context/EntryActionsContext';
|
|
||||||
import useCustomFields from '../../../common/hooks-query/useCustomFields';
|
import useCustomFields from '../../../common/hooks-query/useCustomFields';
|
||||||
import { useLoadedRundownSource } from '../../../common/hooks-query/useScopedRundown';
|
|
||||||
import { useEntryActions } from '../../../common/hooks/useEntryAction';
|
|
||||||
import CuesheetDnd from '../../../views/cuesheet/cuesheet-dnd/CuesheetDnd';
|
import CuesheetDnd from '../../../views/cuesheet/cuesheet-dnd/CuesheetDnd';
|
||||||
import CuesheetTable from '../../../views/cuesheet/cuesheet-table/CuesheetTable';
|
import CuesheetTable from '../../../views/cuesheet/cuesheet-table/CuesheetTable';
|
||||||
import { useCuesheetPermissions } from '../../../views/cuesheet/useTablePermissions';
|
import { useCuesheetPermissions } from '../../../views/cuesheet/useTablePermissions';
|
||||||
@@ -16,8 +13,6 @@ function RundownTable() {
|
|||||||
const { data: customFields, status: customFieldStatus } = useCustomFields();
|
const { data: customFields, status: customFieldStatus } = useCustomFields();
|
||||||
const setPermissions = useCuesheetPermissions((state) => state.setPermissions);
|
const setPermissions = useCuesheetPermissions((state) => state.setPermissions);
|
||||||
const { editorMode } = useEditorFollowMode();
|
const { editorMode } = useEditorFollowMode();
|
||||||
const source = useLoadedRundownSource();
|
|
||||||
const actions = useEntryActions();
|
|
||||||
|
|
||||||
// Editor always has full permissions
|
// Editor always has full permissions
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -35,14 +30,12 @@ function RundownTable() {
|
|||||||
const isLoading = !customFields || customFieldStatus === 'pending';
|
const isLoading = !customFields || customFieldStatus === 'pending';
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<EntryActionsProvider actions={actions}>
|
<CuesheetDnd columns={columns} tableRoot='editor'>
|
||||||
<CuesheetDnd columns={columns} tableRoot='editor'>
|
{isLoading ? (
|
||||||
{isLoading ? (
|
<EmptyPage text='Loading...' />
|
||||||
<EmptyPage text='Loading...' />
|
) : (
|
||||||
) : (
|
<CuesheetTable columns={columns} cuesheetMode={editorMode} tableRoot='editor' />
|
||||||
<CuesheetTable columns={columns} source={source} cuesheetMode={editorMode} tableRoot='editor' />
|
)}
|
||||||
)}
|
</CuesheetDnd>
|
||||||
</CuesheetDnd>
|
|
||||||
</EntryActionsProvider>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ export function useBackstageData(): ViewData<BackstageData> {
|
|||||||
const isMirrored = useViewOptionsStore((state) => state.mirror);
|
const isMirrored = useViewOptionsStore((state) => state.mirror);
|
||||||
|
|
||||||
// HTTP API data
|
// HTTP API data
|
||||||
const { data: rundownData, status: rundownStatus } = useFlatRundown();
|
const { data: rundownData, status: rundownStatus } = useFlatRundown(null);
|
||||||
const { data: projectData, status: projectDataStatus } = useProjectData();
|
const { data: projectData, status: projectDataStatus } = useProjectData();
|
||||||
const { data: settings, status: settingsStatus } = useSettings();
|
const { data: settings, status: settingsStatus } = useSettings();
|
||||||
const { data: customFields, status: customFieldsStatus } = useCustomFields();
|
const { data: customFields, status: customFieldsStatus } = useCustomFields();
|
||||||
|
|||||||
@@ -45,7 +45,7 @@ export const ScheduleProvider = ({ children, selectedEventId }: PropsWithChildre
|
|||||||
[filter],
|
[filter],
|
||||||
);
|
);
|
||||||
|
|
||||||
const { data: events } = usePartialRundown(filterCallback);
|
const { data: events } = usePartialRundown(null, filterCallback);
|
||||||
|
|
||||||
const [firstIndex, setFirstIndex] = useState(-1);
|
const [firstIndex, setFirstIndex] = useState(-1);
|
||||||
const [numPages, setNumPages] = useState(0);
|
const [numPages, setNumPages] = useState(0);
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ export function useCountdownData(): ViewData<CountdownData> {
|
|||||||
const isMirrored = useViewOptionsStore((state) => state.mirror);
|
const isMirrored = useViewOptionsStore((state) => state.mirror);
|
||||||
|
|
||||||
// HTTP API data
|
// HTTP API data
|
||||||
const { data: rundownData, status: rundownStatus } = useFlatRundownWithMetadata();
|
const { data: rundownData, status: rundownStatus } = useFlatRundownWithMetadata(null);
|
||||||
const { data: projectData, status: projectDataStatus } = useProjectData();
|
const { data: projectData, status: projectDataStatus } = useProjectData();
|
||||||
const { data: settings, status: settingsStatus } = useSettings();
|
const { data: settings, status: settingsStatus } = useSettings();
|
||||||
const { data: customFields, status: customFieldsStatus } = useCustomFields();
|
const { data: customFields, status: customFieldsStatus } = useCustomFields();
|
||||||
|
|||||||
@@ -14,7 +14,3 @@
|
|||||||
'table';
|
'table';
|
||||||
color: $ui-white;
|
color: $ui-white;
|
||||||
}
|
}
|
||||||
|
|
||||||
.rundownSelect {
|
|
||||||
min-width: min(20rem, calc(100vw - 6rem));
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -4,51 +4,46 @@ import { IoApps } from 'react-icons/io5';
|
|||||||
import IconButton from '../../common/components/buttons/IconButton';
|
import IconButton from '../../common/components/buttons/IconButton';
|
||||||
import NavigationMenu from '../../common/components/navigation-menu/NavigationMenu';
|
import NavigationMenu from '../../common/components/navigation-menu/NavigationMenu';
|
||||||
import { EntryActionsProvider } from '../../common/context/EntryActionsContext';
|
import { EntryActionsProvider } from '../../common/context/EntryActionsContext';
|
||||||
import { useScopedRundown } from '../../common/hooks-query/useScopedRundown';
|
import { RundownSelectionContextProvider } from '../../common/context/RundownSelectionContext';
|
||||||
import { useScopedEntryActions } from '../../common/hooks/useEntryAction';
|
|
||||||
import { useWindowTitle } from '../../common/hooks/useWindowTitle';
|
import { useWindowTitle } from '../../common/hooks/useWindowTitle';
|
||||||
import { getIsNavigationLocked } from '../../externals';
|
import { getIsNavigationLocked } from '../../externals';
|
||||||
import CuesheetOverview from '../../features/overview/CuesheetOverview';
|
import CuesheetOverview from '../../features/overview/CuesheetOverview';
|
||||||
import EntryEditModal from './cuesheet-edit-modal/EntryEditModal';
|
import EntryEditModal from './cuesheet-edit-modal/EntryEditModal';
|
||||||
import CuesheetProgress from './cuesheet-progress/CuesheetProgress';
|
import CuesheetProgress from './cuesheet-progress/CuesheetProgress';
|
||||||
import CuesheetTableWrapper from './CuesheetTableWrapper';
|
import CuesheetTableWrapper from './CuesheetTableWrapper';
|
||||||
import { FOLLOW_LOADED_RUNDOWN_ID, useCuesheetRundownSelection } from './useCuesheetRundownSelection';
|
|
||||||
|
|
||||||
import styles from './CuesheetPage.module.scss';
|
import styles from './CuesheetPage.module.scss';
|
||||||
|
|
||||||
export default function CuesheetPage() {
|
export default function CuesheetPage() {
|
||||||
'use memo';
|
'use memo';
|
||||||
const [isMenuOpen, menuHandler] = useDisclosure();
|
const [isMenuOpen, menuHandler] = useDisclosure();
|
||||||
const { selectedRundownId, loadedRundownId, setSelectedRundownId, projectRundowns } = useCuesheetRundownSelection();
|
|
||||||
const source = useScopedRundown(selectedRundownId === FOLLOW_LOADED_RUNDOWN_ID ? loadedRundownId : selectedRundownId);
|
|
||||||
|
|
||||||
const actions = useScopedEntryActions(source.rundownId);
|
|
||||||
|
|
||||||
useWindowTitle('Cuesheet');
|
useWindowTitle('Cuesheet');
|
||||||
|
|
||||||
const isLocked = getIsNavigationLocked();
|
const isLocked = getIsNavigationLocked();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<EntryActionsProvider actions={actions}>
|
<RundownSelectionContextProvider>
|
||||||
<NavigationMenu isOpen={isMenuOpen} onClose={menuHandler.close} />
|
<EntryActionsProvider>
|
||||||
<EntryEditModal rundown={source.rundown} />
|
<NavigationMenu isOpen={isMenuOpen} onClose={menuHandler.close} />
|
||||||
<div className={styles.tableWrapper} data-testid='cuesheet'>
|
<EntryEditModal />
|
||||||
<CuesheetOverview>
|
<div className={styles.tableWrapper} data-testid='cuesheet'>
|
||||||
{!isLocked && (
|
<CuesheetOverview>
|
||||||
<IconButton aria-label='Toggle navigation' variant='subtle-white' size='xlarge' onClick={menuHandler.open}>
|
{!isLocked && (
|
||||||
<IoApps />
|
<IconButton
|
||||||
</IconButton>
|
aria-label='Toggle navigation'
|
||||||
)}
|
variant='subtle-white'
|
||||||
</CuesheetOverview>
|
size='xlarge'
|
||||||
<CuesheetProgress />
|
onClick={menuHandler.open}
|
||||||
<CuesheetTableWrapper
|
>
|
||||||
source={source}
|
<IoApps />
|
||||||
selectedRundownId={selectedRundownId}
|
</IconButton>
|
||||||
loadedRundownId={loadedRundownId}
|
)}
|
||||||
setSelectedRundownId={setSelectedRundownId}
|
</CuesheetOverview>
|
||||||
projectRundowns={projectRundowns}
|
<CuesheetProgress />
|
||||||
/>
|
<CuesheetTableWrapper />
|
||||||
</div>
|
</div>
|
||||||
</EntryActionsProvider>
|
</EntryActionsProvider>
|
||||||
|
</RundownSelectionContextProvider>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,39 +1,20 @@
|
|||||||
import { MaybeString, ProjectRundown } from 'ontime-types';
|
|
||||||
import { memo, use, useMemo } from 'react';
|
import { memo, use, useMemo } from 'react';
|
||||||
|
|
||||||
import Select from '../../common/components/select/Select';
|
|
||||||
import EmptyPage from '../../common/components/state/EmptyPage';
|
import EmptyPage from '../../common/components/state/EmptyPage';
|
||||||
import { PresetContext } from '../../common/context/PresetContext';
|
import { PresetContext } from '../../common/context/PresetContext';
|
||||||
|
import { useRundownSelectionContext } from '../../common/context/RundownSelectionContext';
|
||||||
import useCustomFields from '../../common/hooks-query/useCustomFields';
|
import useCustomFields from '../../common/hooks-query/useCustomFields';
|
||||||
import type { RundownSource } from '../../common/hooks-query/useScopedRundown';
|
|
||||||
import { AppMode } from '../../ontimeConfig';
|
|
||||||
import CuesheetDnd from './cuesheet-dnd/CuesheetDnd';
|
import CuesheetDnd from './cuesheet-dnd/CuesheetDnd';
|
||||||
import { makeCuesheetColumns } from './cuesheet-table/cuesheet-table-elements/cuesheetColsFactory';
|
import { makeCuesheetColumns } from './cuesheet-table/cuesheet-table-elements/cuesheetColsFactory';
|
||||||
import CuesheetTable from './cuesheet-table/CuesheetTable';
|
import CuesheetTable from './cuesheet-table/CuesheetTable';
|
||||||
import { useApplyCuesheetPolicy } from './useApplyCuesheetPolicy';
|
import { useApplyCuesheetPolicy } from './useApplyCuesheetPolicy';
|
||||||
import { FOLLOW_LOADED_RUNDOWN_ID } from './useCuesheetRundownSelection';
|
|
||||||
|
|
||||||
import styles from './CuesheetPage.module.scss';
|
|
||||||
|
|
||||||
interface CuesheetTableWrapperProps {
|
|
||||||
source: RundownSource;
|
|
||||||
selectedRundownId: MaybeString;
|
|
||||||
loadedRundownId: string;
|
|
||||||
setSelectedRundownId: (rundownId: string) => void;
|
|
||||||
projectRundowns: ProjectRundown[];
|
|
||||||
}
|
|
||||||
|
|
||||||
export default memo(CuesheetTableWrapper);
|
export default memo(CuesheetTableWrapper);
|
||||||
function CuesheetTableWrapper({
|
function CuesheetTableWrapper() {
|
||||||
source,
|
|
||||||
selectedRundownId,
|
|
||||||
setSelectedRundownId,
|
|
||||||
loadedRundownId,
|
|
||||||
projectRundowns,
|
|
||||||
}: CuesheetTableWrapperProps) {
|
|
||||||
const preset = use(PresetContext);
|
const preset = use(PresetContext);
|
||||||
const isCurrentRundown = source.rundownId !== null && source.rundownId === loadedRundownId;
|
const { isLoadedRundown } = useRundownSelectionContext();
|
||||||
const { cuesheetMode, setCuesheetMode } = useApplyCuesheetPolicy(preset, { canRunMode: isCurrentRundown });
|
|
||||||
|
const { cuesheetMode, setCuesheetMode } = useApplyCuesheetPolicy(preset, { canRunMode: isLoadedRundown });
|
||||||
const { data: customFields, status: customFieldStatus } = useCustomFields();
|
const { data: customFields, status: customFieldStatus } = useCustomFields();
|
||||||
|
|
||||||
const columns = useMemo(
|
const columns = useMemo(
|
||||||
@@ -50,66 +31,11 @@ function CuesheetTableWrapper({
|
|||||||
) : (
|
) : (
|
||||||
<CuesheetTable
|
<CuesheetTable
|
||||||
columns={columns}
|
columns={columns}
|
||||||
source={source}
|
|
||||||
cuesheetMode={cuesheetMode}
|
cuesheetMode={cuesheetMode}
|
||||||
tableRoot='cuesheet'
|
tableRoot='cuesheet'
|
||||||
setCuesheetMode={setCuesheetMode}
|
setCuesheetMode={setCuesheetMode}
|
||||||
isCurrentRundown={isCurrentRundown}
|
|
||||||
insertElement={
|
|
||||||
<>
|
|
||||||
<RundownSelect
|
|
||||||
cuesheetMode={cuesheetMode}
|
|
||||||
selectedRundownId={selectedRundownId}
|
|
||||||
loadedRundownId={loadedRundownId}
|
|
||||||
setSelectedRundownId={setSelectedRundownId}
|
|
||||||
projectRundowns={projectRundowns}
|
|
||||||
/>
|
|
||||||
</>
|
|
||||||
}
|
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</CuesheetDnd>
|
</CuesheetDnd>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
interface RundownSelectProps {
|
|
||||||
cuesheetMode: AppMode;
|
|
||||||
selectedRundownId: MaybeString;
|
|
||||||
loadedRundownId: string;
|
|
||||||
setSelectedRundownId: (rundownId: string) => void;
|
|
||||||
projectRundowns: ProjectRundown[];
|
|
||||||
}
|
|
||||||
|
|
||||||
function RundownSelect({
|
|
||||||
cuesheetMode,
|
|
||||||
projectRundowns,
|
|
||||||
loadedRundownId,
|
|
||||||
selectedRundownId,
|
|
||||||
setSelectedRundownId,
|
|
||||||
}: RundownSelectProps) {
|
|
||||||
'use memo';
|
|
||||||
const options = projectRundowns.map(({ id, title }) => ({
|
|
||||||
value: id,
|
|
||||||
label: loadedRundownId === id ? `${title} (loaded)` : title,
|
|
||||||
}));
|
|
||||||
options.unshift({
|
|
||||||
value: FOLLOW_LOADED_RUNDOWN_ID,
|
|
||||||
label: 'Follow loaded', // TODO: Better wording and maybe icon? and translation
|
|
||||||
});
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className={styles.rundownSelect}>
|
|
||||||
<Select
|
|
||||||
value={selectedRundownId ?? undefined}
|
|
||||||
options={options}
|
|
||||||
onValueChange={(value) => {
|
|
||||||
if (value) {
|
|
||||||
setSelectedRundownId(value);
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
disabled={cuesheetMode === AppMode.Run}
|
|
||||||
fluid
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,21 +0,0 @@
|
|||||||
import {
|
|
||||||
FOLLOW_LOADED_RUNDOWN_ID,
|
|
||||||
getCuesheetRundownStorageKey,
|
|
||||||
resolveSelectedRundownId,
|
|
||||||
} from '../useCuesheetRundownSelection';
|
|
||||||
|
|
||||||
describe('useCuesheetRundownSelection helpers', () => {
|
|
||||||
it('builds a project-scoped storage key', () => {
|
|
||||||
expect(getCuesheetRundownStorageKey('http://localhost:4001', 'My Project')).toBe(
|
|
||||||
'cuesheet-selected-rundown:http://localhost:4001:My Project',
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('falls back to the follow loaded rundown when the stored selection is missing', () => {
|
|
||||||
expect(resolveSelectedRundownId('missing', new Set(['loaded', 'other']))).toBe(FOLLOW_LOADED_RUNDOWN_ID);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('keeps the stored selection when it still exists in the current project', () => {
|
|
||||||
expect(resolveSelectedRundownId('other', new Set(['loaded', 'other']))).toBe('other');
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,16 +1,13 @@
|
|||||||
import { Rundown } from 'ontime-types';
|
|
||||||
import { memo } from 'react';
|
import { memo } from 'react';
|
||||||
|
|
||||||
import Modal from '../../../common/components/modal/Modal';
|
import Modal from '../../../common/components/modal/Modal';
|
||||||
|
import { useContextRundownEditModal } from '../../../common/hooks-query/useContextRundown';
|
||||||
import CuesheetEntryEditor from '../../../features/rundown/entry-editor/CuesheetEventEditor';
|
import CuesheetEntryEditor from '../../../features/rundown/entry-editor/CuesheetEventEditor';
|
||||||
import { useEditModal } from './useEditModal';
|
import { useEditModal } from './useEditModal';
|
||||||
|
|
||||||
interface EntryEditModalProps {
|
|
||||||
rundown: Rundown;
|
|
||||||
}
|
|
||||||
|
|
||||||
export default memo(EntryEditModal);
|
export default memo(EntryEditModal);
|
||||||
function EntryEditModal({ rundown }: EntryEditModalProps) {
|
function EntryEditModal() {
|
||||||
|
const { rundown } = useContextRundownEditModal();
|
||||||
const entryId = useEditModal((state) => state.selectedEntryId);
|
const entryId = useEditModal((state) => state.selectedEntryId);
|
||||||
const closeModal = useEditModal((state) => state.clearSelection);
|
const closeModal = useEditModal((state) => state.clearSelection);
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { useTableNav } from '@table-nav/react';
|
import { useTableNav } from '@table-nav/react';
|
||||||
import { ColumnDef, Table, getCoreRowModel, useReactTable } from '@tanstack/react-table';
|
import { ColumnDef, Table, getCoreRowModel, useReactTable } from '@tanstack/react-table';
|
||||||
import { OntimeEntry, SupportedEntry, TimeField, isOntimeDelay, isOntimeGroup, isOntimeMilestone } from 'ontime-types';
|
import { OntimeEntry, SupportedEntry, TimeField, isOntimeDelay, isOntimeGroup, isOntimeMilestone } from 'ontime-types';
|
||||||
import { ComponentProps, ReactNode, memo, useCallback, useEffect, useMemo, useRef } from 'react';
|
import { ComponentProps, memo, useCallback, useEffect, useMemo, useRef } from 'react';
|
||||||
import {
|
import {
|
||||||
ContextProp,
|
ContextProp,
|
||||||
ItemProps,
|
ItemProps,
|
||||||
@@ -14,7 +14,7 @@ import {
|
|||||||
import EmptyPage from '../../../common/components/state/EmptyPage';
|
import EmptyPage from '../../../common/components/state/EmptyPage';
|
||||||
import EmptyTableBody from '../../../common/components/state/EmptyTableBody';
|
import EmptyTableBody from '../../../common/components/state/EmptyTableBody';
|
||||||
import { useEntryActionsContext } from '../../../common/context/EntryActionsContext';
|
import { useEntryActionsContext } from '../../../common/context/EntryActionsContext';
|
||||||
import type { RundownSource } from '../../../common/hooks-query/useScopedRundown';
|
import { useContextRundownTable } from '../../../common/hooks-query/useContextRundown';
|
||||||
import type { ExtendedEntry } from '../../../common/utils/rundownMetadata';
|
import type { ExtendedEntry } from '../../../common/utils/rundownMetadata';
|
||||||
import { usePersistedRundownOptions } from '../../../features/rundown/rundown.options';
|
import { usePersistedRundownOptions } from '../../../features/rundown/rundown.options';
|
||||||
import { useEventSelection } from '../../../features/rundown/useEventSelection';
|
import { useEventSelection } from '../../../features/rundown/useEventSelection';
|
||||||
@@ -35,34 +35,22 @@ import style from './CuesheetTable.module.scss';
|
|||||||
type CuesheetTableBaseProps = {
|
type CuesheetTableBaseProps = {
|
||||||
columns: ColumnDef<ExtendedEntry>[];
|
columns: ColumnDef<ExtendedEntry>[];
|
||||||
cuesheetMode: AppMode;
|
cuesheetMode: AppMode;
|
||||||
source: RundownSource;
|
|
||||||
insertElement?: ReactNode;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
type EditorCuesheetTableProps = CuesheetTableBaseProps & {
|
type EditorCuesheetTableProps = CuesheetTableBaseProps & {
|
||||||
tableRoot: 'editor';
|
tableRoot: 'editor';
|
||||||
setCuesheetMode?: undefined;
|
setCuesheetMode?: undefined;
|
||||||
isCurrentRundown?: undefined;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
type ViewCuesheetTableProps = CuesheetTableBaseProps & {
|
type ViewCuesheetTableProps = CuesheetTableBaseProps & {
|
||||||
tableRoot: 'cuesheet';
|
tableRoot: 'cuesheet';
|
||||||
setCuesheetMode: (mode: AppMode) => void;
|
setCuesheetMode: (mode: AppMode) => void;
|
||||||
isCurrentRundown?: boolean;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
type CuesheetTableProps = EditorCuesheetTableProps | ViewCuesheetTableProps;
|
type CuesheetTableProps = EditorCuesheetTableProps | ViewCuesheetTableProps;
|
||||||
|
|
||||||
export default function CuesheetTable({
|
export default function CuesheetTable({ columns, cuesheetMode, tableRoot, setCuesheetMode }: CuesheetTableProps) {
|
||||||
columns,
|
const { flatRundown, status, loadedEventId } = useContextRundownTable();
|
||||||
cuesheetMode,
|
|
||||||
source,
|
|
||||||
tableRoot,
|
|
||||||
setCuesheetMode,
|
|
||||||
isCurrentRundown,
|
|
||||||
insertElement,
|
|
||||||
}: CuesheetTableProps) {
|
|
||||||
const { flatRundown, status, selectedEventId } = source;
|
|
||||||
const { updateEntry, updateTimer, addEntry } = useEntryActionsContext();
|
const { updateEntry, updateTimer, addEntry } = useEntryActionsContext();
|
||||||
const canCreateEntries = useCuesheetPermissions((state) => state.canCreateEntries) && cuesheetMode === AppMode.Edit;
|
const canCreateEntries = useCuesheetPermissions((state) => state.canCreateEntries) && cuesheetMode === AppMode.Edit;
|
||||||
|
|
||||||
@@ -143,17 +131,17 @@ export default function CuesheetTable({
|
|||||||
|
|
||||||
// in Run mode, follow the current event
|
// in Run mode, follow the current event
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (virtuosoRef.current === null || cuesheetMode !== AppMode.Run || !selectedEventId) {
|
if (virtuosoRef.current === null || cuesheetMode !== AppMode.Run || !loadedEventId) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const eventIndex = flatRundown.findIndex((event) => event.id === selectedEventId);
|
const eventIndex = flatRundown.findIndex((event) => event.id === loadedEventId);
|
||||||
if (eventIndex === -1) {
|
if (eventIndex === -1) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
virtuosoRef.current.scrollToIndex({ index: eventIndex, behavior: 'auto', align: 'start', offset: -50 });
|
virtuosoRef.current.scrollToIndex({ index: eventIndex, behavior: 'auto', align: 'start', offset: -50 });
|
||||||
}, [cuesheetMode, flatRundown, selectedEventId]);
|
}, [cuesheetMode, flatRundown, loadedEventId]);
|
||||||
|
|
||||||
// Provide an imperative scroll handler for explicit jumps (finder/keyboard)
|
// Provide an imperative scroll handler for explicit jumps (finder/keyboard)
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -242,17 +230,9 @@ export default function CuesheetTable({
|
|||||||
handleResetResizing={resetColumnResizing}
|
handleResetResizing={resetColumnResizing}
|
||||||
handleResetReordering={resetColumnOrder}
|
handleResetReordering={resetColumnOrder}
|
||||||
handleClearToggles={setAllVisible}
|
handleClearToggles={setAllVisible}
|
||||||
insertElement={insertElement}
|
appMode={cuesheetMode}
|
||||||
modeControls={
|
tableRoot={tableRoot}
|
||||||
tableRoot === 'cuesheet'
|
setCuesheetMode={setCuesheetMode}
|
||||||
? {
|
|
||||||
cuesheetMode,
|
|
||||||
setCuesheetMode,
|
|
||||||
isCurrentRundown,
|
|
||||||
}
|
|
||||||
: undefined
|
|
||||||
}
|
|
||||||
showShare={tableRoot === 'cuesheet'}
|
|
||||||
/>
|
/>
|
||||||
<TableVirtuoso
|
<TableVirtuoso
|
||||||
ref={virtuosoRef}
|
ref={virtuosoRef}
|
||||||
|
|||||||
+23
-27
@@ -10,7 +10,9 @@ import Button from '../../../../common/components/buttons/Button';
|
|||||||
import Checkbox from '../../../../common/components/checkbox/Checkbox';
|
import Checkbox from '../../../../common/components/checkbox/Checkbox';
|
||||||
import * as Editor from '../../../../common/components/editor-utils/EditorUtils';
|
import * as Editor from '../../../../common/components/editor-utils/EditorUtils';
|
||||||
import PopoverContents from '../../../../common/components/popover/Popover';
|
import PopoverContents from '../../../../common/components/popover/Popover';
|
||||||
|
import { useRundownSelectionContext } from '../../../../common/context/RundownSelectionContext';
|
||||||
import type { ExtendedEntry } from '../../../../common/utils/rundownMetadata';
|
import type { ExtendedEntry } from '../../../../common/utils/rundownMetadata';
|
||||||
|
import { RundownSelect } from '../../../../features/rundown/common/RundownSelect';
|
||||||
import { AppMode } from '../../../../ontimeConfig';
|
import { AppMode } from '../../../../ontimeConfig';
|
||||||
import { useCuesheetPermissions } from '../../useTablePermissions';
|
import { useCuesheetPermissions } from '../../useTablePermissions';
|
||||||
import CuesheetShareModal from './CuesheetShareModal';
|
import CuesheetShareModal from './CuesheetShareModal';
|
||||||
@@ -30,21 +32,15 @@ type TableHeaderOptionValues = Pick<
|
|||||||
'hideTableSeconds' | 'hideIndexColumn' | 'showDelayedTimes' | 'hideDelays'
|
'hideTableSeconds' | 'hideIndexColumn' | 'showDelayedTimes' | 'hideDelays'
|
||||||
>;
|
>;
|
||||||
|
|
||||||
type TableModeControls = {
|
|
||||||
cuesheetMode: AppMode;
|
|
||||||
setCuesheetMode: (mode: AppMode) => void;
|
|
||||||
isCurrentRundown?: boolean;
|
|
||||||
};
|
|
||||||
|
|
||||||
interface CuesheetTableHeaderToolbarProps {
|
interface CuesheetTableHeaderToolbarProps {
|
||||||
columns: Column<ExtendedEntry, unknown>[];
|
columns: Column<ExtendedEntry, unknown>[];
|
||||||
optionsStore: TableHeaderOptionsStore;
|
optionsStore: TableHeaderOptionsStore;
|
||||||
handleResetResizing: () => void;
|
handleResetResizing: () => void;
|
||||||
handleResetReordering: () => void;
|
handleResetReordering: () => void;
|
||||||
handleClearToggles: () => void;
|
handleClearToggles: () => void;
|
||||||
insertElement?: ReactNode;
|
setCuesheetMode?: (mode: AppMode) => void;
|
||||||
modeControls?: TableModeControls;
|
appMode: AppMode;
|
||||||
showShare?: boolean;
|
tableRoot: 'editor' | 'cuesheet';
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function CuesheetTableHeaderToolbar({
|
export default function CuesheetTableHeaderToolbar({
|
||||||
@@ -53,23 +49,23 @@ export default function CuesheetTableHeaderToolbar({
|
|||||||
handleResetResizing,
|
handleResetResizing,
|
||||||
handleResetReordering,
|
handleResetReordering,
|
||||||
handleClearToggles,
|
handleClearToggles,
|
||||||
insertElement,
|
setCuesheetMode,
|
||||||
modeControls,
|
tableRoot,
|
||||||
showShare = false,
|
appMode,
|
||||||
}: CuesheetTableHeaderToolbarProps) {
|
}: CuesheetTableHeaderToolbarProps) {
|
||||||
const canChangeMode = useCuesheetPermissions((state) => state.canChangeMode);
|
const canChangeMode = useCuesheetPermissions((state) => state.canChangeMode) && tableRoot === 'cuesheet';
|
||||||
const canShare = useCuesheetPermissions((state) => state.canShare);
|
const canShare = useCuesheetPermissions((state) => state.canShare) && tableRoot === 'cuesheet';
|
||||||
|
const showRundownSelect = tableRoot === 'cuesheet';
|
||||||
|
const { isLoadedRundown } = useRundownSelectionContext();
|
||||||
|
|
||||||
const toggleCuesheetMode = (mode: AppMode[]) => {
|
const toggleCuesheetMode = (mode: AppMode[]) => {
|
||||||
const newValue = mode.at(0);
|
const newValue = mode.at(0);
|
||||||
if (!newValue || !modeControls) return;
|
if (!newValue || !setCuesheetMode) return;
|
||||||
modeControls.setCuesheetMode(newValue);
|
setCuesheetMode(newValue);
|
||||||
};
|
};
|
||||||
|
|
||||||
const isBackground = !(modeControls?.isCurrentRundown ?? true);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Toolbar.Root className={style.tableSettings} data-background-rundown={isBackground}>
|
<Toolbar.Root className={style.tableSettings} data-background-rundown={!isLoadedRundown}>
|
||||||
<ViewSettings optionsStore={optionsStore} />
|
<ViewSettings optionsStore={optionsStore} />
|
||||||
<ColumnSettings
|
<ColumnSettings
|
||||||
columns={columns}
|
columns={columns}
|
||||||
@@ -77,14 +73,14 @@ export default function CuesheetTableHeaderToolbar({
|
|||||||
handleResetReordering={handleResetReordering}
|
handleResetReordering={handleResetReordering}
|
||||||
handleClearToggles={handleClearToggles}
|
handleClearToggles={handleClearToggles}
|
||||||
/>
|
/>
|
||||||
{modeControls && canChangeMode && (
|
<div className={style.apart}>
|
||||||
<div className={style.apart}>
|
{showRundownSelect && <RundownSelect appMode={appMode} />}
|
||||||
{insertElement}
|
{canChangeMode && (
|
||||||
<ToggleGroup
|
<ToggleGroup
|
||||||
value={[modeControls.cuesheetMode]}
|
value={[appMode]}
|
||||||
onValueChange={toggleCuesheetMode}
|
onValueChange={toggleCuesheetMode}
|
||||||
className={style.group}
|
className={style.group}
|
||||||
disabled={!modeControls.isCurrentRundown}
|
disabled={!isLoadedRundown}
|
||||||
>
|
>
|
||||||
<Toolbar.Button render={<Toggle />} value={AppMode.Run} className={style.radioButton}>
|
<Toolbar.Button render={<Toggle />} value={AppMode.Run} className={style.radioButton}>
|
||||||
Run
|
Run
|
||||||
@@ -93,10 +89,10 @@ export default function CuesheetTableHeaderToolbar({
|
|||||||
Edit
|
Edit
|
||||||
</Toolbar.Button>
|
</Toolbar.Button>
|
||||||
</ToggleGroup>
|
</ToggleGroup>
|
||||||
</div>
|
)}
|
||||||
)}
|
</div>
|
||||||
|
|
||||||
{showShare && canShare && (
|
{canShare && (
|
||||||
<>
|
<>
|
||||||
<Editor.Separator orientation='vertical' />
|
<Editor.Separator orientation='vertical' />
|
||||||
<CuesheetShareModal />
|
<CuesheetShareModal />
|
||||||
|
|||||||
@@ -1,65 +0,0 @@
|
|||||||
import { useSessionStorage } from '@mantine/hooks';
|
|
||||||
import { startTransition, useCallback, useMemo } from 'react';
|
|
||||||
import { useNavigate } from 'react-router';
|
|
||||||
|
|
||||||
import { useOrderedProjectList } from '../../common/hooks-query/useProjectList';
|
|
||||||
import { useProjectRundowns } from '../../common/hooks-query/useProjectRundowns';
|
|
||||||
import { serverURL } from '../../externals';
|
|
||||||
|
|
||||||
export const FOLLOW_LOADED_RUNDOWN_ID = '__follow-loaded__' as const;
|
|
||||||
|
|
||||||
export function getCuesheetRundownStorageKey(server: string, projectFilename: string) {
|
|
||||||
return `cuesheet-selected-rundown:${server}:${projectFilename}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function resolveSelectedRundownId(storedSelectedRundownId: string | null, availableRundownIds: Set<string>) {
|
|
||||||
if (storedSelectedRundownId && availableRundownIds.has(storedSelectedRundownId)) return storedSelectedRundownId;
|
|
||||||
return FOLLOW_LOADED_RUNDOWN_ID;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function useCuesheetRundownSelection() {
|
|
||||||
'use memo';
|
|
||||||
|
|
||||||
const { data: projectRundowns } = useProjectRundowns();
|
|
||||||
const {
|
|
||||||
data: { lastLoadedProject },
|
|
||||||
} = useOrderedProjectList();
|
|
||||||
const storageKey = useMemo(() => getCuesheetRundownStorageKey(serverURL, lastLoadedProject), [lastLoadedProject]);
|
|
||||||
const [storedSelectedRundownId, setStoredSelectedRundownId] = useSessionStorage<string | null>({
|
|
||||||
key: storageKey,
|
|
||||||
defaultValue: FOLLOW_LOADED_RUNDOWN_ID,
|
|
||||||
});
|
|
||||||
|
|
||||||
const availableRundownIds = new Set(projectRundowns.rundowns.map(({ id }) => id)).add(FOLLOW_LOADED_RUNDOWN_ID);
|
|
||||||
const { loaded: loadedRundownId } = projectRundowns;
|
|
||||||
|
|
||||||
const selectedRundownId = resolveSelectedRundownId(storedSelectedRundownId, availableRundownIds);
|
|
||||||
|
|
||||||
return {
|
|
||||||
loadedRundownId,
|
|
||||||
selectedRundownId,
|
|
||||||
projectRundowns: projectRundowns.rundowns,
|
|
||||||
setSelectedRundownId: (rundownId: string) => {
|
|
||||||
startTransition(() => {
|
|
||||||
setStoredSelectedRundownId(rundownId);
|
|
||||||
});
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export function useDirectLinkToBackgroundEdit() {
|
|
||||||
const {
|
|
||||||
data: { lastLoadedProject },
|
|
||||||
} = useOrderedProjectList();
|
|
||||||
const navigate = useNavigate();
|
|
||||||
const storageKey = getCuesheetRundownStorageKey(serverURL, lastLoadedProject);
|
|
||||||
const [_, setStoredSelectedRundownId] = useSessionStorage<string | null>({ key: storageKey, defaultValue: null });
|
|
||||||
|
|
||||||
return useCallback(
|
|
||||||
async (rundownId: string) => {
|
|
||||||
await navigate('/cuesheet');
|
|
||||||
startTransition(() => setStoredSelectedRundownId(rundownId));
|
|
||||||
},
|
|
||||||
[setStoredSelectedRundownId, navigate],
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
import { lazy } from 'react';
|
import { lazy } from 'react';
|
||||||
|
|
||||||
|
import { RundownSelectionContextProvider } from '../../common/context/RundownSelectionContext';
|
||||||
import TrackingPlaybackBar from '../../features/control/playback/tracking-playback-bar/TrackingPlaybackBar';
|
import TrackingPlaybackBar from '../../features/control/playback/tracking-playback-bar/TrackingPlaybackBar';
|
||||||
import { AppMode } from '../../ontimeConfig';
|
import { AppMode } from '../../ontimeConfig';
|
||||||
import TitleList from './title-list/TitleList';
|
import TitleList from './title-list/TitleList';
|
||||||
@@ -14,44 +15,51 @@ const MessageControl = lazy(() => import('../../features/control/message/Message
|
|||||||
export default function Editor() {
|
export default function Editor() {
|
||||||
const { layoutMode } = useEditorLayout();
|
const { layoutMode } = useEditorLayout();
|
||||||
|
|
||||||
if (layoutMode === EditorLayoutMode.CONTROL) {
|
switch (layoutMode) {
|
||||||
return (
|
case EditorLayoutMode.TRACKING: {
|
||||||
<div id='panels' className={styles.panelContainer}>
|
return (
|
||||||
<div className={styles.left}>
|
<div id='panels' className={`${styles.panelContainer} ${styles.panelContainerTracking}`}>
|
||||||
<TimerControl />
|
<div className={styles.rundownLayout}>
|
||||||
<MessageControl />
|
<div className={styles.titlesPanel}>
|
||||||
</div>
|
<RundownSelectionContextProvider>
|
||||||
<Rundown />
|
<TitleList mode={AppMode.Run} />
|
||||||
</div>
|
</RundownSelectionContextProvider>
|
||||||
);
|
</div>
|
||||||
}
|
<div className={styles.rundownPanel}>
|
||||||
|
<Rundown />
|
||||||
if (layoutMode === EditorLayoutMode.TRACKING) {
|
</div>
|
||||||
return (
|
|
||||||
<div id='panels' className={`${styles.panelContainer} ${styles.panelContainerTracking}`}>
|
|
||||||
<div className={styles.rundownLayout}>
|
|
||||||
<div className={styles.titlesPanel}>
|
|
||||||
<TitleList mode={AppMode.Run} />
|
|
||||||
</div>
|
</div>
|
||||||
<div className={styles.rundownPanel}>
|
<TrackingPlaybackBar />
|
||||||
<Rundown />
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
case EditorLayoutMode.PLANNING: {
|
||||||
|
return (
|
||||||
|
<div id='panels' className={styles.panelContainer}>
|
||||||
|
<div className={styles.rundownLayout}>
|
||||||
|
<div className={styles.titlesPanel}>
|
||||||
|
<RundownSelectionContextProvider>
|
||||||
|
<TitleList mode={AppMode.Edit} />
|
||||||
|
</RundownSelectionContextProvider>
|
||||||
|
</div>
|
||||||
|
<div className={styles.rundownPanel}>
|
||||||
|
<Rundown />
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<TrackingPlaybackBar />
|
);
|
||||||
</div>
|
}
|
||||||
);
|
case EditorLayoutMode.CONTROL:
|
||||||
}
|
default: {
|
||||||
|
return (
|
||||||
return (
|
<div id='panels' className={styles.panelContainer}>
|
||||||
<div id='panels' className={styles.panelContainer}>
|
<div className={styles.left}>
|
||||||
<div className={styles.rundownLayout}>
|
<TimerControl />
|
||||||
<div className={styles.titlesPanel}>
|
<MessageControl />
|
||||||
<TitleList mode={AppMode.Edit} />
|
</div>
|
||||||
</div>
|
|
||||||
<div className={styles.rundownPanel}>
|
|
||||||
<Rundown />
|
<Rundown />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
);
|
||||||
</div>
|
}
|
||||||
);
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { EntryId, MaybeString, SupportedEntry, isOntimeEvent, isOntimeGroup, isOntimeMilestone } from 'ontime-types';
|
import { EntryId, MaybeString, SupportedEntry, isOntimeEvent, isOntimeGroup, isOntimeMilestone } from 'ontime-types';
|
||||||
import { ChangeEvent, useCallback, useEffect, useRef, useState } from 'react';
|
import { ChangeEvent, useCallback, useEffect, useRef, useState } from 'react';
|
||||||
|
|
||||||
import { useFlatRundown } from '../../../common/hooks-query/useRundown';
|
import { useContextRundownFinder } from '../../../common/hooks-query/useContextRundown';
|
||||||
import { useSelectAndRevealEntry } from '../../../features/rundown/useSelectAndRevealEntry';
|
import { useSelectAndRevealEntry } from '../../../features/rundown/useSelectAndRevealEntry';
|
||||||
|
|
||||||
const maxResults = 12;
|
const maxResults = 12;
|
||||||
@@ -38,7 +38,7 @@ type FilterableMilestone = {
|
|||||||
type FilterableEntry = FilterableGroup | FilterableEvent | FilterableMilestone;
|
type FilterableEntry = FilterableGroup | FilterableEvent | FilterableMilestone;
|
||||||
|
|
||||||
export default function useFinder() {
|
export default function useFinder() {
|
||||||
const { data, rundownId } = useFlatRundown();
|
const { rundown: data, rundownId } = useContextRundownFinder();
|
||||||
const [results, setResults] = useState<FilterableEntry[]>([]);
|
const [results, setResults] = useState<FilterableEntry[]>([]);
|
||||||
const [error, setError] = useState<MaybeString>(null);
|
const [error, setError] = useState<MaybeString>(null);
|
||||||
const lastSearchString = useRef('');
|
const lastSearchString = useRef('');
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import { useCallback, useEffect, useMemo, useRef } from 'react';
|
|||||||
import { Virtuoso, VirtuosoHandle } from 'react-virtuoso';
|
import { Virtuoso, VirtuosoHandle } from 'react-virtuoso';
|
||||||
|
|
||||||
import ScrollArea from '../../../common/components/scroll-area/ScrollArea';
|
import ScrollArea from '../../../common/components/scroll-area/ScrollArea';
|
||||||
import useRundown from '../../../common/hooks-query/useRundown';
|
import { useContextRundownList } from '../../../common/hooks-query/useContextRundown';
|
||||||
import { useSelectedEventId } from '../../../common/hooks/useSocket';
|
import { useSelectedEventId } from '../../../common/hooks/useSocket';
|
||||||
import { ExtendedEntry, getFlatRundownMetadata } from '../../../common/utils/rundownMetadata';
|
import { ExtendedEntry, getFlatRundownMetadata } from '../../../common/utils/rundownMetadata';
|
||||||
import { useEventSelection } from '../../../features/rundown/useEventSelection';
|
import { useEventSelection } from '../../../features/rundown/useEventSelection';
|
||||||
@@ -20,7 +20,7 @@ interface TitleListProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export default function TitleList({ mode }: TitleListProps) {
|
export default function TitleList({ mode }: TitleListProps) {
|
||||||
const { data: rundown } = useRundown();
|
const { rundown } = useContextRundownList();
|
||||||
const selectedEventId = useSelectedEventId();
|
const selectedEventId = useSelectedEventId();
|
||||||
const cursor = useEventSelection((state) => state.cursor);
|
const cursor = useEventSelection((state) => state.cursor);
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,9 @@
|
|||||||
import { isValueOfEnum } from 'ontime-utils';
|
import { isValueOfEnum } from 'ontime-utils';
|
||||||
|
import { useCallback, useEffect } from 'react';
|
||||||
import { useSearchParams } from 'react-router';
|
import { useSearchParams } from 'react-router';
|
||||||
|
|
||||||
|
import { setSelectRundownInParams } from '../../common/context/RundownSelectionContext';
|
||||||
|
|
||||||
const layoutParam = 'layout';
|
const layoutParam = 'layout';
|
||||||
|
|
||||||
export enum EditorLayoutMode {
|
export enum EditorLayoutMode {
|
||||||
@@ -20,14 +23,29 @@ function getEditorLayout(value: string | null): EditorLayoutMode {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function useEditorLayout() {
|
export function useEditorLayout() {
|
||||||
|
'use memo';
|
||||||
|
|
||||||
const [searchParams, setSearchParams] = useSearchParams();
|
const [searchParams, setSearchParams] = useSearchParams();
|
||||||
const layoutMode = getEditorLayout(searchParams.get(layoutParam));
|
const layoutMode = getEditorLayout(searchParams.get(layoutParam));
|
||||||
|
|
||||||
const setLayoutMode = (mode: EditorLayoutMode) => {
|
useEffect(() => {
|
||||||
const nextParams = new URLSearchParams(searchParams);
|
setSearchParams((searchParams) => {
|
||||||
nextParams.set(layoutParam, mode);
|
if (layoutMode !== EditorLayoutMode.PLANNING) setSelectRundownInParams(null, searchParams);
|
||||||
setSearchParams(nextParams, { replace: true });
|
return searchParams;
|
||||||
};
|
});
|
||||||
|
}, [setSearchParams, layoutMode]);
|
||||||
|
|
||||||
|
const setLayoutMode = useCallback(
|
||||||
|
(mode: EditorLayoutMode) => {
|
||||||
|
setSearchParams((searchParams) => {
|
||||||
|
searchParams.set(layoutParam, mode);
|
||||||
|
// Only the Planning layout is allowed to look at something other than the current rundown
|
||||||
|
if (mode !== EditorLayoutMode.PLANNING) setSelectRundownInParams(null, searchParams);
|
||||||
|
return searchParams;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
[setSearchParams],
|
||||||
|
);
|
||||||
|
|
||||||
return { layoutMode, setLayoutMode };
|
return { layoutMode, setLayoutMode };
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ export interface TimelineData {
|
|||||||
|
|
||||||
export function useTimelineData(): ViewData<TimelineData> {
|
export function useTimelineData(): ViewData<TimelineData> {
|
||||||
// HTTP API data
|
// HTTP API data
|
||||||
const { data: rundownData, status: rundownStatus } = useFlatRundownWithMetadata();
|
const { data: rundownData, status: rundownStatus } = useFlatRundownWithMetadata(null);
|
||||||
const { data: projectData, status: projectDataStatus } = useProjectData();
|
const { data: projectData, status: projectDataStatus } = useProjectData();
|
||||||
const { data: settings, status: settingsStatus } = useSettings();
|
const { data: settings, status: settingsStatus } = useSettings();
|
||||||
const { data: customFields, status: customFieldsStatus } = useCustomFields();
|
const { data: customFields, status: customFieldsStatus } = useCustomFields();
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { CustomFields, ProjectData, RundownEntries, Settings, ViewSettings } fro
|
|||||||
|
|
||||||
import useCustomFields from '../../common/hooks-query/useCustomFields';
|
import useCustomFields from '../../common/hooks-query/useCustomFields';
|
||||||
import useProjectData from '../../common/hooks-query/useProjectData';
|
import useProjectData from '../../common/hooks-query/useProjectData';
|
||||||
import useRundown from '../../common/hooks-query/useRundown';
|
import { useRundown } from '../../common/hooks-query/useRundown';
|
||||||
import useSettings from '../../common/hooks-query/useSettings';
|
import useSettings from '../../common/hooks-query/useSettings';
|
||||||
import useViewSettings from '../../common/hooks-query/useViewSettings';
|
import useViewSettings from '../../common/hooks-query/useViewSettings';
|
||||||
import { useViewOptionsStore } from '../../common/stores/viewOptions';
|
import { useViewOptionsStore } from '../../common/stores/viewOptions';
|
||||||
@@ -26,7 +26,7 @@ export function useTimerData(): ViewData<TimerData> {
|
|||||||
const { data: viewSettings, status: viewSettingsStatus } = useViewSettings();
|
const { data: viewSettings, status: viewSettingsStatus } = useViewSettings();
|
||||||
const { data: settings, status: settingsStatus } = useSettings();
|
const { data: settings, status: settingsStatus } = useSettings();
|
||||||
const { data: customFields, status: customFieldsStatus } = useCustomFields();
|
const { data: customFields, status: customFieldsStatus } = useCustomFields();
|
||||||
const { data: rundown, status: rundownStatus } = useRundown();
|
const { data: rundown, status: rundownStatus } = useRundown(null);
|
||||||
const { entries } = rundown;
|
const { entries } = rundown;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { Day, Duration, Instant, TimeOfDay } from 'ontime-types';
|
import { Day, Duration, Instant, TimeOfDay } from 'ontime-types';
|
||||||
import { MILLIS_PER_HOUR, MILLIS_PER_MINUTE, MILLIS_PER_SECOND, dayInMs } from 'ontime-utils';
|
import { MILLIS_PER_MINUTE, dayInMs } from 'ontime-utils';
|
||||||
|
|
||||||
/** Returns the current instant */
|
/** Returns the current instant */
|
||||||
export function now(): Instant {
|
export function now(): Instant {
|
||||||
@@ -59,10 +59,6 @@ export function addDuration(instant: Instant, duration: Duration | Duration[]):
|
|||||||
return (instant + totalDuration) as Instant;
|
return (instant + totalDuration) as Instant;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function combineValues<T extends Duration | Day>(...duration: T[]): T {
|
|
||||||
return Array.isArray(duration) ? (duration.reduce<number>((total, current) => total + current, 0) as T) : duration;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Calculates elapsed time on the clock from a starting time to the current time
|
* Calculates elapsed time on the clock from a starting time to the current time
|
||||||
* Handles overnight crossing (when current < start, assumes we've crossed midnight)
|
* Handles overnight crossing (when current < start, assumes we've crossed midnight)
|
||||||
@@ -84,15 +80,3 @@ export function daysSinceStart(startEpoch: Instant, currentEpoch: Instant): Day
|
|||||||
|
|
||||||
return (currentDaySerial - startDaySerial) as Day;
|
return (currentDaySerial - startDaySerial) as Day;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const ZERO_DURATION = 0 as Duration;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* utility function to generate Instant times
|
|
||||||
*/
|
|
||||||
export function generateInstanceFromClock(hour: number, minutes: number, second: number, ms = 0) {
|
|
||||||
return toInstant(
|
|
||||||
(hour * MILLIS_PER_HOUR + minutes * MILLIS_PER_MINUTE + second * MILLIS_PER_SECOND + ms) as TimeOfDay,
|
|
||||||
now(),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,8 +1,7 @@
|
|||||||
import { Duration, EndAction, Playback, TimeOfDay, TimeStrategy, TimerPhase, TimerType } from 'ontime-types';
|
import { EndAction, Playback, TimeOfDay, TimeStrategy, TimerPhase, TimerType } from 'ontime-types';
|
||||||
import { MILLIS_PER_HOUR, MILLIS_PER_MINUTE, MILLIS_PER_SECOND, dayInMs, millisToString } from 'ontime-utils';
|
import { MILLIS_PER_HOUR, MILLIS_PER_MINUTE, MILLIS_PER_SECOND, dayInMs, millisToString } from 'ontime-utils';
|
||||||
|
|
||||||
import * as timeCore from '../../lib/time-core/timeCore.js';
|
import type { RuntimeState } from '../../stores/runtimeState.js';
|
||||||
import type { InternalRuntimeState, RuntimeState } from '../../stores/runtimeState.js';
|
|
||||||
import {
|
import {
|
||||||
findDayOffset,
|
findDayOffset,
|
||||||
getCurrent,
|
getCurrent,
|
||||||
@@ -20,48 +19,48 @@ const asTimeOfDay = (value: number): RuntimeState['clock'] => value as RuntimeSt
|
|||||||
describe('getElapsed()', () => {
|
describe('getElapsed()', () => {
|
||||||
it('returns active elapsed time from startedAt without add-time adjustments', () => {
|
it('returns active elapsed time from startedAt without add-time adjustments', () => {
|
||||||
const state = {
|
const state = {
|
||||||
_now: timeCore.generateInstanceFromClock(0, 5, 0),
|
clock: 5 * MILLIS_PER_MINUTE,
|
||||||
timer: {
|
timer: {
|
||||||
addedTime: -10 * MILLIS_PER_MINUTE,
|
addedTime: -10 * MILLIS_PER_MINUTE,
|
||||||
current: 15 * MILLIS_PER_MINUTE,
|
current: 15 * MILLIS_PER_MINUTE,
|
||||||
duration: 20 * MILLIS_PER_MINUTE,
|
duration: 20 * MILLIS_PER_MINUTE,
|
||||||
startedAt: timeCore.generateInstanceFromClock(0, 2, 0),
|
startedAt: 2 * MILLIS_PER_MINUTE,
|
||||||
},
|
},
|
||||||
_timer: {
|
_timer: {
|
||||||
pausedAt: null,
|
pausedAt: null,
|
||||||
pausedDuration: 0,
|
pausedDuration: 0,
|
||||||
},
|
},
|
||||||
} as InternalRuntimeState;
|
} as RuntimeState;
|
||||||
|
|
||||||
expect(getElapsed(state)).toBe(3 * MILLIS_PER_MINUTE);
|
expect(getElapsed(state)).toBe(3 * MILLIS_PER_MINUTE);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('subtracts accumulated pause time', () => {
|
it('subtracts accumulated pause time', () => {
|
||||||
const state = {
|
const state = {
|
||||||
_now: timeCore.generateInstanceFromClock(0, 10, 0),
|
clock: 10 * MILLIS_PER_MINUTE,
|
||||||
timer: {
|
timer: {
|
||||||
startedAt: timeCore.generateInstanceFromClock(0, 2, 0),
|
startedAt: 2 * MILLIS_PER_MINUTE,
|
||||||
},
|
},
|
||||||
_timer: {
|
_timer: {
|
||||||
pausedAt: null,
|
pausedAt: null,
|
||||||
pausedDuration: 5 * MILLIS_PER_MINUTE,
|
pausedDuration: 5 * MILLIS_PER_MINUTE,
|
||||||
},
|
},
|
||||||
} as InternalRuntimeState;
|
} as RuntimeState;
|
||||||
|
|
||||||
expect(getElapsed(state)).toBe(3 * MILLIS_PER_MINUTE);
|
expect(getElapsed(state)).toBe(3 * MILLIS_PER_MINUTE);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('uses the current pause start while paused', () => {
|
it('uses the current pause start while paused', () => {
|
||||||
const state = {
|
const state = {
|
||||||
_now: timeCore.generateInstanceFromClock(0, 10, 0),
|
clock: 10 * MILLIS_PER_MINUTE,
|
||||||
timer: {
|
timer: {
|
||||||
startedAt: timeCore.generateInstanceFromClock(0, 2, 0),
|
startedAt: 2 * MILLIS_PER_MINUTE,
|
||||||
},
|
},
|
||||||
_timer: {
|
_timer: {
|
||||||
pausedAt: 7 * MILLIS_PER_MINUTE,
|
pausedAt: 7 * MILLIS_PER_MINUTE,
|
||||||
pausedDuration: 1 * MILLIS_PER_MINUTE,
|
pausedDuration: 1 * MILLIS_PER_MINUTE,
|
||||||
},
|
},
|
||||||
} as InternalRuntimeState;
|
} as RuntimeState;
|
||||||
|
|
||||||
expect(getElapsed(state)).toBe(4 * MILLIS_PER_MINUTE);
|
expect(getElapsed(state)).toBe(4 * MILLIS_PER_MINUTE);
|
||||||
});
|
});
|
||||||
@@ -83,7 +82,7 @@ describe('getExpectedFinish()', () => {
|
|||||||
pausedAt: null,
|
pausedAt: null,
|
||||||
hasFinished: false,
|
hasFinished: false,
|
||||||
},
|
},
|
||||||
} as InternalRuntimeState;
|
} as RuntimeState;
|
||||||
const calculatedFinish = getExpectedFinish(state);
|
const calculatedFinish = getExpectedFinish(state);
|
||||||
expect(calculatedFinish).toBe(null);
|
expect(calculatedFinish).toBe(null);
|
||||||
});
|
});
|
||||||
@@ -96,13 +95,13 @@ describe('getExpectedFinish()', () => {
|
|||||||
timer: {
|
timer: {
|
||||||
addedTime: 0,
|
addedTime: 0,
|
||||||
duration: 10,
|
duration: 10,
|
||||||
startedAt: timeCore.generateInstanceFromClock(0, 0, 0, 10),
|
startedAt: 10,
|
||||||
},
|
},
|
||||||
_timer: {
|
_timer: {
|
||||||
pausedAt: null,
|
pausedAt: null,
|
||||||
hasFinished: true,
|
hasFinished: true,
|
||||||
},
|
},
|
||||||
} as InternalRuntimeState;
|
} as RuntimeState;
|
||||||
const calculatedFinish = getExpectedFinish(state);
|
const calculatedFinish = getExpectedFinish(state);
|
||||||
expect(calculatedFinish).toBe(20);
|
expect(calculatedFinish).toBe(20);
|
||||||
});
|
});
|
||||||
@@ -115,13 +114,13 @@ describe('getExpectedFinish()', () => {
|
|||||||
timer: {
|
timer: {
|
||||||
addedTime: 0,
|
addedTime: 0,
|
||||||
duration: 10,
|
duration: 10,
|
||||||
startedAt: timeCore.generateInstanceFromClock(0, 0, 0, 1),
|
startedAt: 1,
|
||||||
},
|
},
|
||||||
_timer: {
|
_timer: {
|
||||||
pausedAt: null,
|
pausedAt: null,
|
||||||
hasFinished: false,
|
hasFinished: false,
|
||||||
},
|
},
|
||||||
} as InternalRuntimeState;
|
} as RuntimeState;
|
||||||
const calculatedFinish = getExpectedFinish(state);
|
const calculatedFinish = getExpectedFinish(state);
|
||||||
expect(calculatedFinish).toBe(11);
|
expect(calculatedFinish).toBe(11);
|
||||||
});
|
});
|
||||||
@@ -134,13 +133,13 @@ describe('getExpectedFinish()', () => {
|
|||||||
timer: {
|
timer: {
|
||||||
addedTime: 20,
|
addedTime: 20,
|
||||||
duration: 10,
|
duration: 10,
|
||||||
startedAt: timeCore.generateInstanceFromClock(0, 0, 0, 1),
|
startedAt: 1,
|
||||||
},
|
},
|
||||||
_timer: {
|
_timer: {
|
||||||
pausedAt: null,
|
pausedAt: null,
|
||||||
hasFinished: false,
|
hasFinished: false,
|
||||||
},
|
},
|
||||||
} as InternalRuntimeState;
|
} as RuntimeState;
|
||||||
|
|
||||||
const calculatedFinish = getExpectedFinish(state);
|
const calculatedFinish = getExpectedFinish(state);
|
||||||
expect(calculatedFinish).toBe(31);
|
expect(calculatedFinish).toBe(31);
|
||||||
@@ -154,13 +153,13 @@ describe('getExpectedFinish()', () => {
|
|||||||
timer: {
|
timer: {
|
||||||
addedTime: -10,
|
addedTime: -10,
|
||||||
duration: 10,
|
duration: 10,
|
||||||
startedAt: timeCore.generateInstanceFromClock(0, 0, 0, 1),
|
startedAt: 1,
|
||||||
},
|
},
|
||||||
_timer: {
|
_timer: {
|
||||||
pausedAt: null,
|
pausedAt: null,
|
||||||
hasFinished: false,
|
hasFinished: false,
|
||||||
},
|
},
|
||||||
} as InternalRuntimeState;
|
} as RuntimeState;
|
||||||
|
|
||||||
const calculatedFinish = getExpectedFinish(state);
|
const calculatedFinish = getExpectedFinish(state);
|
||||||
expect(calculatedFinish).toBe(1);
|
expect(calculatedFinish).toBe(1);
|
||||||
@@ -174,13 +173,13 @@ describe('getExpectedFinish()', () => {
|
|||||||
timer: {
|
timer: {
|
||||||
addedTime: -100,
|
addedTime: -100,
|
||||||
duration: 10,
|
duration: 10,
|
||||||
startedAt: timeCore.generateInstanceFromClock(0, 0, 0, 1),
|
startedAt: 1,
|
||||||
},
|
},
|
||||||
_timer: {
|
_timer: {
|
||||||
pausedAt: null,
|
pausedAt: null,
|
||||||
hasFinished: false,
|
hasFinished: false,
|
||||||
},
|
},
|
||||||
} as InternalRuntimeState;
|
} as RuntimeState;
|
||||||
|
|
||||||
const calculatedFinish = getExpectedFinish(state);
|
const calculatedFinish = getExpectedFinish(state);
|
||||||
expect(calculatedFinish).toBe(1);
|
expect(calculatedFinish).toBe(1);
|
||||||
@@ -194,13 +193,13 @@ describe('getExpectedFinish()', () => {
|
|||||||
timer: {
|
timer: {
|
||||||
addedTime: 0,
|
addedTime: 0,
|
||||||
duration: 0,
|
duration: 0,
|
||||||
startedAt: timeCore.generateInstanceFromClock(0, 0, 0, 1),
|
startedAt: 1,
|
||||||
},
|
},
|
||||||
_timer: {
|
_timer: {
|
||||||
pausedAt: null,
|
pausedAt: null,
|
||||||
hasFinished: false,
|
hasFinished: false,
|
||||||
},
|
},
|
||||||
} as InternalRuntimeState;
|
} as RuntimeState;
|
||||||
|
|
||||||
const calculatedFinish = getExpectedFinish(state);
|
const calculatedFinish = getExpectedFinish(state);
|
||||||
expect(calculatedFinish).toBe(1);
|
expect(calculatedFinish).toBe(1);
|
||||||
@@ -214,13 +213,13 @@ describe('getExpectedFinish()', () => {
|
|||||||
timer: {
|
timer: {
|
||||||
addedTime: 0,
|
addedTime: 0,
|
||||||
duration: dayInMs,
|
duration: dayInMs,
|
||||||
startedAt: timeCore.generateInstanceFromClock(0, 0, 0, 10),
|
startedAt: 10,
|
||||||
},
|
},
|
||||||
_timer: {
|
_timer: {
|
||||||
pausedAt: null,
|
pausedAt: null,
|
||||||
hasFinished: false,
|
hasFinished: false,
|
||||||
},
|
},
|
||||||
} as InternalRuntimeState;
|
} as RuntimeState;
|
||||||
|
|
||||||
const calculatedFinish = getExpectedFinish(state);
|
const calculatedFinish = getExpectedFinish(state);
|
||||||
expect(calculatedFinish).toBe(10);
|
expect(calculatedFinish).toBe(10);
|
||||||
@@ -235,13 +234,13 @@ describe('getExpectedFinish()', () => {
|
|||||||
timer: {
|
timer: {
|
||||||
addedTime: 10,
|
addedTime: 10,
|
||||||
duration: dayInMs,
|
duration: dayInMs,
|
||||||
startedAt: timeCore.generateInstanceFromClock(0, 0, 0, 10),
|
startedAt: 10,
|
||||||
},
|
},
|
||||||
_timer: {
|
_timer: {
|
||||||
pausedAt: null,
|
pausedAt: null,
|
||||||
hasFinished: false,
|
hasFinished: false,
|
||||||
},
|
},
|
||||||
} as InternalRuntimeState;
|
} as RuntimeState;
|
||||||
|
|
||||||
const calculatedFinish = getExpectedFinish(state);
|
const calculatedFinish = getExpectedFinish(state);
|
||||||
expect(calculatedFinish).toBe(40);
|
expect(calculatedFinish).toBe(40);
|
||||||
@@ -254,7 +253,7 @@ describe('getExpectedFinish()', () => {
|
|||||||
},
|
},
|
||||||
timer: {
|
timer: {
|
||||||
addedTime: 0,
|
addedTime: 0,
|
||||||
startedAt: timeCore.generateInstanceFromClock(22, 0, 0),
|
startedAt: 79200000, // 22:00:00
|
||||||
},
|
},
|
||||||
_timer: {
|
_timer: {
|
||||||
pausedAt: null,
|
pausedAt: null,
|
||||||
@@ -264,7 +263,7 @@ describe('getExpectedFinish()', () => {
|
|||||||
actualStart: 79200000,
|
actualStart: 79200000,
|
||||||
plannedEnd: 600000,
|
plannedEnd: 600000,
|
||||||
},
|
},
|
||||||
} as InternalRuntimeState;
|
} as RuntimeState;
|
||||||
|
|
||||||
const calculatedFinish = getExpectedFinish(state);
|
const calculatedFinish = getExpectedFinish(state);
|
||||||
// expected finish is not a duration but a point in time
|
// expected finish is not a duration but a point in time
|
||||||
@@ -280,7 +279,7 @@ describe('getCurrent()', () => {
|
|||||||
timeEnd: 30,
|
timeEnd: 30,
|
||||||
timerType: TimerType.CountDown,
|
timerType: TimerType.CountDown,
|
||||||
},
|
},
|
||||||
_now: timeCore.generateInstanceFromClock(0, 0, 0),
|
clock: 0,
|
||||||
timer: {
|
timer: {
|
||||||
addedTime: 10,
|
addedTime: 10,
|
||||||
duration: 111, // <-- we take the duration value
|
duration: 111, // <-- we take the duration value
|
||||||
@@ -290,7 +289,7 @@ describe('getCurrent()', () => {
|
|||||||
pausedAt: null,
|
pausedAt: null,
|
||||||
hasFinished: false,
|
hasFinished: false,
|
||||||
},
|
},
|
||||||
} as InternalRuntimeState;
|
} as RuntimeState;
|
||||||
|
|
||||||
const current = getCurrent(state);
|
const current = getCurrent(state);
|
||||||
expect(current).toBe(111);
|
expect(current).toBe(111);
|
||||||
@@ -301,17 +300,17 @@ describe('getCurrent()', () => {
|
|||||||
timeEnd: 10,
|
timeEnd: 10,
|
||||||
timerType: TimerType.CountDown,
|
timerType: TimerType.CountDown,
|
||||||
},
|
},
|
||||||
_now: timeCore.generateInstanceFromClock(0, 0, 0, 1),
|
clock: 1,
|
||||||
timer: {
|
timer: {
|
||||||
addedTime: 0,
|
addedTime: 0,
|
||||||
duration: 10,
|
duration: 10,
|
||||||
startedAt: timeCore.generateInstanceFromClock(0, 0, 0),
|
startedAt: 0,
|
||||||
},
|
},
|
||||||
_timer: {
|
_timer: {
|
||||||
pausedAt: null,
|
pausedAt: null,
|
||||||
hasFinished: false,
|
hasFinished: false,
|
||||||
},
|
},
|
||||||
} as InternalRuntimeState;
|
} as RuntimeState;
|
||||||
|
|
||||||
const current = getCurrent(state);
|
const current = getCurrent(state);
|
||||||
expect(current).toBe(9);
|
expect(current).toBe(9);
|
||||||
@@ -322,17 +321,17 @@ describe('getCurrent()', () => {
|
|||||||
timeEnd: 10,
|
timeEnd: 10,
|
||||||
timerType: TimerType.CountDown,
|
timerType: TimerType.CountDown,
|
||||||
},
|
},
|
||||||
_now: timeCore.generateInstanceFromClock(0, 0, 0, 1),
|
clock: 1,
|
||||||
timer: {
|
timer: {
|
||||||
addedTime: 10,
|
addedTime: 10,
|
||||||
duration: 10,
|
duration: 10,
|
||||||
startedAt: timeCore.generateInstanceFromClock(0, 0, 0),
|
startedAt: 0,
|
||||||
},
|
},
|
||||||
_timer: {
|
_timer: {
|
||||||
pausedAt: null,
|
pausedAt: null,
|
||||||
hasFinished: false,
|
hasFinished: false,
|
||||||
},
|
},
|
||||||
} as InternalRuntimeState;
|
} as RuntimeState;
|
||||||
|
|
||||||
const current = getCurrent(state);
|
const current = getCurrent(state);
|
||||||
expect(current).toBe(19);
|
expect(current).toBe(19);
|
||||||
@@ -343,17 +342,17 @@ describe('getCurrent()', () => {
|
|||||||
timeEnd: 20,
|
timeEnd: 20,
|
||||||
timerType: TimerType.CountDown,
|
timerType: TimerType.CountDown,
|
||||||
},
|
},
|
||||||
_now: timeCore.generateInstanceFromClock(0, 0, 0, 10),
|
clock: 10,
|
||||||
timer: {
|
timer: {
|
||||||
addedTime: 0,
|
addedTime: 0,
|
||||||
duration: dayInMs + 10,
|
duration: dayInMs + 10,
|
||||||
startedAt: timeCore.generateInstanceFromClock(0, 0, 0, 10),
|
startedAt: 10,
|
||||||
},
|
},
|
||||||
_timer: {
|
_timer: {
|
||||||
pausedAt: null,
|
pausedAt: null,
|
||||||
hasFinished: false,
|
hasFinished: false,
|
||||||
},
|
},
|
||||||
} as InternalRuntimeState;
|
} as RuntimeState;
|
||||||
|
|
||||||
const current = getCurrent(state);
|
const current = getCurrent(state);
|
||||||
expect(current).toBe(dayInMs + 10);
|
expect(current).toBe(dayInMs + 10);
|
||||||
@@ -364,17 +363,17 @@ describe('getCurrent()', () => {
|
|||||||
timeEnd: 20,
|
timeEnd: 20,
|
||||||
timerType: TimerType.CountDown,
|
timerType: TimerType.CountDown,
|
||||||
},
|
},
|
||||||
_now: timeCore.generateInstanceFromClock(0, 0, 0, 5),
|
clock: 5,
|
||||||
timer: {
|
timer: {
|
||||||
addedTime: 0,
|
addedTime: 0,
|
||||||
duration: dayInMs + 10,
|
duration: dayInMs + 10,
|
||||||
startedAt: timeCore.generateInstanceFromClock(0, 0, 0, 10),
|
startedAt: 10,
|
||||||
},
|
},
|
||||||
_timer: {
|
_timer: {
|
||||||
pausedAt: null,
|
pausedAt: null,
|
||||||
hasFinished: false,
|
hasFinished: false,
|
||||||
},
|
},
|
||||||
} as InternalRuntimeState;
|
} as RuntimeState;
|
||||||
|
|
||||||
const current = getCurrent(state);
|
const current = getCurrent(state);
|
||||||
expect(current).toBe(15);
|
expect(current).toBe(15);
|
||||||
@@ -385,17 +384,17 @@ describe('getCurrent()', () => {
|
|||||||
timeEnd: 20,
|
timeEnd: 20,
|
||||||
timerType: TimerType.CountDown,
|
timerType: TimerType.CountDown,
|
||||||
},
|
},
|
||||||
_now: timeCore.generateInstanceFromClock(0, 0, 0, 5),
|
clock: 5,
|
||||||
timer: {
|
timer: {
|
||||||
addedTime: 20,
|
addedTime: 20,
|
||||||
duration: dayInMs + 10,
|
duration: dayInMs + 10,
|
||||||
startedAt: timeCore.generateInstanceFromClock(0, 0, 0, 10),
|
startedAt: 10,
|
||||||
},
|
},
|
||||||
_timer: {
|
_timer: {
|
||||||
pausedAt: null,
|
pausedAt: null,
|
||||||
hasFinished: false,
|
hasFinished: false,
|
||||||
},
|
},
|
||||||
} as InternalRuntimeState;
|
} as RuntimeState;
|
||||||
|
|
||||||
const current = getCurrent(state);
|
const current = getCurrent(state);
|
||||||
expect(current).toBe(35);
|
expect(current).toBe(35);
|
||||||
@@ -408,7 +407,7 @@ describe('getCurrent()', () => {
|
|||||||
timeEnd: 100,
|
timeEnd: 100,
|
||||||
countToEnd: true,
|
countToEnd: true,
|
||||||
},
|
},
|
||||||
_now: timeCore.generateInstanceFromClock(0, 0, 0, 30),
|
clock: 30,
|
||||||
timer: {
|
timer: {
|
||||||
addedTime: 0,
|
addedTime: 0,
|
||||||
duration: 100,
|
duration: 100,
|
||||||
@@ -421,7 +420,7 @@ describe('getCurrent()', () => {
|
|||||||
pausedAt: null,
|
pausedAt: null,
|
||||||
hasFinished: false,
|
hasFinished: false,
|
||||||
},
|
},
|
||||||
} as InternalRuntimeState;
|
} as RuntimeState;
|
||||||
|
|
||||||
const current = getCurrent(state);
|
const current = getCurrent(state);
|
||||||
expect(current).toBe(70);
|
expect(current).toBe(70);
|
||||||
@@ -433,11 +432,11 @@ describe('getCurrent()', () => {
|
|||||||
timeEnd: 100,
|
timeEnd: 100,
|
||||||
countToEnd: true,
|
countToEnd: true,
|
||||||
},
|
},
|
||||||
_now: timeCore.generateInstanceFromClock(0, 0, 0, 30),
|
clock: 30,
|
||||||
timer: {
|
timer: {
|
||||||
addedTime: 0,
|
addedTime: 0,
|
||||||
duration: 100,
|
duration: 100,
|
||||||
startedAt: timeCore.generateInstanceFromClock(0, 0, 0, 10),
|
startedAt: 10,
|
||||||
},
|
},
|
||||||
rundown: {
|
rundown: {
|
||||||
plannedEnd: 100,
|
plannedEnd: 100,
|
||||||
@@ -446,7 +445,7 @@ describe('getCurrent()', () => {
|
|||||||
pausedAt: null,
|
pausedAt: null,
|
||||||
hasFinished: false,
|
hasFinished: false,
|
||||||
},
|
},
|
||||||
} as InternalRuntimeState;
|
} as RuntimeState;
|
||||||
|
|
||||||
const current = getCurrent(state);
|
const current = getCurrent(state);
|
||||||
expect(current).toBe(70);
|
expect(current).toBe(70);
|
||||||
@@ -458,11 +457,11 @@ describe('getCurrent()', () => {
|
|||||||
timeEnd: 100,
|
timeEnd: 100,
|
||||||
countToEnd: true,
|
countToEnd: true,
|
||||||
},
|
},
|
||||||
_now: timeCore.generateInstanceFromClock(0, 0, 0, 30),
|
clock: 30,
|
||||||
timer: {
|
timer: {
|
||||||
addedTime: 7,
|
addedTime: 7,
|
||||||
duration: 100,
|
duration: 100,
|
||||||
startedAt: timeCore.generateInstanceFromClock(0, 0, 0, 10),
|
startedAt: 10,
|
||||||
},
|
},
|
||||||
rundown: {
|
rundown: {
|
||||||
plannedEnd: 100,
|
plannedEnd: 100,
|
||||||
@@ -471,7 +470,7 @@ describe('getCurrent()', () => {
|
|||||||
pausedAt: null,
|
pausedAt: null,
|
||||||
hasFinished: false,
|
hasFinished: false,
|
||||||
},
|
},
|
||||||
} as InternalRuntimeState;
|
} as RuntimeState;
|
||||||
|
|
||||||
const current = getCurrent(state);
|
const current = getCurrent(state);
|
||||||
expect(current).toBe(77);
|
expect(current).toBe(77);
|
||||||
@@ -484,11 +483,11 @@ describe('getCurrent()', () => {
|
|||||||
timeEnd: 600000, // 00:10:00
|
timeEnd: 600000, // 00:10:00
|
||||||
countToEnd: true,
|
countToEnd: true,
|
||||||
},
|
},
|
||||||
_now: timeCore.generateInstanceFromClock(22, 5, 0),
|
clock: 79500000, // 22:05:00
|
||||||
timer: {
|
timer: {
|
||||||
addedTime: 0,
|
addedTime: 0,
|
||||||
duration: Infinity, // not relevant,
|
duration: Infinity, // not relevant,
|
||||||
startedAt: timeCore.generateInstanceFromClock(22, 0, 0),
|
startedAt: 79200000, // 22:00:00
|
||||||
},
|
},
|
||||||
rundown: {
|
rundown: {
|
||||||
actualStart: 79200000,
|
actualStart: 79200000,
|
||||||
@@ -498,7 +497,7 @@ describe('getCurrent()', () => {
|
|||||||
pausedAt: null,
|
pausedAt: null,
|
||||||
hasFinished: false,
|
hasFinished: false,
|
||||||
},
|
},
|
||||||
} as InternalRuntimeState;
|
} as RuntimeState;
|
||||||
|
|
||||||
const current = getCurrent(state);
|
const current = getCurrent(state);
|
||||||
expect(current).toBe(dayInMs - 79500000 + 600000);
|
expect(current).toBe(dayInMs - 79500000 + 600000);
|
||||||
@@ -506,7 +505,7 @@ describe('getCurrent()', () => {
|
|||||||
|
|
||||||
it('handles events that were started late', () => {
|
it('handles events that were started late', () => {
|
||||||
const state = {
|
const state = {
|
||||||
_now: timeCore.generateInstanceFromClock(22, 46, 40), // 22:46:40 <--- starting 16 min after the scheduled end
|
clock: 82000000, // 22:46:40 <--- starting 16 min after the scheduled end
|
||||||
eventNow: {
|
eventNow: {
|
||||||
timeStart: 77400000, // 21:30:00
|
timeStart: 77400000, // 21:30:00
|
||||||
timeEnd: 81000000, // 22:30:00
|
timeEnd: 81000000, // 22:30:00
|
||||||
@@ -516,7 +515,7 @@ describe('getCurrent()', () => {
|
|||||||
timer: {
|
timer: {
|
||||||
addedTime: 0,
|
addedTime: 0,
|
||||||
duration: Infinity, // not relevant,
|
duration: Infinity, // not relevant,
|
||||||
startedAt: timeCore.generateInstanceFromClock(22, 0, 0),
|
startedAt: 79200000, // 22:00:00
|
||||||
},
|
},
|
||||||
rundown: {
|
rundown: {
|
||||||
actualStart: 82000000, // 22:46:40 <--- started now
|
actualStart: 82000000, // 22:46:40 <--- started now
|
||||||
@@ -526,7 +525,7 @@ describe('getCurrent()', () => {
|
|||||||
pausedAt: null,
|
pausedAt: null,
|
||||||
hasFinished: false,
|
hasFinished: false,
|
||||||
},
|
},
|
||||||
} as InternalRuntimeState;
|
} as RuntimeState;
|
||||||
|
|
||||||
const current = getCurrent(state);
|
const current = getCurrent(state);
|
||||||
expect(current).toBe(81000000 - 82000000); // <-- planned end - now
|
expect(current).toBe(81000000 - 82000000); // <-- planned end - now
|
||||||
@@ -542,17 +541,17 @@ describe('getExpectedFinish() and getCurrentTime() combined', () => {
|
|||||||
timeEnd: 10,
|
timeEnd: 10,
|
||||||
timerType: TimerType.CountDown,
|
timerType: TimerType.CountDown,
|
||||||
},
|
},
|
||||||
_now: timeCore.generateInstanceFromClock(0, 0, 0),
|
clock: 0,
|
||||||
timer: {
|
timer: {
|
||||||
addedTime: 0,
|
addedTime: 0,
|
||||||
duration,
|
duration,
|
||||||
startedAt: timeCore.generateInstanceFromClock(0, 0, 0),
|
startedAt: 0,
|
||||||
},
|
},
|
||||||
_timer: {
|
_timer: {
|
||||||
pausedAt: null,
|
pausedAt: null,
|
||||||
hasFinished: false,
|
hasFinished: false,
|
||||||
},
|
},
|
||||||
} as InternalRuntimeState;
|
} as RuntimeState;
|
||||||
|
|
||||||
const expectedFinish = getExpectedFinish(state);
|
const expectedFinish = getExpectedFinish(state);
|
||||||
const current = getCurrent(state);
|
const current = getCurrent(state);
|
||||||
@@ -570,25 +569,25 @@ describe('getExpectedFinish() and getCurrentTime() combined', () => {
|
|||||||
timeEnd: 10,
|
timeEnd: 10,
|
||||||
timerType: TimerType.CountDown,
|
timerType: TimerType.CountDown,
|
||||||
},
|
},
|
||||||
_now: timeCore.generateInstanceFromClock(0, 0, 0, 5),
|
clock: 5,
|
||||||
timer: {
|
timer: {
|
||||||
addedTime: 3,
|
addedTime: 3,
|
||||||
duration,
|
duration,
|
||||||
startedAt: timeCore.generateInstanceFromClock(0, 0, 0),
|
startedAt: 0,
|
||||||
},
|
},
|
||||||
_timer: {
|
_timer: {
|
||||||
pausedAt: null,
|
pausedAt: null,
|
||||||
hasFinished: false,
|
hasFinished: false,
|
||||||
},
|
},
|
||||||
} as InternalRuntimeState;
|
} as RuntimeState;
|
||||||
|
|
||||||
const expectedFinish = getExpectedFinish(state);
|
const expectedFinish = getExpectedFinish(state);
|
||||||
const current = getCurrent(state);
|
const current = getCurrent(state);
|
||||||
|
|
||||||
const elapsed = duration - current;
|
const elapsed = duration - current;
|
||||||
expect(expectedFinish).toBe(13);
|
expect(expectedFinish).toBe(13);
|
||||||
expect(current).toBe(8);
|
|
||||||
expect(elapsed).toBe(2);
|
expect(elapsed).toBe(2);
|
||||||
|
expect(current).toBe(8);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -872,7 +871,7 @@ describe('getRuntimeOffset()', () => {
|
|||||||
dayOffset: 0,
|
dayOffset: 0,
|
||||||
},
|
},
|
||||||
timer: {
|
timer: {
|
||||||
startedAt: timeCore.generateInstanceFromClock(0, 0, 0, 150),
|
startedAt: 150,
|
||||||
addedTime: 0,
|
addedTime: 0,
|
||||||
current: 0,
|
current: 0,
|
||||||
},
|
},
|
||||||
@@ -884,9 +883,9 @@ describe('getRuntimeOffset()', () => {
|
|||||||
plannedStart: 100,
|
plannedStart: 100,
|
||||||
currentDay: 0,
|
currentDay: 0,
|
||||||
},
|
},
|
||||||
_now: timeCore.generateInstanceFromClock(0, 0, 0, 150),
|
clock: 150,
|
||||||
_startDayOffset: 0,
|
_startDayOffset: 0,
|
||||||
} as InternalRuntimeState;
|
} as RuntimeState;
|
||||||
|
|
||||||
const { absolute } = getRuntimeOffset(state);
|
const { absolute } = getRuntimeOffset(state);
|
||||||
expect(absolute).toBe(50);
|
expect(absolute).toBe(50);
|
||||||
@@ -900,7 +899,7 @@ describe('getRuntimeOffset()', () => {
|
|||||||
dayOffset: 0,
|
dayOffset: 0,
|
||||||
},
|
},
|
||||||
timer: {
|
timer: {
|
||||||
startedAt: timeCore.generateInstanceFromClock(0, 0, 0, 150), // we started 50ms delayed
|
startedAt: 150, // we started 50ms delayed
|
||||||
addedTime: 10, // we compensated with 10ms
|
addedTime: 10, // we compensated with 10ms
|
||||||
current: 10, // we are 10ms into the timer
|
current: 10, // we are 10ms into the timer
|
||||||
},
|
},
|
||||||
@@ -913,7 +912,7 @@ describe('getRuntimeOffset()', () => {
|
|||||||
currentDay: 0,
|
currentDay: 0,
|
||||||
},
|
},
|
||||||
_startDayOffset: 0,
|
_startDayOffset: 0,
|
||||||
} as InternalRuntimeState;
|
} as RuntimeState;
|
||||||
|
|
||||||
const { absolute } = getRuntimeOffset(state);
|
const { absolute } = getRuntimeOffset(state);
|
||||||
expect(absolute).toBe(60);
|
expect(absolute).toBe(60);
|
||||||
@@ -928,7 +927,7 @@ describe('getRuntimeOffset()', () => {
|
|||||||
dayOffset: 0,
|
dayOffset: 0,
|
||||||
},
|
},
|
||||||
timer: {
|
timer: {
|
||||||
startedAt: timeCore.generateInstanceFromClock(0, 0, 0, 100), // we started ontime
|
startedAt: 100, // we started ontime
|
||||||
current: -10, // we are 10 seconds over
|
current: -10, // we are 10 seconds over
|
||||||
addedTime: 0, // we have not compensated with added time
|
addedTime: 0, // we have not compensated with added time
|
||||||
},
|
},
|
||||||
@@ -941,7 +940,7 @@ describe('getRuntimeOffset()', () => {
|
|||||||
currentDay: 0,
|
currentDay: 0,
|
||||||
},
|
},
|
||||||
_startDayOffset: 0,
|
_startDayOffset: 0,
|
||||||
} as InternalRuntimeState;
|
} as RuntimeState;
|
||||||
|
|
||||||
const { absolute } = getRuntimeOffset(state);
|
const { absolute } = getRuntimeOffset(state);
|
||||||
expect(absolute).toBe(10);
|
expect(absolute).toBe(10);
|
||||||
@@ -955,9 +954,9 @@ describe('getRuntimeOffset()', () => {
|
|||||||
timeEnd: 150,
|
timeEnd: 150,
|
||||||
dayOffset: 0,
|
dayOffset: 0,
|
||||||
},
|
},
|
||||||
_now: timeCore.generateInstanceFromClock(0, 0, 0, 150),
|
clock: 150,
|
||||||
timer: {
|
timer: {
|
||||||
startedAt: timeCore.generateInstanceFromClock(0, 0, 0, 100), // started on time
|
startedAt: 100, // started on time
|
||||||
current: 25, // are 25ms into it
|
current: 25, // are 25ms into it
|
||||||
addedTime: 0,
|
addedTime: 0,
|
||||||
},
|
},
|
||||||
@@ -970,7 +969,7 @@ describe('getRuntimeOffset()', () => {
|
|||||||
currentDay: 0,
|
currentDay: 0,
|
||||||
},
|
},
|
||||||
_startDayOffset: 0,
|
_startDayOffset: 0,
|
||||||
} as InternalRuntimeState;
|
} as RuntimeState;
|
||||||
|
|
||||||
const { absolute } = getRuntimeOffset(state);
|
const { absolute } = getRuntimeOffset(state);
|
||||||
expect(absolute).toBe(25);
|
expect(absolute).toBe(25);
|
||||||
@@ -978,7 +977,7 @@ describe('getRuntimeOffset()', () => {
|
|||||||
|
|
||||||
it('offset doesnt exist if we havent started', () => {
|
it('offset doesnt exist if we havent started', () => {
|
||||||
const state = {
|
const state = {
|
||||||
_now: timeCore.generateInstanceFromClock(22, 48, 0), // 78480789
|
clock: 78480789,
|
||||||
eventNow: {
|
eventNow: {
|
||||||
id: 'd6a2ce',
|
id: 'd6a2ce',
|
||||||
timeStart: 77400000,
|
timeStart: 77400000,
|
||||||
@@ -1012,7 +1011,7 @@ describe('getRuntimeOffset()', () => {
|
|||||||
},
|
},
|
||||||
_startDayOffset: 0,
|
_startDayOffset: 0,
|
||||||
_timer: { pausedAt: null },
|
_timer: { pausedAt: null },
|
||||||
} as InternalRuntimeState;
|
} as RuntimeState;
|
||||||
|
|
||||||
const { absolute } = getRuntimeOffset(state);
|
const { absolute } = getRuntimeOffset(state);
|
||||||
expect(absolute).toBe(0);
|
expect(absolute).toBe(0);
|
||||||
@@ -1020,7 +1019,7 @@ describe('getRuntimeOffset()', () => {
|
|||||||
|
|
||||||
it('with time-to-end, offsets dont exist if we are not in overtime', () => {
|
it('with time-to-end, offsets dont exist if we are not in overtime', () => {
|
||||||
const state = {
|
const state = {
|
||||||
_now: timeCore.generateInstanceFromClock(22, 13, 20),
|
clock: 80000000, // 22:13:20
|
||||||
eventNow: {
|
eventNow: {
|
||||||
id: 'd6a2ce',
|
id: 'd6a2ce',
|
||||||
type: 'event',
|
type: 'event',
|
||||||
@@ -1064,11 +1063,11 @@ describe('getRuntimeOffset()', () => {
|
|||||||
expectedFinish: 81600000,
|
expectedFinish: 81600000,
|
||||||
playback: Playback.Play,
|
playback: Playback.Play,
|
||||||
secondaryTimer: null,
|
secondaryTimer: null,
|
||||||
startedAt: timeCore.generateInstanceFromClock(21, 40, 0),
|
startedAt: 78000000,
|
||||||
},
|
},
|
||||||
_startDayOffset: 0,
|
_startDayOffset: 0,
|
||||||
_timer: { pausedAt: null },
|
_timer: { pausedAt: null },
|
||||||
} as InternalRuntimeState;
|
} as RuntimeState;
|
||||||
|
|
||||||
const { absolute } = getRuntimeOffset(state);
|
const { absolute } = getRuntimeOffset(state);
|
||||||
expect(absolute).toBe(0);
|
expect(absolute).toBe(0);
|
||||||
@@ -1076,7 +1075,7 @@ describe('getRuntimeOffset()', () => {
|
|||||||
|
|
||||||
it('with time-to-end, offset is the overtime', () => {
|
it('with time-to-end, offset is the overtime', () => {
|
||||||
const state = {
|
const state = {
|
||||||
_now: timeCore.generateInstanceFromClock(22, 46, 40),
|
clock: 82000000, // 22:46:40
|
||||||
eventNow: {
|
eventNow: {
|
||||||
id: 'd6a2ce',
|
id: 'd6a2ce',
|
||||||
type: 'event',
|
type: 'event',
|
||||||
@@ -1120,11 +1119,11 @@ describe('getRuntimeOffset()', () => {
|
|||||||
expectedFinish: 81600000,
|
expectedFinish: 81600000,
|
||||||
playback: Playback.Play,
|
playback: Playback.Play,
|
||||||
secondaryTimer: null,
|
secondaryTimer: null,
|
||||||
startedAt: timeCore.generateInstanceFromClock(21, 40, 0),
|
startedAt: 78000000,
|
||||||
},
|
},
|
||||||
_startDayOffset: 0,
|
_startDayOffset: 0,
|
||||||
_timer: { pausedAt: null },
|
_timer: { pausedAt: null },
|
||||||
} as InternalRuntimeState;
|
} as RuntimeState;
|
||||||
|
|
||||||
const { absolute } = getRuntimeOffset(state);
|
const { absolute } = getRuntimeOffset(state);
|
||||||
expect(absolute).toBe(400000 - 200000); // <--- offset is always the overtime + added time
|
expect(absolute).toBe(400000 - 200000); // <--- offset is always the overtime + added time
|
||||||
@@ -1132,7 +1131,7 @@ describe('getRuntimeOffset()', () => {
|
|||||||
|
|
||||||
it('handles time-to-end started after the end time', () => {
|
it('handles time-to-end started after the end time', () => {
|
||||||
const state = {
|
const state = {
|
||||||
_now: timeCore.generateInstanceFromClock(22, 46, 40), // 22:46:40 <--- starting 16m 40s after the scheduled end
|
clock: 82000000, // 22:46:40 <--- starting 16m 40s after the scheduled end
|
||||||
eventNow: {
|
eventNow: {
|
||||||
id: 'd6a2ce',
|
id: 'd6a2ce',
|
||||||
timeStart: 77400000, // 21:30:00
|
timeStart: 77400000, // 21:30:00
|
||||||
@@ -1165,14 +1164,14 @@ describe('getRuntimeOffset()', () => {
|
|||||||
expectedFinish: 82000000 + 3600000, // <--- now + duration
|
expectedFinish: 82000000 + 3600000, // <--- now + duration
|
||||||
playback: Playback.Play,
|
playback: Playback.Play,
|
||||||
secondaryTimer: null,
|
secondaryTimer: null,
|
||||||
startedAt: timeCore.generateInstanceFromClock(22, 46, 40),
|
startedAt: 82000000, // <--- started now
|
||||||
},
|
},
|
||||||
_startDayOffset: 0,
|
_startDayOffset: 0,
|
||||||
_timer: { pausedAt: null },
|
_timer: { pausedAt: null },
|
||||||
} as InternalRuntimeState;
|
} as RuntimeState;
|
||||||
|
|
||||||
const updateCurrent = getCurrent(state);
|
const updateCurrent = getCurrent(state);
|
||||||
state.timer.current = updateCurrent as Duration;
|
state.timer.current = updateCurrent;
|
||||||
const { absolute } = getRuntimeOffset(state);
|
const { absolute } = getRuntimeOffset(state);
|
||||||
expect(millisToString(absolute)).toBe('00:16:40');
|
expect(millisToString(absolute)).toBe('00:16:40');
|
||||||
expect(absolute).toBe(82000000 - 81000000); // <-- now - planned end
|
expect(absolute).toBe(82000000 - 81000000); // <-- now - planned end
|
||||||
@@ -1188,7 +1187,7 @@ describe('getRuntimeOffset() relative', () => {
|
|||||||
dayOffset: 0,
|
dayOffset: 0,
|
||||||
},
|
},
|
||||||
timer: {
|
timer: {
|
||||||
startedAt: timeCore.generateInstanceFromClock(0, 0, 0, 150),
|
startedAt: 150,
|
||||||
addedTime: 0,
|
addedTime: 0,
|
||||||
current: 0,
|
current: 0,
|
||||||
},
|
},
|
||||||
@@ -1201,7 +1200,7 @@ describe('getRuntimeOffset() relative', () => {
|
|||||||
currentDay: 0,
|
currentDay: 0,
|
||||||
},
|
},
|
||||||
_startDayOffset: 0,
|
_startDayOffset: 0,
|
||||||
} as InternalRuntimeState;
|
} as RuntimeState;
|
||||||
|
|
||||||
const { absolute, relative } = getRuntimeOffset(state);
|
const { absolute, relative } = getRuntimeOffset(state);
|
||||||
expect(absolute).toBe(0);
|
expect(absolute).toBe(0);
|
||||||
@@ -1215,7 +1214,7 @@ describe('getRuntimeOffset() relative', () => {
|
|||||||
dayOffset: 0,
|
dayOffset: 0,
|
||||||
},
|
},
|
||||||
timer: {
|
timer: {
|
||||||
startedAt: timeCore.generateInstanceFromClock(0, 0, 0, 150),
|
startedAt: 150,
|
||||||
addedTime: 0,
|
addedTime: 0,
|
||||||
current: 0,
|
current: 0,
|
||||||
},
|
},
|
||||||
@@ -1228,7 +1227,7 @@ describe('getRuntimeOffset() relative', () => {
|
|||||||
currentDay: 0,
|
currentDay: 0,
|
||||||
},
|
},
|
||||||
_startDayOffset: 0,
|
_startDayOffset: 0,
|
||||||
} as InternalRuntimeState;
|
} as RuntimeState;
|
||||||
|
|
||||||
const { absolute, relative } = getRuntimeOffset(state);
|
const { absolute, relative } = getRuntimeOffset(state);
|
||||||
expect(absolute).toBe(50);
|
expect(absolute).toBe(50);
|
||||||
@@ -1242,7 +1241,7 @@ describe('getRuntimeOffset() relative', () => {
|
|||||||
dayOffset: 0,
|
dayOffset: 0,
|
||||||
},
|
},
|
||||||
timer: {
|
timer: {
|
||||||
startedAt: timeCore.generateInstanceFromClock(0, 0, 0, 100),
|
startedAt: 100,
|
||||||
addedTime: 0,
|
addedTime: 0,
|
||||||
current: 0,
|
current: 0,
|
||||||
},
|
},
|
||||||
@@ -1255,7 +1254,7 @@ describe('getRuntimeOffset() relative', () => {
|
|||||||
currentDay: 0,
|
currentDay: 0,
|
||||||
},
|
},
|
||||||
_startDayOffset: 0,
|
_startDayOffset: 0,
|
||||||
} as InternalRuntimeState;
|
} as RuntimeState;
|
||||||
|
|
||||||
const { absolute, relative } = getRuntimeOffset(state);
|
const { absolute, relative } = getRuntimeOffset(state);
|
||||||
expect(absolute).toBe(-50);
|
expect(absolute).toBe(-50);
|
||||||
@@ -1277,7 +1276,7 @@ describe('getTimerPhase()', () => {
|
|||||||
secondaryTimer: null,
|
secondaryTimer: null,
|
||||||
startedAt: null,
|
startedAt: null,
|
||||||
},
|
},
|
||||||
} as InternalRuntimeState;
|
} as RuntimeState;
|
||||||
|
|
||||||
const phase = getTimerPhase(state);
|
const phase = getTimerPhase(state);
|
||||||
expect(phase).toBe(TimerPhase.None);
|
expect(phase).toBe(TimerPhase.None);
|
||||||
@@ -1295,7 +1294,7 @@ describe('getTimerPhase()', () => {
|
|||||||
timeDanger: 100,
|
timeDanger: 100,
|
||||||
timeWarning: 200,
|
timeWarning: 200,
|
||||||
},
|
},
|
||||||
} as InternalRuntimeState;
|
} as RuntimeState;
|
||||||
|
|
||||||
const phase = getTimerPhase(state);
|
const phase = getTimerPhase(state);
|
||||||
expect(phase).toBe(TimerPhase.Overtime);
|
expect(phase).toBe(TimerPhase.Overtime);
|
||||||
@@ -1313,7 +1312,7 @@ describe('getTimerPhase()', () => {
|
|||||||
timeDanger: 100,
|
timeDanger: 100,
|
||||||
timeWarning: 200,
|
timeWarning: 200,
|
||||||
},
|
},
|
||||||
} as InternalRuntimeState;
|
} as RuntimeState;
|
||||||
|
|
||||||
const phase = getTimerPhase(state);
|
const phase = getTimerPhase(state);
|
||||||
expect(phase).toBe(TimerPhase.Danger);
|
expect(phase).toBe(TimerPhase.Danger);
|
||||||
@@ -1331,7 +1330,7 @@ describe('getTimerPhase()', () => {
|
|||||||
timeDanger: 100,
|
timeDanger: 100,
|
||||||
timeWarning: 200,
|
timeWarning: 200,
|
||||||
},
|
},
|
||||||
} as InternalRuntimeState;
|
} as RuntimeState;
|
||||||
|
|
||||||
const phase = getTimerPhase(state);
|
const phase = getTimerPhase(state);
|
||||||
expect(phase).toBe(TimerPhase.Warning);
|
expect(phase).toBe(TimerPhase.Warning);
|
||||||
@@ -1349,7 +1348,7 @@ describe('getTimerPhase()', () => {
|
|||||||
timeDanger: 100,
|
timeDanger: 100,
|
||||||
timeWarning: 200,
|
timeWarning: 200,
|
||||||
},
|
},
|
||||||
} as InternalRuntimeState;
|
} as RuntimeState;
|
||||||
|
|
||||||
const phase = getTimerPhase(state);
|
const phase = getTimerPhase(state);
|
||||||
expect(phase).toBe(TimerPhase.Default);
|
expect(phase).toBe(TimerPhase.Default);
|
||||||
@@ -1357,7 +1356,7 @@ describe('getTimerPhase()', () => {
|
|||||||
|
|
||||||
it('#1042 identifies waiting to roll', () => {
|
it('#1042 identifies waiting to roll', () => {
|
||||||
const state = {
|
const state = {
|
||||||
_now: timeCore.generateInstanceFromClock(16, 28, 11), //55691050
|
clock: 55691050,
|
||||||
eventNow: null,
|
eventNow: null,
|
||||||
eventNext: null,
|
eventNext: null,
|
||||||
rundown: {
|
rundown: {
|
||||||
@@ -1383,13 +1382,13 @@ describe('getTimerPhase()', () => {
|
|||||||
startedAt: null,
|
startedAt: null,
|
||||||
},
|
},
|
||||||
_timer: {
|
_timer: {
|
||||||
forceFinish: false,
|
forceFinish: null,
|
||||||
pausedAt: null,
|
pausedAt: null,
|
||||||
},
|
},
|
||||||
_rundown: {
|
_rundown: {
|
||||||
totalDelay: 0,
|
totalDelay: 0,
|
||||||
},
|
},
|
||||||
} as InternalRuntimeState;
|
} as RuntimeState;
|
||||||
|
|
||||||
const phase = getTimerPhase(state);
|
const phase = getTimerPhase(state);
|
||||||
expect(phase).toBe(TimerPhase.Pending);
|
expect(phase).toBe(TimerPhase.Pending);
|
||||||
@@ -1397,7 +1396,7 @@ describe('getTimerPhase()', () => {
|
|||||||
|
|
||||||
it('#1042 identifies waiting to roll', () => {
|
it('#1042 identifies waiting to roll', () => {
|
||||||
const state = {
|
const state = {
|
||||||
_now: timeCore.generateInstanceFromClock(16, 28, 11), //55691050
|
clock: 55691050,
|
||||||
eventNow: null,
|
eventNow: null,
|
||||||
eventNext: null,
|
eventNext: null,
|
||||||
rundown: {
|
rundown: {
|
||||||
@@ -1423,13 +1422,13 @@ describe('getTimerPhase()', () => {
|
|||||||
startedAt: null,
|
startedAt: null,
|
||||||
},
|
},
|
||||||
_timer: {
|
_timer: {
|
||||||
forceFinish: false,
|
forceFinish: null,
|
||||||
pausedAt: null,
|
pausedAt: null,
|
||||||
},
|
},
|
||||||
_rundown: {
|
_rundown: {
|
||||||
totalDelay: 0,
|
totalDelay: 0,
|
||||||
},
|
},
|
||||||
} as InternalRuntimeState;
|
} as RuntimeState;
|
||||||
|
|
||||||
const phase = getTimerPhase(state);
|
const phase = getTimerPhase(state);
|
||||||
expect(phase).toBe(TimerPhase.Pending);
|
expect(phase).toBe(TimerPhase.Pending);
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
import { copyFile } from 'fs/promises';
|
import { copyFile } from 'fs/promises';
|
||||||
import { join } from 'path';
|
import { join } from 'path';
|
||||||
|
|
||||||
import { DatabaseModel, LogOrigin, ProjectFileListResponse } from 'ontime-types';
|
import { DatabaseModel, LogOrigin, ProjectFileListResponse, RefetchKey } from 'ontime-types';
|
||||||
import { getErrorMessage, getFirstRundown } from 'ontime-utils';
|
import { getErrorMessage, getFirstRundown } from 'ontime-utils';
|
||||||
|
|
||||||
|
import { sendRefetch } from '../../adapters/WebsocketAdapter.js';
|
||||||
import { parseCustomFields } from '../../api-data/custom-fields/customFields.parser.js';
|
import { parseCustomFields } from '../../api-data/custom-fields/customFields.parser.js';
|
||||||
import { parseDatabaseModel } from '../../api-data/db/db.parser.js';
|
import { parseDatabaseModel } from '../../api-data/db/db.parser.js';
|
||||||
import { getCurrentRundown } from '../../api-data/rundown/rundown.dao.js';
|
import { getCurrentRundown } from '../../api-data/rundown/rundown.dao.js';
|
||||||
@@ -105,6 +106,9 @@ async function loadProject(projectData: DatabaseModel, fileName: string, rundown
|
|||||||
currentProjectName: fileName,
|
currentProjectName: fileName,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
setImmediate(() => {
|
||||||
|
sendRefetch(RefetchKey.ProjectFiles);
|
||||||
|
});
|
||||||
return fileName;
|
return fileName;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -263,6 +267,10 @@ export async function duplicateProjectFile(originalFile: string, newFilename: st
|
|||||||
|
|
||||||
const pathToDuplicate = getPathToProject(newFilename);
|
const pathToDuplicate = getPathToProject(newFilename);
|
||||||
await copyFile(projectFilePath, pathToDuplicate);
|
await copyFile(projectFilePath, pathToDuplicate);
|
||||||
|
|
||||||
|
setImmediate(() => {
|
||||||
|
sendRefetch(RefetchKey.ProjectFiles);
|
||||||
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -293,6 +301,10 @@ export async function renameProjectFile(originalFile: string, newFilename: strin
|
|||||||
const newFileName = await loadProject(projectData.data, newFilename);
|
const newFileName = await loadProject(projectData.data, newFilename);
|
||||||
return newFileName;
|
return newFileName;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
setImmediate(() => {
|
||||||
|
sendRefetch(RefetchKey.ProjectFiles);
|
||||||
|
});
|
||||||
return newFilename;
|
return newFilename;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -332,6 +344,9 @@ export async function deleteProjectFile(filename: string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
await deleteFile(projectFilePath);
|
await deleteFile(projectFilePath);
|
||||||
|
setImmediate(() => {
|
||||||
|
sendRefetch(RefetchKey.ProjectFiles);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -374,6 +389,7 @@ export async function patchCurrentProject(data: Partial<DatabaseModel>) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const updatedData = await getDataProvider().getData();
|
const updatedData = getDataProvider().getData();
|
||||||
|
|
||||||
return updatedData;
|
return updatedData;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,8 +5,6 @@ import { RundownMetadata } from '../api-data/rundown/rundown.types.js';
|
|||||||
import { getTimedIndexFromPlayableIndex } from '../api-data/rundown/rundown.utils.js';
|
import { getTimedIndexFromPlayableIndex } from '../api-data/rundown/rundown.utils.js';
|
||||||
import { normaliseEndTime } from './timerUtils.js';
|
import { normaliseEndTime } from './timerUtils.js';
|
||||||
|
|
||||||
// import * as timeCore from '../lib/time-core/timeCore.js';
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Finds current event in a rolling rundown
|
* Finds current event in a rolling rundown
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -760,17 +760,16 @@ function broadcastResult(_target: any, _propertyKey: string, descriptor: Propert
|
|||||||
|
|
||||||
// save the restore state
|
// save the restore state
|
||||||
if (hasImmediateChanges) {
|
if (hasImmediateChanges) {
|
||||||
const internalState = runtimeState.getInternalState();
|
|
||||||
restoreService
|
restoreService
|
||||||
.save({
|
.save({
|
||||||
playback: state.timer.playback,
|
playback: state.timer.playback,
|
||||||
selectedEventId: state.eventNow?.id ?? null,
|
selectedEventId: state.eventNow?.id ?? null,
|
||||||
startedAt: state.timer.startedAt,
|
startedAt: state.timer.startedAt,
|
||||||
addedTime: state.timer.addedTime,
|
addedTime: state.timer.addedTime,
|
||||||
pausedAt: internalState._timer.pausedAt,
|
pausedAt: state._timer.pausedAt,
|
||||||
pausedDuration: internalState._timer.pausedDuration,
|
pausedDuration: state._timer.pausedDuration,
|
||||||
firstStart: state.rundown.actualStart,
|
firstStart: state.rundown.actualStart,
|
||||||
startEpoch: internalState._startEpoch,
|
startEpoch: state._startEpoch,
|
||||||
currentDay: state.rundown.currentDay,
|
currentDay: state.rundown.currentDay,
|
||||||
})
|
})
|
||||||
.catch((_e) => {
|
.catch((_e) => {
|
||||||
|
|||||||
@@ -1,9 +1,7 @@
|
|||||||
import { Day, Duration, Maybe, TimeOfDay, TimerPhase } from 'ontime-types';
|
import { Day, MaybeNumber, TimeOfDay, TimerPhase } from 'ontime-types';
|
||||||
import { MILLIS_PER_HOUR, checkIsNow, dayInMs, isPlaybackActive } from 'ontime-utils';
|
import { MILLIS_PER_HOUR, checkIsNow, dayInMs, isPlaybackActive } from 'ontime-utils';
|
||||||
|
|
||||||
import * as timeCore from '../lib/time-core/timeCore.js';
|
import type { RuntimeState } from '../stores/runtimeState.js';
|
||||||
import { ZERO_DURATION } from '../lib/time-core/timeCore.js';
|
|
||||||
import type { InternalRuntimeState, RuntimeState } from '../stores/runtimeState.js';
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* handle events that span over midnight
|
* handle events that span over midnight
|
||||||
@@ -22,46 +20,48 @@ export function hasCrossedMidnight(previous: TimeOfDay, current: TimeOfDay): boo
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Calculates expected finish time of a running timer
|
* Calculates expected finish time of a running timer
|
||||||
* @param {InternalRuntimeState} state runtime state
|
* @param {RuntimeState} state runtime state
|
||||||
* @returns {number | null} new current time or null if nothing is running
|
* @returns {number | null} new current time or null if nothing is running
|
||||||
*/
|
*/
|
||||||
export function getExpectedFinish(state: InternalRuntimeState): Maybe<TimeOfDay> {
|
export function getExpectedFinish(state: RuntimeState): MaybeNumber {
|
||||||
const { startedAt, duration, addedTime } = state.timer;
|
const { startedAt, duration, addedTime } = state.timer;
|
||||||
|
|
||||||
if (state.eventNow === null) {
|
if (state.eventNow === null) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const { countToEnd, timeEnd } = state.eventNow;
|
const { countToEnd, timeEnd } = state.eventNow;
|
||||||
const { pausedAt } = state._timer;
|
const { pausedAt } = state._timer;
|
||||||
const { _now } = state;
|
const { clock } = state;
|
||||||
|
|
||||||
if (startedAt === null) {
|
if (startedAt === null) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const pausedTime = pausedAt !== null ? timeCore.toTimeOfDay(_now) - pausedAt : 0;
|
const pausedTime = pausedAt != null ? clock - pausedAt : 0;
|
||||||
|
|
||||||
if (countToEnd) {
|
if (countToEnd) {
|
||||||
return (timeEnd + addedTime + pausedTime) as TimeOfDay;
|
return timeEnd + addedTime + pausedTime;
|
||||||
}
|
}
|
||||||
|
|
||||||
// handle events that finish the day after
|
// handle events that finish the day after
|
||||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion -- duration exists if there is a timer
|
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion -- duration exists if ther eis a timer
|
||||||
const expectedFinish = timeCore.toTimeOfDay(startedAt) + duration! + addedTime + pausedTime;
|
const expectedFinish = startedAt + duration! + addedTime + pausedTime;
|
||||||
|
|
||||||
if (expectedFinish > dayInMs) {
|
if (expectedFinish > dayInMs) {
|
||||||
return (expectedFinish - dayInMs) as TimeOfDay;
|
return expectedFinish - dayInMs;
|
||||||
}
|
}
|
||||||
|
|
||||||
// an event cannot finish before it started (user added too much negative time)
|
// an event cannot finish before it started (user added too much negative time)
|
||||||
return Math.max(expectedFinish, timeCore.toTimeOfDay(startedAt)) as TimeOfDay;
|
return Math.max(expectedFinish, startedAt);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Calculates running countdown
|
* Calculates running countdown
|
||||||
* @param {InternalRuntimeState} state runtime state
|
* @param {RuntimeState} state runtime state
|
||||||
* @returns {number} current time for timer
|
* @returns {number} current time for timer
|
||||||
*/
|
*/
|
||||||
export function getCurrent(state: InternalRuntimeState): Duration {
|
|
||||||
|
export function getCurrent(state: RuntimeState): number {
|
||||||
// eslint-disable-next-line no-unused-labels -- dev code path
|
// eslint-disable-next-line no-unused-labels -- dev code path
|
||||||
DEV: {
|
DEV: {
|
||||||
if (state.eventNow === null || state.timer.duration === null) {
|
if (state.eventNow === null || state.timer.duration === null) {
|
||||||
@@ -71,12 +71,12 @@ export function getCurrent(state: InternalRuntimeState): Duration {
|
|||||||
const { startedAt, duration, addedTime } = state.timer;
|
const { startedAt, duration, addedTime } = state.timer;
|
||||||
const { countToEnd, timeStart, timeEnd } = state.eventNow;
|
const { countToEnd, timeStart, timeEnd } = state.eventNow;
|
||||||
const { pausedAt } = state._timer;
|
const { pausedAt } = state._timer;
|
||||||
const { _now } = state;
|
const { clock } = state;
|
||||||
|
|
||||||
if (countToEnd) {
|
if (countToEnd) {
|
||||||
const isEventOverMidnight = timeStart > timeEnd;
|
const isEventOverMidnight = timeStart > timeEnd;
|
||||||
const correctDay = isEventOverMidnight ? dayInMs : 0;
|
const correctDay = isEventOverMidnight ? dayInMs : 0;
|
||||||
return (correctDay - timeCore.toTimeOfDay(_now) + timeEnd + addedTime) as Duration;
|
return correctDay - clock + timeEnd + addedTime;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (startedAt === null) {
|
if (startedAt === null) {
|
||||||
@@ -84,19 +84,19 @@ export function getCurrent(state: InternalRuntimeState): Duration {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (pausedAt != null) {
|
if (pausedAt != null) {
|
||||||
return (timeCore.toTimeOfDay(startedAt) + duration + addedTime - pausedAt) as Duration;
|
return startedAt + duration + addedTime - pausedAt;
|
||||||
}
|
}
|
||||||
|
|
||||||
const hasPassedMidnight = timeCore.toTimeOfDay(startedAt) > timeCore.toTimeOfDay(_now);
|
const hasPassedMidnight = startedAt > clock;
|
||||||
const correctDay = hasPassedMidnight ? dayInMs : 0;
|
const correctDay = hasPassedMidnight ? dayInMs : 0;
|
||||||
return (timeCore.toTimeOfDay(startedAt) + duration + addedTime - timeCore.toTimeOfDay(_now) - correctDay) as Duration;
|
return startedAt + duration + addedTime - clock - correctDay;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Calculates active time elapsed since the timer started.
|
* Calculates active time elapsed since the timer started.
|
||||||
*/
|
*/
|
||||||
export function getElapsed(state: InternalRuntimeState): Maybe<Duration> {
|
export function getElapsed(state: RuntimeState): MaybeNumber {
|
||||||
const { _now } = state;
|
const { clock } = state;
|
||||||
const { startedAt } = state.timer;
|
const { startedAt } = state.timer;
|
||||||
const { pausedAt, pausedDuration } = state._timer;
|
const { pausedAt, pausedDuration } = state._timer;
|
||||||
|
|
||||||
@@ -104,11 +104,11 @@ export function getElapsed(state: InternalRuntimeState): Maybe<Duration> {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const referenceClock = pausedAt ?? timeCore.toTimeOfDay(_now);
|
const referenceClock = pausedAt ?? clock;
|
||||||
const elapsedSinceStart = getTimeSinceStart(referenceClock, timeCore.toTimeOfDay(startedAt));
|
const elapsedSinceStart = getTimeSinceStart(referenceClock, startedAt);
|
||||||
const activeElapsed = elapsedSinceStart - pausedDuration;
|
const activeElapsed = elapsedSinceStart - pausedDuration;
|
||||||
|
|
||||||
return Math.max(0, activeElapsed) as Duration;
|
return Math.max(0, activeElapsed);
|
||||||
}
|
}
|
||||||
|
|
||||||
function getTimeSinceStart(clock: TimeOfDay, startedAt: number): number {
|
function getTimeSinceStart(clock: TimeOfDay, startedAt: number): number {
|
||||||
@@ -127,7 +127,7 @@ function getTimeSinceStart(clock: TimeOfDay, startedAt: number): number {
|
|||||||
* @returns {boolean}
|
* @returns {boolean}
|
||||||
*/
|
*/
|
||||||
export function skippedOutOfEvent(state: RuntimeState, previousTime: number, skipLimit: number): boolean {
|
export function skippedOutOfEvent(state: RuntimeState, previousTime: number, skipLimit: number): boolean {
|
||||||
// we cant have skipped if we haven't started
|
// we cant have skipped if we havent started
|
||||||
if (state.timer.expectedFinish === null || state.timer.startedAt === null) {
|
if (state.timer.expectedFinish === null || state.timer.startedAt === null) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -151,12 +151,12 @@ export function skippedOutOfEvent(state: RuntimeState, previousTime: number, ski
|
|||||||
* Positive offset is over time / behind schedule
|
* Positive offset is over time / behind schedule
|
||||||
* Negative offset is under time / ahead of schedule
|
* Negative offset is under time / ahead of schedule
|
||||||
*/
|
*/
|
||||||
export function getRuntimeOffset(state: InternalRuntimeState): { absolute: Duration; relative: Duration } {
|
export function getRuntimeOffset(state: RuntimeState): { absolute: number; relative: number } {
|
||||||
const { eventNow, _now, _startDayOffset } = state;
|
const { eventNow, clock, _startDayOffset } = state;
|
||||||
const { addedTime, current, startedAt } = state.timer;
|
const { addedTime, current, startedAt } = state.timer;
|
||||||
// nothing to calculate if there are no loaded events or if we havent started
|
// 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 || _startDayOffset === null) {
|
||||||
return { absolute: ZERO_DURATION, relative: ZERO_DURATION };
|
return { absolute: 0, relative: 0 };
|
||||||
}
|
}
|
||||||
|
|
||||||
const { countToEnd, timeStart, dayOffset } = eventNow;
|
const { countToEnd, timeStart, dayOffset } = eventNow;
|
||||||
@@ -171,23 +171,20 @@ export function getRuntimeOffset(state: InternalRuntimeState): { absolute: Durat
|
|||||||
}
|
}
|
||||||
|
|
||||||
// difference between planned event start and actual event start (will be positive if we started behind)
|
// difference between planned event start and actual event start (will be positive if we started behind)
|
||||||
const eventStartOffset =
|
const eventStartOffset = startedAt + _startDayOffset * dayInMs - (timeStart + dayOffset * dayInMs);
|
||||||
timeCore.toTimeOfDay(startedAt) + _startDayOffset * dayInMs - (timeStart + dayOffset * dayInMs);
|
|
||||||
|
|
||||||
// how long has the event been running over (is a negative number when in over timer so inverted before adding to offset)
|
// 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));
|
const overtime = Math.abs(Math.min(current, 0));
|
||||||
|
|
||||||
// time the playback was paused, the different from now to when we paused is added to the offset TODO: brakes when crossing midnight
|
// 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 : timeCore.toTimeOfDay(_now) - state._timer.pausedAt;
|
const pausedTime = state._timer.pausedAt === null ? 0 : clock - state._timer.pausedAt;
|
||||||
|
|
||||||
// absolute offset is difference between schedule and playback time
|
// absolute offset is difference between schedule and playback time
|
||||||
// in case of count to end, the absolute offset is overtime and added time
|
// in case of count to end, the absolute offset is overtime and added time
|
||||||
const absolute = (
|
const absolute = countToEnd ? overtime + addedTime : eventStartOffset + overtime + pausedTime + addedTime;
|
||||||
countToEnd ? overtime + addedTime : eventStartOffset + overtime + pausedTime + addedTime
|
|
||||||
) as Duration;
|
|
||||||
|
|
||||||
// the relative offset is the same as the absolute but adjusted relative to the actual start time
|
// the relative offset is the same as the absolute but adjusted relative to the actual start time
|
||||||
const relative = (absolute + plannedStart - actualStart - _startDayOffset * dayInMs) as Duration;
|
const relative = absolute + plannedStart - actualStart - _startDayOffset * dayInMs;
|
||||||
|
|
||||||
return { absolute, relative };
|
return { absolute, relative };
|
||||||
}
|
}
|
||||||
@@ -196,7 +193,7 @@ export function getRuntimeOffset(state: InternalRuntimeState): { absolute: Durat
|
|||||||
* Checks running timer to see which phase it currently is in
|
* Checks running timer to see which phase it currently is in
|
||||||
* @param state
|
* @param state
|
||||||
*/
|
*/
|
||||||
export function getTimerPhase(state: InternalRuntimeState): TimerPhase {
|
export function getTimerPhase(state: RuntimeState): TimerPhase {
|
||||||
if (!isPlaybackActive(state.timer.playback)) {
|
if (!isPlaybackActive(state.timer.playback)) {
|
||||||
return TimerPhase.None;
|
return TimerPhase.None;
|
||||||
}
|
}
|
||||||
@@ -226,7 +223,7 @@ export function getTimerPhase(state: InternalRuntimeState): TimerPhase {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Finds the day offset relative to an event start
|
* Finds the day offset relative to an event start
|
||||||
* used by the RuntimeState on first start to get correct offsets
|
* used byt the runtimeState on first start to get correct offsets
|
||||||
*/
|
*/
|
||||||
export function findDayOffset(plannedStart: number, clock: number): Day {
|
export function findDayOffset(plannedStart: number, clock: number): Day {
|
||||||
const distance = clock - plannedStart;
|
const distance = clock - plannedStart;
|
||||||
|
|||||||
@@ -1,12 +1,33 @@
|
|||||||
import { Day, type Duration, type Instant, OffsetMode, Playback, TimeOfDay, TimerPhase } from 'ontime-types';
|
import { OffsetMode, Playback, type TimeOfDay, TimerPhase } from 'ontime-types';
|
||||||
import { deepmerge } from 'ontime-utils';
|
import { deepmerge } from 'ontime-utils';
|
||||||
|
|
||||||
import { ZERO_DURATION } from '../../lib/time-core/timeCore.js';
|
import type { RuntimeState } from '../runtimeState.js';
|
||||||
import type { InternalRuntimeState, RuntimeState } from '../runtimeState.js';
|
|
||||||
|
|
||||||
export const runtimePlaceholder = {
|
const baseState: RuntimeState = {
|
||||||
|
clock: 0 as TimeOfDay,
|
||||||
|
eventNow: null,
|
||||||
|
eventNext: null,
|
||||||
|
eventFlag: null,
|
||||||
|
groupNow: null,
|
||||||
|
rundown: {
|
||||||
|
selectedEventIndex: null,
|
||||||
|
numEvents: 0,
|
||||||
|
plannedStart: 0,
|
||||||
|
plannedEnd: 0,
|
||||||
|
actualStart: null,
|
||||||
|
actualGroupStart: null,
|
||||||
|
currentDay: 0,
|
||||||
|
},
|
||||||
|
offset: {
|
||||||
|
absolute: 0,
|
||||||
|
relative: 0,
|
||||||
|
mode: OffsetMode.Absolute,
|
||||||
|
expectedRundownEnd: null,
|
||||||
|
expectedGroupEnd: null,
|
||||||
|
expectedFlagStart: null,
|
||||||
|
},
|
||||||
timer: {
|
timer: {
|
||||||
addedTime: ZERO_DURATION,
|
addedTime: 0,
|
||||||
current: null,
|
current: null,
|
||||||
duration: null,
|
duration: null,
|
||||||
elapsed: null,
|
elapsed: null,
|
||||||
@@ -16,43 +37,10 @@ export const runtimePlaceholder = {
|
|||||||
secondaryTimer: null,
|
secondaryTimer: null,
|
||||||
startedAt: null,
|
startedAt: null,
|
||||||
},
|
},
|
||||||
offset: {
|
|
||||||
absolute: ZERO_DURATION,
|
|
||||||
relative: ZERO_DURATION,
|
|
||||||
mode: OffsetMode.Absolute,
|
|
||||||
expectedGroupEnd: null,
|
|
||||||
expectedRundownEnd: null,
|
|
||||||
expectedFlagStart: null,
|
|
||||||
},
|
|
||||||
rundown: {
|
|
||||||
selectedEventIndex: null,
|
|
||||||
numEvents: 0,
|
|
||||||
plannedStart: 0 as TimeOfDay,
|
|
||||||
plannedEnd: 0 as TimeOfDay,
|
|
||||||
actualStart: null,
|
|
||||||
actualGroupStart: null,
|
|
||||||
|
|
||||||
currentDay: 0 as Day,
|
|
||||||
},
|
|
||||||
} as Readonly<InternalRuntimeState>;
|
|
||||||
|
|
||||||
const baseState: InternalRuntimeState = {
|
|
||||||
_now: 0 as Instant,
|
|
||||||
eventNow: null,
|
|
||||||
eventNext: null,
|
|
||||||
eventFlag: null,
|
|
||||||
groupNow: null,
|
|
||||||
rundown: {
|
|
||||||
...runtimePlaceholder.rundown,
|
|
||||||
},
|
|
||||||
offset: {
|
|
||||||
...runtimePlaceholder.offset,
|
|
||||||
},
|
|
||||||
timer: { ...runtimePlaceholder.timer },
|
|
||||||
_timer: {
|
_timer: {
|
||||||
forceFinish: false,
|
forceFinish: null,
|
||||||
pausedAt: null,
|
pausedAt: null,
|
||||||
pausedDuration: 0 as Duration,
|
pausedDuration: 0,
|
||||||
secondaryTarget: null,
|
secondaryTarget: null,
|
||||||
hasFinished: false,
|
hasFinished: false,
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -6,10 +6,9 @@ import { rundownCache } from '../../api-data/rundown/rundown.dao.js';
|
|||||||
import { initRundown } from '../../api-data/rundown/rundown.service.js';
|
import { initRundown } from '../../api-data/rundown/rundown.service.js';
|
||||||
import { RundownMetadata } from '../../api-data/rundown/rundown.types.js';
|
import { RundownMetadata } from '../../api-data/rundown/rundown.types.js';
|
||||||
import {
|
import {
|
||||||
type InternalRuntimeState,
|
type RuntimeState,
|
||||||
addTime,
|
addTime,
|
||||||
clearState,
|
clearState,
|
||||||
getInternalState,
|
|
||||||
getState,
|
getState,
|
||||||
load,
|
load,
|
||||||
loadGroupFlagAndEnd,
|
loadGroupFlagAndEnd,
|
||||||
@@ -33,7 +32,7 @@ const mockEvent = {
|
|||||||
} as PlayableEvent;
|
} as PlayableEvent;
|
||||||
|
|
||||||
const mockState = {
|
const mockState = {
|
||||||
_now: 666,
|
clock: 666,
|
||||||
eventNow: null,
|
eventNow: null,
|
||||||
eventNext: null,
|
eventNext: null,
|
||||||
rundown: {
|
rundown: {
|
||||||
@@ -54,7 +53,7 @@ const mockState = {
|
|||||||
pausedAt: null,
|
pausedAt: null,
|
||||||
hasFinished: false,
|
hasFinished: false,
|
||||||
},
|
},
|
||||||
} as InternalRuntimeState;
|
} as RuntimeState;
|
||||||
|
|
||||||
vi.mock('../../classes/data-provider/DataProvider.js', () => {
|
vi.mock('../../classes/data-provider/DataProvider.js', () => {
|
||||||
return {
|
return {
|
||||||
@@ -109,34 +108,34 @@ describe('mutation on runtimeState', () => {
|
|||||||
|
|
||||||
const { metadata, rundown } = rundownCache.get();
|
const { metadata, rundown } = rundownCache.get();
|
||||||
load(mockEvent, rundown, metadata);
|
load(mockEvent, rundown, metadata);
|
||||||
let newState = getInternalState();
|
let newState = getState();
|
||||||
expect(newState.eventNow?.id).toBe(mockEvent.id);
|
expect(newState.eventNow?.id).toBe(mockEvent.id);
|
||||||
expect(newState.eventNext?.id).toBe('event2');
|
expect(newState.eventNext?.id).toBe('event2');
|
||||||
expect(newState.timer.playback).toBe(Playback.Armed);
|
expect(newState.timer.playback).toBe(Playback.Armed);
|
||||||
expect(getState().clock).not.toBe(666);
|
expect(newState.clock).not.toBe(666);
|
||||||
expect(newState.groupNow).toBeNull();
|
expect(newState.groupNow).toBeNull();
|
||||||
|
|
||||||
// 2. Start event
|
// 2. Start event
|
||||||
vi.setSystemTime('jan 1 00:02');
|
vi.setSystemTime('jan 1 00:02');
|
||||||
let success = start();
|
let success = start();
|
||||||
newState = getInternalState();
|
newState = getState();
|
||||||
expect(success).toBe(true);
|
expect(success).toBe(true);
|
||||||
expect(newState.timer).toMatchObject({
|
expect(newState.timer).toMatchObject({
|
||||||
playback: Playback.Play,
|
playback: Playback.Play,
|
||||||
});
|
});
|
||||||
expect(newState.rundown.actualStart).toBe(getState().clock);
|
expect(newState.rundown.actualStart).toBe(newState.clock);
|
||||||
|
|
||||||
// 3. Pause event
|
// 3. Pause event
|
||||||
vi.setSystemTime('jan 1 00:03');
|
vi.setSystemTime('jan 1 00:03');
|
||||||
success = pause();
|
success = pause();
|
||||||
newState = getInternalState();
|
newState = getState();
|
||||||
expect(success).toBe(true);
|
expect(success).toBe(true);
|
||||||
expect(getState().clock).not.toBe(666);
|
expect(newState.clock).not.toBe(666);
|
||||||
expect(newState.timer).toMatchObject({
|
expect(newState.timer).toMatchObject({
|
||||||
playback: Playback.Pause,
|
playback: Playback.Pause,
|
||||||
addedTime: 0,
|
addedTime: 0,
|
||||||
});
|
});
|
||||||
expect(newState._timer.pausedAt).toEqual(getState().clock);
|
expect(newState._timer.pausedAt).toEqual(newState.clock);
|
||||||
|
|
||||||
success = pause();
|
success = pause();
|
||||||
expect(success).toBe(false);
|
expect(success).toBe(false);
|
||||||
@@ -144,7 +143,7 @@ describe('mutation on runtimeState', () => {
|
|||||||
// 4. Restart event
|
// 4. Restart event
|
||||||
vi.setSystemTime('jan 1 00:04');
|
vi.setSystemTime('jan 1 00:04');
|
||||||
success = start();
|
success = start();
|
||||||
newState = getInternalState();
|
newState = getState();
|
||||||
expect(success).toBe(true);
|
expect(success).toBe(true);
|
||||||
expect(newState.timer).toMatchObject({
|
expect(newState.timer).toMatchObject({
|
||||||
playback: Playback.Play,
|
playback: Playback.Play,
|
||||||
@@ -164,7 +163,7 @@ describe('mutation on runtimeState', () => {
|
|||||||
// 5. Stop event
|
// 5. Stop event
|
||||||
vi.setSystemTime('jan 1 00:05');
|
vi.setSystemTime('jan 1 00:05');
|
||||||
success = stop();
|
success = stop();
|
||||||
newState = getInternalState();
|
newState = getState();
|
||||||
expect(success).toBe(true);
|
expect(success).toBe(true);
|
||||||
expect(newState.eventNow).toBe(null);
|
expect(newState.eventNow).toBe(null);
|
||||||
expect(newState.timer).toMatchObject({
|
expect(newState.timer).toMatchObject({
|
||||||
@@ -826,7 +825,7 @@ describe('roll mode', () => {
|
|||||||
test('pending event', () => {
|
test('pending event', () => {
|
||||||
const { rundown, metadata } = rundownCache.get();
|
const { rundown, metadata } = rundownCache.get();
|
||||||
const { eventId, didStart } = roll(rundown, metadata);
|
const { eventId, didStart } = roll(rundown, metadata);
|
||||||
const state = getInternalState();
|
const state = getState();
|
||||||
|
|
||||||
expect(eventId).toBe('1');
|
expect(eventId).toBe('1');
|
||||||
expect(didStart).toBe(false);
|
expect(didStart).toBe(false);
|
||||||
@@ -945,7 +944,7 @@ describe('loadGroupFlagAndEnd()', () => {
|
|||||||
groupNow: null,
|
groupNow: null,
|
||||||
eventNow: rundown.entries[11],
|
eventNow: rundown.entries[11],
|
||||||
rundown: { actualGroupStart: null },
|
rundown: { actualGroupStart: null },
|
||||||
} as InternalRuntimeState;
|
} as RuntimeState;
|
||||||
|
|
||||||
const metadata = { playableEventOrder: ['0', '11', '3'], flags: ['1'] } as RundownMetadata;
|
const metadata = { playableEventOrder: ['0', '11', '3'], flags: ['1'] } as RundownMetadata;
|
||||||
|
|
||||||
@@ -973,7 +972,7 @@ describe('loadGroupFlagAndEnd()', () => {
|
|||||||
groupNow: rundown.entries[1],
|
groupNow: rundown.entries[1],
|
||||||
eventNow: rundown.entries[22],
|
eventNow: rundown.entries[22],
|
||||||
rundown: { actualGroupStart: null },
|
rundown: { actualGroupStart: null },
|
||||||
} as InternalRuntimeState;
|
} as RuntimeState;
|
||||||
|
|
||||||
const metadata = { playableEventOrder: ['0', '11', '22'], flags: ['1'] } as RundownMetadata;
|
const metadata = { playableEventOrder: ['0', '11', '22'], flags: ['1'] } as RundownMetadata;
|
||||||
|
|
||||||
@@ -1001,7 +1000,7 @@ describe('loadGroupFlagAndEnd()', () => {
|
|||||||
groupNow: rundown.entries[1],
|
groupNow: rundown.entries[1],
|
||||||
eventNow: rundown.entries[0],
|
eventNow: rundown.entries[0],
|
||||||
rundown: { actualGroupStart: null },
|
rundown: { actualGroupStart: null },
|
||||||
} as InternalRuntimeState;
|
} as RuntimeState;
|
||||||
|
|
||||||
const metadata = { playableEventOrder: ['0', '11', '22'], flags: ['1'] } as RundownMetadata;
|
const metadata = { playableEventOrder: ['0', '11', '22'], flags: ['1'] } as RundownMetadata;
|
||||||
|
|
||||||
@@ -1026,7 +1025,7 @@ describe('loadGroupFlagAndEnd()', () => {
|
|||||||
groupNow: null,
|
groupNow: null,
|
||||||
eventNow: rundown.entries[0],
|
eventNow: rundown.entries[0],
|
||||||
rundown: { actualGroupStart: null },
|
rundown: { actualGroupStart: null },
|
||||||
} as InternalRuntimeState;
|
} as RuntimeState;
|
||||||
|
|
||||||
const metadata = { playableEventOrder: ['0', '1'], flags: ['1'] } as RundownMetadata;
|
const metadata = { playableEventOrder: ['0', '1'], flags: ['1'] } as RundownMetadata;
|
||||||
|
|
||||||
@@ -1088,7 +1087,7 @@ describe('loadGroupFlagAndEnd()', () => {
|
|||||||
groupNow: null,
|
groupNow: null,
|
||||||
eventNow: rundown.entries[0],
|
eventNow: rundown.entries[0],
|
||||||
rundown: { actualGroupStart: null },
|
rundown: { actualGroupStart: null },
|
||||||
} as InternalRuntimeState;
|
} as RuntimeState;
|
||||||
|
|
||||||
const metadata = { playableEventOrder: ['0', '1', '2', '3'], flags: ['3'] } as RundownMetadata;
|
const metadata = { playableEventOrder: ['0', '1', '2', '3'], flags: ['3'] } as RundownMetadata;
|
||||||
|
|
||||||
@@ -1170,7 +1169,7 @@ describe('loadGroupFlagAndEnd()', () => {
|
|||||||
groupNow: null,
|
groupNow: null,
|
||||||
eventNow: rundown.entries[0],
|
eventNow: rundown.entries[0],
|
||||||
rundown: { actualGroupStart: null },
|
rundown: { actualGroupStart: null },
|
||||||
} as InternalRuntimeState;
|
} as RuntimeState;
|
||||||
|
|
||||||
const metadata = { playableEventOrder: ['0', '1', '2', '3'], flags: ['3'] } as RundownMetadata;
|
const metadata = { playableEventOrder: ['0', '1', '2', '3'], flags: ['3'] } as RundownMetadata;
|
||||||
|
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ import {
|
|||||||
TimerPhase,
|
TimerPhase,
|
||||||
TimerState,
|
TimerState,
|
||||||
isOntimeEvent,
|
isOntimeEvent,
|
||||||
|
runtimeStorePlaceholder,
|
||||||
} from 'ontime-types';
|
} from 'ontime-types';
|
||||||
import {
|
import {
|
||||||
calculateDuration,
|
calculateDuration,
|
||||||
@@ -43,18 +44,14 @@ import {
|
|||||||
hasCrossedMidnight,
|
hasCrossedMidnight,
|
||||||
} from '../services/timerUtils.js';
|
} from '../services/timerUtils.js';
|
||||||
import { timerConfig } from '../setup/config.js';
|
import { timerConfig } from '../setup/config.js';
|
||||||
import { runtimePlaceholder } from './__mocks__/runtimeState.mocks.js';
|
|
||||||
|
|
||||||
type ExpectedMetadata = {
|
type ExpectedMetadata = {
|
||||||
event: OntimeEvent;
|
event: OntimeEvent;
|
||||||
accumulatedGap: Duration;
|
accumulatedGap: number;
|
||||||
isLinkedToLoaded: boolean;
|
isLinkedToLoaded: boolean;
|
||||||
} | null;
|
} | null;
|
||||||
|
|
||||||
export type RuntimeState = {
|
export type RuntimeState = {
|
||||||
/**
|
|
||||||
* @deprecated we want to phase out the 24h clock
|
|
||||||
*/
|
|
||||||
clock: TimeOfDay;
|
clock: TimeOfDay;
|
||||||
groupNow: OntimeGroup | null;
|
groupNow: OntimeGroup | null;
|
||||||
eventNow: PlayableEvent | null;
|
eventNow: PlayableEvent | null;
|
||||||
@@ -64,72 +61,36 @@ export type RuntimeState = {
|
|||||||
timer: TimerState;
|
timer: TimerState;
|
||||||
rundown: RundownState;
|
rundown: RundownState;
|
||||||
// private properties of the timer calculations
|
// private properties of the timer calculations
|
||||||
};
|
|
||||||
|
|
||||||
export type InternalRuntimeState = Omit<RuntimeState, 'clock' | 'timer' | 'offset' | 'rundown'> & {
|
|
||||||
timer: {
|
|
||||||
addedTime: Duration;
|
|
||||||
current: Maybe<Duration>;
|
|
||||||
duration: Maybe<Duration>;
|
|
||||||
elapsed: Maybe<Duration>;
|
|
||||||
expectedFinish: Maybe<TimeOfDay>;
|
|
||||||
phase: TimerPhase;
|
|
||||||
playback: Playback;
|
|
||||||
secondaryTimer: Maybe<Duration>;
|
|
||||||
startedAt: Maybe<Instant>;
|
|
||||||
};
|
|
||||||
offset: {
|
|
||||||
absolute: Duration;
|
|
||||||
relative: Duration;
|
|
||||||
mode: OffsetMode;
|
|
||||||
expectedGroupEnd: Maybe<TimeOfDay>;
|
|
||||||
expectedRundownEnd: Maybe<TimeOfDay>;
|
|
||||||
expectedFlagStart: Maybe<TimeOfDay>;
|
|
||||||
};
|
|
||||||
rundown: {
|
|
||||||
selectedEventIndex: Maybe<number>;
|
|
||||||
numEvents: number;
|
|
||||||
plannedStart: Maybe<TimeOfDay>;
|
|
||||||
plannedEnd: Maybe<TimeOfDay>;
|
|
||||||
actualStart: Maybe<TimeOfDay>;
|
|
||||||
currentDay: Maybe<Day>;
|
|
||||||
actualGroupStart: Maybe<TimeOfDay>;
|
|
||||||
};
|
|
||||||
_now: Instant;
|
|
||||||
_timer: {
|
_timer: {
|
||||||
forceFinish: boolean; // whether we should declare an event as finished, will contain the finish time
|
forceFinish: Maybe<TimeOfDay>; // whether we should declare an event as finished, will contain the finish time
|
||||||
pausedAt: Maybe<TimeOfDay>;
|
pausedAt: Maybe<TimeOfDay>;
|
||||||
pausedDuration: Duration;
|
pausedDuration: number;
|
||||||
secondaryTarget: Maybe<TimeOfDay>;
|
secondaryTarget: Maybe<TimeOfDay>;
|
||||||
hasFinished: boolean;
|
hasFinished: boolean;
|
||||||
};
|
};
|
||||||
_rundown: {
|
_rundown: {
|
||||||
totalDelay: number; // this value comes from rundown service
|
totalDelay: number; // this value comes from rundown service
|
||||||
};
|
};
|
||||||
_startDayOffset: Maybe<Day>;
|
|
||||||
_startEpoch: Maybe<Instant>;
|
|
||||||
_group: ExpectedMetadata;
|
_group: ExpectedMetadata;
|
||||||
_flag: ExpectedMetadata;
|
_flag: ExpectedMetadata;
|
||||||
_end: ExpectedMetadata;
|
_end: ExpectedMetadata;
|
||||||
|
_startEpoch: Maybe<Instant>;
|
||||||
|
_startDayOffset: Maybe<Day>;
|
||||||
};
|
};
|
||||||
|
|
||||||
const runtimeState: InternalRuntimeState = {
|
const runtimeState: RuntimeState = {
|
||||||
_now: timeCore.now(),
|
clock: timeCore.timeOfDayNow(),
|
||||||
groupNow: null,
|
groupNow: null,
|
||||||
eventNow: null,
|
eventNow: null,
|
||||||
eventNext: null,
|
eventNext: null,
|
||||||
eventFlag: null,
|
eventFlag: null,
|
||||||
offset: { ...runtimePlaceholder.offset },
|
offset: { ...runtimeStorePlaceholder.offset },
|
||||||
timer: {
|
timer: { ...runtimeStorePlaceholder.timer },
|
||||||
...runtimePlaceholder.timer,
|
rundown: { ...runtimeStorePlaceholder.rundown },
|
||||||
},
|
|
||||||
rundown: {
|
|
||||||
...runtimePlaceholder.rundown,
|
|
||||||
},
|
|
||||||
_timer: {
|
_timer: {
|
||||||
forceFinish: false,
|
forceFinish: null,
|
||||||
pausedAt: null,
|
pausedAt: null,
|
||||||
pausedDuration: 0 as Duration,
|
pausedDuration: 0,
|
||||||
secondaryTarget: null,
|
secondaryTarget: null,
|
||||||
hasFinished: false,
|
hasFinished: false,
|
||||||
},
|
},
|
||||||
@@ -143,32 +104,19 @@ const runtimeState: InternalRuntimeState = {
|
|||||||
_startDayOffset: null,
|
_startDayOffset: null,
|
||||||
};
|
};
|
||||||
|
|
||||||
export function getInternalState(): Readonly<InternalRuntimeState> {
|
|
||||||
return runtimeState;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getState(): Readonly<RuntimeState> {
|
export function getState(): Readonly<RuntimeState> {
|
||||||
// create a shallow copy of the state
|
// create a shallow copy of the state
|
||||||
return {
|
return {
|
||||||
...runtimeState,
|
...runtimeState,
|
||||||
clock: timeCore.toTimeOfDay(runtimeState._now),
|
|
||||||
eventNow: runtimeState.eventNow ? { ...runtimeState.eventNow } : null,
|
eventNow: runtimeState.eventNow ? { ...runtimeState.eventNow } : null,
|
||||||
eventNext: runtimeState.eventNext ? { ...runtimeState.eventNext } : null,
|
eventNext: runtimeState.eventNext ? { ...runtimeState.eventNext } : null,
|
||||||
eventFlag: runtimeState.eventFlag ? { ...runtimeState.eventFlag } : null,
|
eventFlag: runtimeState.eventFlag ? { ...runtimeState.eventFlag } : null,
|
||||||
groupNow: runtimeState.groupNow ? { ...runtimeState.groupNow } : null,
|
groupNow: runtimeState.groupNow ? { ...runtimeState.groupNow } : null,
|
||||||
offset: { ...runtimeState.offset },
|
offset: { ...runtimeState.offset },
|
||||||
rundown: { ...runtimeState.rundown },
|
rundown: { ...runtimeState.rundown },
|
||||||
timer: {
|
timer: { ...runtimeState.timer },
|
||||||
addedTime: runtimeState.timer.addedTime,
|
_timer: { ...runtimeState._timer },
|
||||||
current: runtimeState.timer.current,
|
_rundown: { ...runtimeState._rundown },
|
||||||
duration: runtimeState.timer.duration,
|
|
||||||
elapsed: runtimeState.timer.elapsed,
|
|
||||||
expectedFinish: runtimeState.timer.expectedFinish,
|
|
||||||
phase: runtimeState.timer.phase,
|
|
||||||
playback: runtimeState.timer.playback,
|
|
||||||
secondaryTimer: runtimeState.timer.secondaryTimer,
|
|
||||||
startedAt: runtimeState.timer.startedAt ? timeCore.toTimeOfDay(runtimeState.timer.startedAt) : null,
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -179,18 +127,22 @@ export function clearEventData() {
|
|||||||
runtimeState.eventNow = null;
|
runtimeState.eventNow = null;
|
||||||
runtimeState.eventNext = null;
|
runtimeState.eventNext = null;
|
||||||
|
|
||||||
runtimeState.offset = { ...runtimePlaceholder.offset };
|
runtimeState.offset.absolute = 0;
|
||||||
|
runtimeState.offset.relative = 0;
|
||||||
|
runtimeState.offset.expectedFlagStart = null;
|
||||||
|
runtimeState.offset.expectedGroupEnd = null;
|
||||||
|
runtimeState.offset.expectedRundownEnd = null;
|
||||||
|
|
||||||
runtimeState.rundown.selectedEventIndex = null;
|
runtimeState.rundown.selectedEventIndex = null;
|
||||||
|
|
||||||
runtimeState.timer.playback = Playback.Stop;
|
runtimeState.timer.playback = Playback.Stop;
|
||||||
runtimeState._now = timeCore.now();
|
runtimeState.clock = timeCore.timeOfDayNow();
|
||||||
runtimeState.timer = { ...runtimePlaceholder.timer };
|
runtimeState.timer = { ...runtimeStorePlaceholder.timer };
|
||||||
|
|
||||||
// when clearing, we maintain the total delay from the rundown
|
// when clearing, we maintain the total delay from the rundown
|
||||||
runtimeState._timer.forceFinish = false;
|
runtimeState._timer.forceFinish = null;
|
||||||
runtimeState._timer.pausedAt = null;
|
runtimeState._timer.pausedAt = null;
|
||||||
runtimeState._timer.pausedDuration = 0 as Duration;
|
runtimeState._timer.pausedDuration = 0;
|
||||||
runtimeState._timer.secondaryTarget = null;
|
runtimeState._timer.secondaryTarget = null;
|
||||||
runtimeState._timer.hasFinished = false;
|
runtimeState._timer.hasFinished = false;
|
||||||
}
|
}
|
||||||
@@ -208,18 +160,22 @@ export function clearState() {
|
|||||||
runtimeState.rundown.actualStart = null;
|
runtimeState.rundown.actualStart = null;
|
||||||
runtimeState.rundown.selectedEventIndex = null;
|
runtimeState.rundown.selectedEventIndex = null;
|
||||||
|
|
||||||
runtimeState.offset = { ...runtimePlaceholder.offset };
|
runtimeState.offset.absolute = 0;
|
||||||
|
runtimeState.offset.relative = 0;
|
||||||
|
runtimeState.offset.expectedRundownEnd = null;
|
||||||
|
runtimeState.offset.expectedGroupEnd = null;
|
||||||
|
runtimeState.offset.expectedFlagStart = null;
|
||||||
|
|
||||||
runtimeState._end = null;
|
runtimeState._end = null;
|
||||||
|
|
||||||
runtimeState.timer.playback = Playback.Stop;
|
runtimeState.timer.playback = Playback.Stop;
|
||||||
runtimeState._now = timeCore.now();
|
runtimeState.clock = timeCore.timeOfDayNow();
|
||||||
runtimeState.timer = { ...runtimePlaceholder.timer };
|
runtimeState.timer = { ...runtimeStorePlaceholder.timer };
|
||||||
|
|
||||||
// when clearing, we maintain the total delay from the rundown
|
// when clearing, we maintain the total delay from the rundown
|
||||||
runtimeState._timer.forceFinish = false;
|
runtimeState._timer.forceFinish = null;
|
||||||
runtimeState._timer.pausedAt = null;
|
runtimeState._timer.pausedAt = null;
|
||||||
runtimeState._timer.pausedDuration = 0 as Duration;
|
runtimeState._timer.pausedDuration = 0;
|
||||||
runtimeState._timer.secondaryTarget = null;
|
runtimeState._timer.secondaryTarget = null;
|
||||||
runtimeState._timer.hasFinished = false;
|
runtimeState._timer.hasFinished = false;
|
||||||
|
|
||||||
@@ -261,9 +217,9 @@ export function updateRundownData(rundownData: {
|
|||||||
runtimeState._rundown.totalDelay = rundownData.totalDelay;
|
runtimeState._rundown.totalDelay = rundownData.totalDelay;
|
||||||
|
|
||||||
runtimeState.rundown.numEvents = rundownData.numEvents;
|
runtimeState.rundown.numEvents = rundownData.numEvents;
|
||||||
runtimeState.rundown.plannedStart = rundownData.firstStart as TimeOfDay;
|
runtimeState.rundown.plannedStart = rundownData.firstStart;
|
||||||
runtimeState.rundown.plannedEnd =
|
runtimeState.rundown.plannedEnd =
|
||||||
rundownData.firstStart === null ? null : ((rundownData.firstStart + rundownData.totalDuration) as TimeOfDay);
|
rundownData.firstStart === null ? null : rundownData.firstStart + rundownData.totalDuration;
|
||||||
|
|
||||||
if (isPlaybackActive(runtimeState.timer.playback)) getExpectedTimes();
|
if (isPlaybackActive(runtimeState.timer.playback)) getExpectedTimes();
|
||||||
}
|
}
|
||||||
@@ -297,7 +253,7 @@ export function load(
|
|||||||
|
|
||||||
// update state
|
// update state
|
||||||
runtimeState.timer.playback = Playback.Armed;
|
runtimeState.timer.playback = Playback.Armed;
|
||||||
runtimeState.timer.duration = event.duration as Duration;
|
runtimeState.timer.duration = calculateDuration(event.timeStart, event.timeEnd);
|
||||||
runtimeState.timer.current = getCurrent(runtimeState);
|
runtimeState.timer.current = getCurrent(runtimeState);
|
||||||
runtimeState.rundown.numEvents = metadata.timedEventOrder.length;
|
runtimeState.rundown.numEvents = metadata.timedEventOrder.length;
|
||||||
|
|
||||||
@@ -311,13 +267,13 @@ export function load(
|
|||||||
(firstStart === null || typeof firstStart === 'number') &&
|
(firstStart === null || typeof firstStart === 'number') &&
|
||||||
(startEpoch === null || typeof startEpoch === 'number')
|
(startEpoch === null || typeof startEpoch === 'number')
|
||||||
) {
|
) {
|
||||||
runtimeState.rundown.actualStart = firstStart as TimeOfDay;
|
runtimeState.rundown.actualStart = firstStart;
|
||||||
runtimeState._startEpoch = startEpoch;
|
runtimeState._startEpoch = startEpoch;
|
||||||
if (firstStart !== null && runtimeState.rundown.plannedStart !== null) {
|
if (firstStart !== null && runtimeState.rundown.plannedStart !== null) {
|
||||||
runtimeState._startDayOffset = findDayOffset(runtimeState.rundown.plannedStart, firstStart);
|
runtimeState._startDayOffset = findDayOffset(runtimeState.rundown.plannedStart, firstStart);
|
||||||
}
|
}
|
||||||
if (currentDay !== undefined) {
|
if (currentDay !== undefined) {
|
||||||
runtimeState.rundown.currentDay = currentDay as Day;
|
runtimeState.rundown.currentDay = currentDay;
|
||||||
}
|
}
|
||||||
const { absolute, relative } = getRuntimeOffset(runtimeState);
|
const { absolute, relative } = getRuntimeOffset(runtimeState);
|
||||||
runtimeState.offset.absolute = absolute;
|
runtimeState.offset.absolute = absolute;
|
||||||
@@ -409,16 +365,16 @@ export function updateLoaded(event?: PlayableEvent): string | undefined {
|
|||||||
|
|
||||||
// handle edge cases with roll
|
// handle edge cases with roll
|
||||||
if (runtimeState.timer.playback === Playback.Roll) {
|
if (runtimeState.timer.playback === Playback.Roll) {
|
||||||
const offsetClock = timeCore.toTimeOfDay(runtimeState._now) - runtimeState.offset.absolute;
|
const offsetClock = runtimeState.clock - runtimeState.offset.absolute;
|
||||||
// if waiting to roll, we update the targets and potentially start the timer
|
// if waiting to roll, we update the targets and potentially start the timer
|
||||||
if (runtimeState._timer.secondaryTarget !== null) {
|
if (runtimeState._timer.secondaryTarget !== null) {
|
||||||
if (runtimeState.eventNow.timeStart < offsetClock && offsetClock < runtimeState.eventNow.timeEnd) {
|
if (runtimeState.eventNow.timeStart < offsetClock && offsetClock < runtimeState.eventNow.timeEnd) {
|
||||||
// if the event is now, we queue a start
|
// if the event is now, we queue a start
|
||||||
runtimeState._timer.secondaryTarget = runtimeState.eventNow.timeStart as TimeOfDay;
|
runtimeState._timer.secondaryTarget = runtimeState.eventNow.timeStart as TimeOfDay;
|
||||||
runtimeState.timer.secondaryTimer = (runtimeState._timer.secondaryTarget - offsetClock) as Duration;
|
runtimeState.timer.secondaryTimer = runtimeState._timer.secondaryTarget - offsetClock;
|
||||||
} else {
|
} else {
|
||||||
runtimeState._timer.secondaryTarget = normaliseRollStart(
|
runtimeState._timer.secondaryTarget = normaliseRollStart(
|
||||||
runtimeState.eventNow.timeStart as TimeOfDay,
|
runtimeState.eventNow.timeStart,
|
||||||
offsetClock,
|
offsetClock,
|
||||||
) as TimeOfDay;
|
) as TimeOfDay;
|
||||||
}
|
}
|
||||||
@@ -435,7 +391,7 @@ export function updateLoaded(event?: PlayableEvent): string | undefined {
|
|||||||
|
|
||||||
runtimeState.timer.startedAt = null;
|
runtimeState.timer.startedAt = null;
|
||||||
runtimeState._timer.hasFinished = false;
|
runtimeState._timer.hasFinished = false;
|
||||||
runtimeState.timer.addedTime = 0 as Duration;
|
runtimeState.timer.addedTime = 0;
|
||||||
runtimeState._timer.pausedAt = null;
|
runtimeState._timer.pausedAt = null;
|
||||||
|
|
||||||
// this could be looked after by the timer
|
// this could be looked after by the timer
|
||||||
@@ -458,7 +414,7 @@ export function updateAll(rundown: Rundown, metadata: RundownMetadata) {
|
|||||||
loadGroupFlagAndEnd(rundown, metadata, eventNowIndex);
|
loadGroupFlagAndEnd(rundown, metadata, eventNowIndex);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function start(state: InternalRuntimeState = runtimeState): boolean {
|
export function start(state: RuntimeState = runtimeState): boolean {
|
||||||
if (state.eventNow === null) {
|
if (state.eventNow === null) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -466,19 +422,22 @@ export function start(state: InternalRuntimeState = runtimeState): boolean {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
state._now = timeCore.now();
|
const epoch = timeCore.now();
|
||||||
|
const now = timeCore.toTimeOfDay(epoch);
|
||||||
|
|
||||||
|
state.clock = now;
|
||||||
state.timer.secondaryTimer = null;
|
state.timer.secondaryTimer = null;
|
||||||
|
|
||||||
// add paused time if it exists
|
// add paused time if it exists
|
||||||
if (state._timer.pausedAt) {
|
if (state._timer.pausedAt) {
|
||||||
const timeToAdd = (timeCore.toTimeOfDay(state._now) - state._timer.pausedAt) as Duration;
|
const timeToAdd = state.clock - state._timer.pausedAt;
|
||||||
state.timer.addedTime = timeCore.combineValues(state.timer.addedTime, timeToAdd);
|
state.timer.addedTime += timeToAdd;
|
||||||
state._timer.pausedDuration = timeCore.combineValues(state._timer.pausedDuration, timeToAdd);
|
state._timer.pausedDuration += timeToAdd;
|
||||||
state._timer.pausedAt = null;
|
state._timer.pausedAt = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (state.timer.startedAt === null) {
|
if (state.timer.startedAt === null) {
|
||||||
state.timer.startedAt = state._now;
|
state.timer.startedAt = state.clock;
|
||||||
}
|
}
|
||||||
|
|
||||||
state.timer.playback = Playback.Play;
|
state.timer.playback = Playback.Play;
|
||||||
@@ -486,15 +445,14 @@ export function start(state: InternalRuntimeState = runtimeState): boolean {
|
|||||||
state.timer.elapsed = getElapsed(state);
|
state.timer.elapsed = getElapsed(state);
|
||||||
|
|
||||||
if (state.rundown.actualStart === null) {
|
if (state.rundown.actualStart === null) {
|
||||||
state._startDayOffset = (findDayOffset(state.eventNow.timeStart, timeCore.toTimeOfDay(state._now)) +
|
state._startDayOffset = (findDayOffset(state.eventNow.timeStart, state.clock) + state.eventNow.dayOffset) as Day;
|
||||||
state.eventNow.dayOffset) as Day;
|
|
||||||
state.rundown.currentDay = state._startDayOffset;
|
state.rundown.currentDay = state._startDayOffset;
|
||||||
state._startEpoch = state._now;
|
state._startEpoch = epoch;
|
||||||
state.rundown.actualStart = timeCore.toTimeOfDay(state._now);
|
state.rundown.actualStart = state.clock;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (state.groupNow !== null && state.rundown.actualGroupStart === null) {
|
if (state.groupNow !== null && state.rundown.actualGroupStart === null) {
|
||||||
state.rundown.actualGroupStart = timeCore.toTimeOfDay(state._now);
|
state.rundown.actualGroupStart = state.clock;
|
||||||
}
|
}
|
||||||
|
|
||||||
// update timer phase
|
// update timer phase
|
||||||
@@ -517,18 +475,18 @@ export function start(state: InternalRuntimeState = runtimeState): boolean {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function pause(state: InternalRuntimeState = runtimeState): boolean {
|
export function pause(state: RuntimeState = runtimeState): boolean {
|
||||||
if (state.timer.playback !== Playback.Play) {
|
if (state.timer.playback !== Playback.Play) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
state.timer.playback = Playback.Pause;
|
state.timer.playback = Playback.Pause;
|
||||||
state._now = timeCore.now();
|
state.clock = timeCore.timeOfDayNow();
|
||||||
state._timer.pausedAt = timeCore.toTimeOfDay(state._now);
|
state._timer.pausedAt = state.clock;
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function stop(state: InternalRuntimeState = runtimeState): boolean {
|
export function stop(state: RuntimeState = runtimeState): boolean {
|
||||||
if (state.timer.playback === Playback.Stop) {
|
if (state.timer.playback === Playback.Stop) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -558,7 +516,7 @@ export function addTime(amount: number) {
|
|||||||
|
|
||||||
if (willGoNegative && !runtimeState._timer.hasFinished) {
|
if (willGoNegative && !runtimeState._timer.hasFinished) {
|
||||||
// set finished time so side effects are triggered
|
// set finished time so side effects are triggered
|
||||||
runtimeState._timer.forceFinish = true;
|
runtimeState._timer.forceFinish = timeCore.timeOfDayNow();
|
||||||
} else {
|
} else {
|
||||||
const willGoPositive = runtimeState.timer.current < 0 && runtimeState.timer.current + amount > 0;
|
const willGoPositive = runtimeState.timer.current < 0 && runtimeState.timer.current + amount > 0;
|
||||||
if (willGoPositive) {
|
if (willGoPositive) {
|
||||||
@@ -567,8 +525,8 @@ export function addTime(amount: number) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// we can update the state after handling the side effects
|
// we can update the state after handling the side effects
|
||||||
runtimeState.timer.addedTime = timeCore.combineValues(runtimeState.timer.addedTime, amount as Duration);
|
runtimeState.timer.addedTime += amount;
|
||||||
runtimeState.timer.current = timeCore.combineValues(runtimeState.timer.current, amount as Duration);
|
runtimeState.timer.current += amount;
|
||||||
runtimeState.timer.elapsed = getElapsed(runtimeState);
|
runtimeState.timer.elapsed = getElapsed(runtimeState);
|
||||||
|
|
||||||
// update runtime delays: over - under
|
// update runtime delays: over - under
|
||||||
@@ -588,8 +546,10 @@ export type UpdateResult = {
|
|||||||
|
|
||||||
export function update(): UpdateResult {
|
export function update(): UpdateResult {
|
||||||
// 0. there are some things we always do
|
// 0. there are some things we always do
|
||||||
const previousClock = timeCore.toTimeOfDay(runtimeState._now);
|
const previousClock = runtimeState.clock;
|
||||||
runtimeState._now = timeCore.now(); // we update the clock on every update call
|
const epoch = timeCore.now();
|
||||||
|
const now = timeCore.toTimeOfDay(epoch);
|
||||||
|
runtimeState.clock = now; // we update the clock on every update call
|
||||||
|
|
||||||
// 1. is playback idle?
|
// 1. is playback idle?
|
||||||
if (!isPlaybackActive(runtimeState.timer.playback)) {
|
if (!isPlaybackActive(runtimeState.timer.playback)) {
|
||||||
@@ -598,13 +558,13 @@ export function update(): UpdateResult {
|
|||||||
|
|
||||||
// calculate currentDay from epoch (days elapsed since playback was started)
|
// calculate currentDay from epoch (days elapsed since playback was started)
|
||||||
if (runtimeState._startEpoch !== null && runtimeState._startDayOffset !== null) {
|
if (runtimeState._startEpoch !== null && runtimeState._startDayOffset !== null) {
|
||||||
const daysSinceStart = timeCore.daysSinceStart(runtimeState._startEpoch, runtimeState._now);
|
const daysSinceStart = timeCore.daysSinceStart(runtimeState._startEpoch, epoch);
|
||||||
runtimeState.rundown.currentDay = timeCore.combineValues(runtimeState._startDayOffset, daysSinceStart);
|
runtimeState.rundown.currentDay = runtimeState._startDayOffset + daysSinceStart;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2. are we waiting to roll?
|
// 2. are we waiting to roll?
|
||||||
if (runtimeState.timer.playback === Playback.Roll && runtimeState.timer.secondaryTimer !== null) {
|
if (runtimeState.timer.playback === Playback.Roll && runtimeState.timer.secondaryTimer !== null) {
|
||||||
const clockHasCrossedMidnight = hasCrossedMidnight(previousClock, timeCore.toTimeOfDay(runtimeState._now));
|
const clockHasCrossedMidnight = hasCrossedMidnight(previousClock, now);
|
||||||
return updateIfWaitingToRoll(clockHasCrossedMidnight);
|
return updateIfWaitingToRoll(clockHasCrossedMidnight);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -658,24 +618,22 @@ export function update(): UpdateResult {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// account for offset
|
// account for offset
|
||||||
const offsetClock = timeCore.toTimeOfDay(
|
const offsetClock = runtimeState.clock + runtimeState.offset.absolute;
|
||||||
timeCore.addDuration(runtimeState._now, runtimeState.offset.absolute as Duration),
|
|
||||||
);
|
|
||||||
runtimeState.timer.phase = TimerPhase.Pending;
|
runtimeState.timer.phase = TimerPhase.Pending;
|
||||||
|
|
||||||
if (hasCrossedMidnight) {
|
if (hasCrossedMidnight) {
|
||||||
// if we crossed midnight, we need to update the target
|
// if we crossed midnight, we need to update the target
|
||||||
// this is the same logic from the roll function
|
// this is the same logic from the roll function
|
||||||
runtimeState._timer.secondaryTarget = normaliseRollStart(
|
runtimeState._timer.secondaryTarget = normaliseRollStart(
|
||||||
runtimeState.eventNow.timeStart as TimeOfDay,
|
runtimeState.eventNow.timeStart,
|
||||||
offsetClock,
|
offsetClock,
|
||||||
) as TimeOfDay;
|
) as TimeOfDay;
|
||||||
}
|
}
|
||||||
|
|
||||||
runtimeState.timer.secondaryTimer = (runtimeState._timer.secondaryTarget! - offsetClock) as Duration;
|
runtimeState.timer.secondaryTimer = runtimeState._timer.secondaryTarget! - offsetClock;
|
||||||
return {
|
return {
|
||||||
hasTimerFinished: false,
|
hasTimerFinished: false,
|
||||||
hasSecondaryTimerFinished: runtimeState.timer.secondaryTimer! <= 0,
|
hasSecondaryTimerFinished: runtimeState.timer.secondaryTimer <= 0,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -692,7 +650,9 @@ export function roll(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// we will need to do some calculations, update the time first
|
// we will need to do some calculations, update the time first
|
||||||
runtimeState._now = timeCore.now();
|
const epoch = timeCore.now();
|
||||||
|
const now = timeCore.toTimeOfDay(epoch);
|
||||||
|
runtimeState.clock = now;
|
||||||
|
|
||||||
// 2. if there is an event armed, we use it
|
// 2. if there is an event armed, we use it
|
||||||
if (runtimeState.timer.playback === Playback.Armed || runtimeState.timer.phase === TimerPhase.Pending) {
|
if (runtimeState.timer.playback === Playback.Armed || runtimeState.timer.phase === TimerPhase.Pending) {
|
||||||
@@ -704,9 +664,7 @@ export function roll(
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (offset) {
|
if (offset) {
|
||||||
runtimeState.offset = {
|
runtimeState.offset = { ...offset };
|
||||||
...offset,
|
|
||||||
} as InternalRuntimeState['offset'];
|
|
||||||
}
|
}
|
||||||
runtimeState.timer.playback = Playback.Roll;
|
runtimeState.timer.playback = Playback.Roll;
|
||||||
|
|
||||||
@@ -715,16 +673,15 @@ export function roll(
|
|||||||
runtimeState.eventNow.timeEnd < runtimeState.eventNow.timeStart
|
runtimeState.eventNow.timeEnd < runtimeState.eventNow.timeStart
|
||||||
? runtimeState.eventNow.timeEnd + dayInMs
|
? runtimeState.eventNow.timeEnd + dayInMs
|
||||||
: runtimeState.eventNow.timeEnd;
|
: runtimeState.eventNow.timeEnd;
|
||||||
runtimeState.timer.expectedFinish = normalisedEndTime as TimeOfDay;
|
runtimeState.timer.expectedFinish = normalisedEndTime;
|
||||||
|
|
||||||
// account for offset
|
// account for offset
|
||||||
// const offsetClock = timeCore.addDuration(runtimeState._now, -runtimeState.offset.absolute as Duration);
|
const offsetClock = runtimeState.clock - runtimeState.offset.absolute;
|
||||||
const offsetClock = timeCore.toTimeOfDay(runtimeState._now) - runtimeState.offset.absolute;
|
|
||||||
|
|
||||||
// state catch up
|
// state catch up
|
||||||
runtimeState.timer.duration = calculateDuration(runtimeState.eventNow.timeStart, normalisedEndTime);
|
runtimeState.timer.duration = calculateDuration(runtimeState.eventNow.timeStart, normalisedEndTime);
|
||||||
runtimeState.timer.current = runtimeState.timer.duration;
|
runtimeState.timer.current = runtimeState.timer.duration;
|
||||||
runtimeState.timer.elapsed = 0 as Duration;
|
runtimeState.timer.elapsed = 0;
|
||||||
|
|
||||||
// check if the event is ready to start or if needs to be pending
|
// check if the event is ready to start or if needs to be pending
|
||||||
const isNow = checkIsNow(runtimeState.eventNow.timeStart, runtimeState.eventNow.timeEnd, offsetClock);
|
const isNow = checkIsNow(runtimeState.eventNow.timeStart, runtimeState.eventNow.timeEnd, offsetClock);
|
||||||
@@ -733,8 +690,8 @@ export function roll(
|
|||||||
* If we are starting an event in roll mode
|
* If we are starting an event in roll mode
|
||||||
* we backtrace all the start times to the supposed start time of the event
|
* we backtrace all the start times to the supposed start time of the event
|
||||||
*/
|
*/
|
||||||
const plannedStart = runtimeState.eventNow.timeStart as TimeOfDay;
|
const plannedStart = runtimeState.eventNow.timeStart;
|
||||||
runtimeState.timer.startedAt = timeCore.toInstant(plannedStart, runtimeState._now);
|
runtimeState.timer.startedAt = plannedStart;
|
||||||
// reset the secondary timer to cancel any countdowns
|
// reset the secondary timer to cancel any countdowns
|
||||||
runtimeState.timer.secondaryTimer = null;
|
runtimeState.timer.secondaryTimer = null;
|
||||||
|
|
||||||
@@ -752,20 +709,18 @@ export function roll(
|
|||||||
runtimeState._startDayOffset = (findDayOffset(runtimeState.eventNow.timeStart, plannedStart) +
|
runtimeState._startDayOffset = (findDayOffset(runtimeState.eventNow.timeStart, plannedStart) +
|
||||||
runtimeState.eventNow.dayOffset) as Day;
|
runtimeState.eventNow.dayOffset) as Day;
|
||||||
// backdate _startEpoch to when the event conceptually started
|
// backdate _startEpoch to when the event conceptually started
|
||||||
const timeElapsed = timeCore.elapsedTime(timeCore.toTimeOfDay(runtimeState._now), plannedStart as TimeOfDay);
|
const timeElapsed = timeCore.elapsedTime(runtimeState.clock, plannedStart as TimeOfDay);
|
||||||
runtimeState._startEpoch = timeCore.addDuration(runtimeState._now, -timeElapsed as Duration);
|
runtimeState._startEpoch = timeCore.addDuration(epoch, -timeElapsed as Duration);
|
||||||
// calculate currentDay from the backdated epoch
|
// calculate currentDay from the backdated epoch
|
||||||
runtimeState.rundown.currentDay = timeCore.combineValues(
|
runtimeState.rundown.currentDay =
|
||||||
runtimeState._startDayOffset,
|
runtimeState._startDayOffset + timeCore.daysSinceStart(runtimeState._startEpoch, epoch);
|
||||||
timeCore.daysSinceStart(runtimeState._startEpoch, runtimeState._now),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
runtimeState._timer.secondaryTarget = normaliseRollStart(
|
runtimeState._timer.secondaryTarget = normaliseRollStart(
|
||||||
runtimeState.eventNow.timeStart as TimeOfDay,
|
runtimeState.eventNow.timeStart,
|
||||||
offsetClock,
|
offsetClock,
|
||||||
) as TimeOfDay;
|
) as TimeOfDay;
|
||||||
runtimeState.timer.secondaryTimer = (runtimeState._timer.secondaryTarget - offsetClock) as Duration;
|
runtimeState.timer.secondaryTimer = runtimeState._timer.secondaryTarget - offsetClock;
|
||||||
runtimeState.timer.phase = TimerPhase.Pending;
|
runtimeState.timer.phase = TimerPhase.Pending;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -782,9 +737,9 @@ export function roll(
|
|||||||
|
|
||||||
// account for offset but we only keep it if passed to us
|
// account for offset but we only keep it if passed to us
|
||||||
if (offset) {
|
if (offset) {
|
||||||
runtimeState.offset = { ...offset } as InternalRuntimeState['offset'];
|
runtimeState.offset = { ...offset };
|
||||||
}
|
}
|
||||||
const offsetClock = timeCore.toTimeOfDay(runtimeState._now) - runtimeState.offset.absolute;
|
const offsetClock = runtimeState.clock - runtimeState.offset.absolute;
|
||||||
|
|
||||||
const { index, isPending } = loadRoll(rundown, metadata, offsetClock);
|
const { index, isPending } = loadRoll(rundown, metadata, offsetClock);
|
||||||
|
|
||||||
@@ -811,7 +766,7 @@ export function roll(
|
|||||||
runtimeState.timer.phase = TimerPhase.Pending;
|
runtimeState.timer.phase = TimerPhase.Pending;
|
||||||
// we need to normalise start time in case it is the day after
|
// we need to normalise start time in case it is the day after
|
||||||
runtimeState._timer.secondaryTarget = normaliseRollStart(runtimeState.eventNow.timeStart, offsetClock) as TimeOfDay;
|
runtimeState._timer.secondaryTarget = normaliseRollStart(runtimeState.eventNow.timeStart, offsetClock) as TimeOfDay;
|
||||||
runtimeState.timer.secondaryTimer = (runtimeState._timer.secondaryTarget - offsetClock) as Duration;
|
runtimeState.timer.secondaryTimer = runtimeState._timer.secondaryTarget - offsetClock;
|
||||||
|
|
||||||
// preload timer properties
|
// preload timer properties
|
||||||
runtimeState.timer.duration = calculateDuration(runtimeState.eventNow.timeStart, runtimeState.eventNow.timeEnd);
|
runtimeState.timer.duration = calculateDuration(runtimeState.eventNow.timeStart, runtimeState.eventNow.timeEnd);
|
||||||
@@ -834,18 +789,18 @@ export function roll(
|
|||||||
* when we start in roll mode
|
* when we start in roll mode
|
||||||
* we need to backtrace all times to the supposed start time of the event
|
* we need to backtrace all times to the supposed start time of the event
|
||||||
*/
|
*/
|
||||||
const plannedStart = runtimeState.eventNow.timeStart as TimeOfDay;
|
const plannedStart = runtimeState.eventNow.timeStart;
|
||||||
const endTime =
|
const endTime =
|
||||||
runtimeState.eventNow.timeEnd < runtimeState.eventNow.timeStart
|
runtimeState.eventNow.timeEnd < runtimeState.eventNow.timeStart
|
||||||
? runtimeState.eventNow.timeEnd + dayInMs
|
? runtimeState.eventNow.timeEnd + dayInMs
|
||||||
: runtimeState.eventNow.timeEnd;
|
: runtimeState.eventNow.timeEnd;
|
||||||
runtimeState.timer.startedAt = timeCore.toInstant(plannedStart, runtimeState._now);
|
runtimeState.timer.startedAt = plannedStart;
|
||||||
runtimeState.timer.expectedFinish = endTime as TimeOfDay;
|
runtimeState.timer.expectedFinish = endTime;
|
||||||
|
|
||||||
// state catch up
|
// state catch up
|
||||||
runtimeState.timer.duration = calculateDuration(runtimeState.eventNow.timeStart, endTime);
|
runtimeState.timer.duration = calculateDuration(runtimeState.eventNow.timeStart, endTime);
|
||||||
runtimeState.timer.current = getCurrent(runtimeState);
|
runtimeState.timer.current = getCurrent(runtimeState);
|
||||||
runtimeState.timer.elapsed = 0 as Duration;
|
runtimeState.timer.elapsed = 0;
|
||||||
|
|
||||||
// update runtime
|
// update runtime
|
||||||
runtimeState.rundown.actualStart = plannedStart;
|
runtimeState.rundown.actualStart = plannedStart;
|
||||||
@@ -858,11 +813,11 @@ export function roll(
|
|||||||
runtimeState._startDayOffset = (findDayOffset(runtimeState.eventNow.timeStart, plannedStart) +
|
runtimeState._startDayOffset = (findDayOffset(runtimeState.eventNow.timeStart, plannedStart) +
|
||||||
runtimeState.eventNow.dayOffset) as Day;
|
runtimeState.eventNow.dayOffset) as Day;
|
||||||
// backdate _startEpoch to when the event conceptually started
|
// backdate _startEpoch to when the event conceptually started
|
||||||
const timeElapsed = timeCore.elapsedTime(timeCore.toTimeOfDay(runtimeState._now), plannedStart as TimeOfDay);
|
const timeElapsed = timeCore.elapsedTime(runtimeState.clock, plannedStart as TimeOfDay);
|
||||||
runtimeState._startEpoch = timeCore.addDuration(runtimeState._now, -timeElapsed as Duration);
|
runtimeState._startEpoch = timeCore.addDuration(epoch, -timeElapsed as Duration);
|
||||||
// calculate currentDay from the backdated epoch
|
// calculate currentDay from the backdated epoch
|
||||||
runtimeState.rundown.currentDay = (runtimeState._startDayOffset +
|
runtimeState.rundown.currentDay = (runtimeState._startDayOffset +
|
||||||
timeCore.daysSinceStart(runtimeState._startEpoch, runtimeState._now)) as Day;
|
timeCore.daysSinceStart(runtimeState._startEpoch, epoch)) as Day;
|
||||||
|
|
||||||
return { eventId: runtimeState.eventNow.id, didStart: true };
|
return { eventId: runtimeState.eventNow.id, didStart: true };
|
||||||
}
|
}
|
||||||
@@ -897,11 +852,7 @@ function getExpectedTimes(state = runtimeState) {
|
|||||||
plannedStart,
|
plannedStart,
|
||||||
actualStart,
|
actualStart,
|
||||||
});
|
});
|
||||||
state.offset.expectedGroupEnd = getExpectedEnd(
|
state.offset.expectedGroupEnd = getExpectedEnd(lastEvent, lastEventExpectedStart, state.rundown.currentDay!);
|
||||||
lastEvent,
|
|
||||||
lastEventExpectedStart,
|
|
||||||
state.rundown.currentDay!,
|
|
||||||
) as TimeOfDay;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -918,7 +869,7 @@ function getExpectedTimes(state = runtimeState) {
|
|||||||
plannedStart,
|
plannedStart,
|
||||||
actualStart,
|
actualStart,
|
||||||
});
|
});
|
||||||
state.offset.expectedFlagStart = expectedStart as TimeOfDay;
|
state.offset.expectedFlagStart = expectedStart;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -933,7 +884,7 @@ function getExpectedTimes(state = runtimeState) {
|
|||||||
plannedStart,
|
plannedStart,
|
||||||
actualStart,
|
actualStart,
|
||||||
});
|
});
|
||||||
state.offset.expectedRundownEnd = getExpectedEnd(event, expectedStart, state.rundown.currentDay!) as TimeOfDay;
|
state.offset.expectedRundownEnd = getExpectedEnd(event, expectedStart, state.rundown.currentDay!);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1006,13 +957,13 @@ export function loadGroupFlagAndEnd(
|
|||||||
if (!foundFlag && metadata.flags.includes(entry.id)) {
|
if (!foundFlag && metadata.flags.includes(entry.id)) {
|
||||||
foundFlag = true;
|
foundFlag = true;
|
||||||
state.eventFlag = entry as PlayableEvent; // we know it is playable as it is coming from the playableEventOrder list
|
state.eventFlag = entry as PlayableEvent; // we know it is playable as it is coming from the playableEventOrder list
|
||||||
state._flag = { event: entry, isLinkedToLoaded, accumulatedGap: accumulatedGap as Duration };
|
state._flag = { event: entry, isLinkedToLoaded, accumulatedGap };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!foundGroupEnd && entry.id === lastEventInGroup?.id) {
|
if (!foundGroupEnd && entry.id === lastEventInGroup?.id) {
|
||||||
foundGroupEnd = true;
|
foundGroupEnd = true;
|
||||||
state._group = { event: lastEventInGroup, isLinkedToLoaded, accumulatedGap: accumulatedGap as Duration };
|
state._group = { event: lastEventInGroup, isLinkedToLoaded, accumulatedGap };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1020,7 +971,7 @@ export function loadGroupFlagAndEnd(
|
|||||||
const lastID = playableEventOrder.at(-1);
|
const lastID = playableEventOrder.at(-1);
|
||||||
const lastEvent = lastID ? (entries[lastID] as OntimeEvent) : null;
|
const lastEvent = lastID ? (entries[lastID] as OntimeEvent) : null;
|
||||||
if (lastEvent) {
|
if (lastEvent) {
|
||||||
state._end = { event: lastEvent, isLinkedToLoaded, accumulatedGap: accumulatedGap as Duration };
|
state._end = { event: lastEvent, isLinkedToLoaded, accumulatedGap };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
export enum RefetchKey {
|
export enum RefetchKey {
|
||||||
All = 'all',
|
All = 'all',
|
||||||
CustomFields = 'custom-fields',
|
CustomFields = 'custom-fields',
|
||||||
|
ProjectFiles = 'project-files',
|
||||||
ProjectData = 'project-data',
|
ProjectData = 'project-data',
|
||||||
ProjectRundowns = 'project-rundowns',
|
ProjectRundowns = 'project-rundowns',
|
||||||
Report = 'report',
|
Report = 'report',
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import type { Duration, OntimeEvent } from 'ontime-types';
|
import type { OntimeEvent } from 'ontime-types';
|
||||||
import { TimeStrategy } from 'ontime-types';
|
import { TimeStrategy } from 'ontime-types';
|
||||||
|
|
||||||
import { dayInMs } from '../date-utils/conversionUtils.js';
|
import { dayInMs } from '../date-utils/conversionUtils.js';
|
||||||
@@ -103,17 +103,16 @@ export function validateTimes(
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* @description calculates event duration considering midnight
|
* @description calculates event duration considering midnight
|
||||||
* @deprecated - duration is the more useful value from the event data, prefer using that directly
|
|
||||||
* @param {number} timeStart
|
* @param {number} timeStart
|
||||||
* @param {number} timeEnd
|
* @param {number} timeEnd
|
||||||
* @returns {number}
|
* @returns {number}
|
||||||
*/
|
*/
|
||||||
export function calculateDuration(timeStart: number, timeEnd: number): Duration {
|
export function calculateDuration(timeStart: number, timeEnd: number): number {
|
||||||
// Durations must be positive
|
// Durations must be positive
|
||||||
if (timeEnd < timeStart) {
|
if (timeEnd < timeStart) {
|
||||||
return (timeEnd + dayInMs - timeStart) as Duration;
|
return timeEnd + dayInMs - timeStart;
|
||||||
}
|
}
|
||||||
return (timeEnd - timeStart) as Duration;
|
return timeEnd - timeStart;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function calculateEnd(timeStart: number, duration: number): number {
|
export function calculateEnd(timeStart: number, duration: number): number {
|
||||||
|
|||||||
Reference in New Issue
Block a user