mirror of
https://github.com/cpvalente/ontime.git
synced 2026-08-21 23:19:09 +00:00
refactor: normalise data (#756)
* chore: remove legal from bundle * refactor: create normalised dataset * refactor: cuesheet uses flat rundown * refactor: multi-selection * refactor: prevent stale data on server restart * refactor: increase ID size * chore: instrument operation * chore: update csv tests * fix: resolve directory to test-db (#758)
This commit is contained in:
@@ -1,5 +1,5 @@
|
|||||||
import axios from 'axios';
|
import axios from 'axios';
|
||||||
import { GetRundownCached, OntimeRundown, OntimeRundownEntry } from 'ontime-types';
|
import { OntimeEvent, OntimeRundown, OntimeRundownEntry, RundownCached } from 'ontime-types';
|
||||||
|
|
||||||
import { rundownURL } from './apiConstants';
|
import { rundownURL } from './apiConstants';
|
||||||
|
|
||||||
@@ -7,7 +7,7 @@ import { rundownURL } from './apiConstants';
|
|||||||
* @description HTTP request to fetch all events
|
* @description HTTP request to fetch all events
|
||||||
* @return {Promise}
|
* @return {Promise}
|
||||||
*/
|
*/
|
||||||
export async function fetchCachedRundown(): Promise<GetRundownCached> {
|
export async function fetchCachedRundown(): Promise<RundownCached> {
|
||||||
const res = await axios.get(`${rundownURL}/cached`);
|
const res = await axios.get(`${rundownURL}/cached`);
|
||||||
return res.data;
|
return res.data;
|
||||||
}
|
}
|
||||||
@@ -26,7 +26,7 @@ export async function fetchRundown(): Promise<OntimeRundown> {
|
|||||||
* @description HTTP request to post new event
|
* @description HTTP request to post new event
|
||||||
* @return {Promise}
|
* @return {Promise}
|
||||||
*/
|
*/
|
||||||
export async function requestPostEvent(data: OntimeRundownEntry) {
|
export async function requestPostEvent(data: Partial<OntimeRundownEntry>) {
|
||||||
return axios.post(rundownURL, data);
|
return axios.post(rundownURL, data);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -39,7 +39,7 @@ export async function requestPutEvent(data: Partial<OntimeRundownEntry>) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type BatchEditEntry = {
|
type BatchEditEntry = {
|
||||||
data: Partial<OntimeRundownEntry>;
|
data: Partial<OntimeEvent>;
|
||||||
ids: string[];
|
ids: string[];
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -1,15 +1,16 @@
|
|||||||
|
import { useEffect, useState } from 'react';
|
||||||
import { useQuery } from '@tanstack/react-query';
|
import { useQuery } from '@tanstack/react-query';
|
||||||
import { GetRundownCached } from 'ontime-types';
|
import { NormalisedRundown, OntimeRundown, RundownCached } from 'ontime-types';
|
||||||
|
|
||||||
import { queryRefetchInterval } from '../../ontimeConfig';
|
import { queryRefetchInterval } from '../../ontimeConfig';
|
||||||
import { RUNDOWN } from '../api/apiConstants';
|
import { RUNDOWN } from '../api/apiConstants';
|
||||||
import { fetchCachedRundown } from '../api/eventsApi';
|
import { fetchCachedRundown } from '../api/eventsApi';
|
||||||
|
|
||||||
const cachedRundownPlaceholder = { rundown: [], revision: -1 };
|
// revision is -1 so that the remote revision is higher
|
||||||
|
const cachedRundownPlaceholder = { order: [] as string[], rundown: {} as NormalisedRundown, revision: -1 };
|
||||||
|
|
||||||
// TODO: can we leverage structural sharing to see if data has changed?
|
|
||||||
export default function useRundown() {
|
export default function useRundown() {
|
||||||
const { data, status, isError, refetch, isFetching } = useQuery<GetRundownCached>({
|
const { data, status, isError, refetch, isFetching } = useQuery<RundownCached>({
|
||||||
queryKey: RUNDOWN,
|
queryKey: RUNDOWN,
|
||||||
queryFn: fetchCachedRundown,
|
queryFn: fetchCachedRundown,
|
||||||
placeholderData: cachedRundownPlaceholder,
|
placeholderData: cachedRundownPlaceholder,
|
||||||
@@ -17,13 +18,24 @@ export default function useRundown() {
|
|||||||
retryDelay: (attempt) => attempt * 2500,
|
retryDelay: (attempt) => attempt * 2500,
|
||||||
refetchInterval: queryRefetchInterval,
|
refetchInterval: queryRefetchInterval,
|
||||||
networkMode: 'always',
|
networkMode: 'always',
|
||||||
// structuralSharing: (oldData: GetRundownCached | undefined, newData: GetRundownCached) => {
|
|
||||||
// if (oldData === undefined) {
|
|
||||||
// return cachedRundownPlaceholder;
|
|
||||||
// }
|
|
||||||
// const hasDataChanged = oldData?.revision === newData.revision;
|
|
||||||
// return hasDataChanged ? oldData : newData;
|
|
||||||
// },
|
|
||||||
});
|
});
|
||||||
return { data: data?.rundown ?? [], status, isError, refetch, isFetching };
|
return { data: data ?? cachedRundownPlaceholder, status, isError, refetch, isFetching };
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useFlatRundown() {
|
||||||
|
const { data, status } = useRundown();
|
||||||
|
|
||||||
|
const [prevRevision, setPrevRevision] = useState<number>(-1);
|
||||||
|
const [flatRunDown, setFlatRunDown] = useState<OntimeRundown>([]);
|
||||||
|
|
||||||
|
// update data whenever the revision changes
|
||||||
|
useEffect(() => {
|
||||||
|
if (data.revision !== -1 && data.revision !== prevRevision) {
|
||||||
|
const flatRundown = data.order.map((id) => data.rundown[id]);
|
||||||
|
setFlatRunDown(flatRundown);
|
||||||
|
setPrevRevision(data.revision);
|
||||||
|
}
|
||||||
|
}, [data.order, data.revision, data.rundown, prevRevision]);
|
||||||
|
|
||||||
|
return { data: flatRunDown, status };
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { useCallback } from 'react';
|
import { useCallback } from 'react';
|
||||||
import { useMutation, useQueryClient } from '@tanstack/react-query';
|
import { useMutation, useQueryClient } from '@tanstack/react-query';
|
||||||
import { GetRundownCached, isOntimeEvent, OntimeRundownEntry } from 'ontime-types';
|
import { isOntimeEvent, OntimeEvent, OntimeRundownEntry, RundownCached } from 'ontime-types';
|
||||||
import { getPreviousEvent, swapOntimeEvents } from 'ontime-utils';
|
import { reorderArray, swapEventData } from 'ontime-utils';
|
||||||
|
|
||||||
import { RUNDOWN } from '../api/apiConstants';
|
import { RUNDOWN } from '../api/apiConstants';
|
||||||
import { logAxiosError } from '../api/apiUtils';
|
import { logAxiosError } from '../api/apiUtils';
|
||||||
@@ -34,8 +34,6 @@ export const useEventAction = () => {
|
|||||||
* @private
|
* @private
|
||||||
*/
|
*/
|
||||||
const _addEventMutation = useMutation({
|
const _addEventMutation = useMutation({
|
||||||
// Mutation finished, failed or successful
|
|
||||||
// Fetch anyway, just to be sure
|
|
||||||
mutationFn: requestPostEvent,
|
mutationFn: requestPostEvent,
|
||||||
onSettled: () => {
|
onSettled: () => {
|
||||||
queryClient.invalidateQueries({ queryKey: RUNDOWN });
|
queryClient.invalidateQueries({ queryKey: RUNDOWN });
|
||||||
@@ -69,10 +67,12 @@ export const useEventAction = () => {
|
|||||||
after: options?.after,
|
after: options?.after,
|
||||||
};
|
};
|
||||||
|
|
||||||
const rundown = queryClient.getQueryData<GetRundownCached>(RUNDOWN)?.rundown ?? [];
|
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion -- we know this has a value
|
||||||
|
const rundownData = queryClient.getQueryData<RundownCached>(RUNDOWN)!;
|
||||||
|
const { rundown } = rundownData;
|
||||||
|
|
||||||
if (applicationOptions.startTimeIsLastEnd && applicationOptions?.lastEventId) {
|
if (applicationOptions.startTimeIsLastEnd && applicationOptions?.lastEventId) {
|
||||||
const previousEvent = rundown.find((event) => event.id === applicationOptions.lastEventId);
|
const previousEvent = rundown[applicationOptions.lastEventId];
|
||||||
if (isOntimeEvent(previousEvent)) {
|
if (isOntimeEvent(previousEvent)) {
|
||||||
newEvent.timeStart = previousEvent.timeEnd;
|
newEvent.timeStart = previousEvent.timeEnd;
|
||||||
newEvent.timeEnd = previousEvent.timeEnd;
|
newEvent.timeEnd = previousEvent.timeEnd;
|
||||||
@@ -90,7 +90,6 @@ export const useEventAction = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// @ts-expect-error -- we know that the object is well formed now
|
|
||||||
await _addEventMutation.mutateAsync(newEvent);
|
await _addEventMutation.mutateAsync(newEvent);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logAxiosError('Failed adding event', error);
|
logAxiosError('Failed adding event', error);
|
||||||
@@ -111,18 +110,15 @@ export const useEventAction = () => {
|
|||||||
await queryClient.cancelQueries({ queryKey: RUNDOWN });
|
await queryClient.cancelQueries({ queryKey: RUNDOWN });
|
||||||
|
|
||||||
// Snapshot the previous value
|
// Snapshot the previous value
|
||||||
const previousData = queryClient.getQueryData<GetRundownCached>(RUNDOWN);
|
const previousData = queryClient.getQueryData<RundownCached>(RUNDOWN);
|
||||||
|
const eventId = newEvent.id;
|
||||||
|
|
||||||
if (previousData) {
|
if (previousData && eventId) {
|
||||||
// optimistically update object
|
// optimistically update object
|
||||||
const optimisticRundown = [...previousData.rundown];
|
const newRundown = { ...previousData.rundown };
|
||||||
const index = optimisticRundown.findIndex((event) => event.id === newEvent.id);
|
// @ts-expect-error -- we expect the events to be of same type
|
||||||
if (index > -1) {
|
newRundown[eventId] = { ...newRundown[eventId], ...newEvent };
|
||||||
// @ts-expect-error -- we expect the event types to match
|
queryClient.setQueryData(RUNDOWN, { order: previousData.order, rundown: newRundown, revision: -1 });
|
||||||
optimisticRundown[index] = { ...optimisticRundown[index], ...newEvent };
|
|
||||||
|
|
||||||
queryClient.setQueryData(RUNDOWN, { rundown: optimisticRundown, revision: -1 });
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Return a context with the previous and new events
|
// Return a context with the previous and new events
|
||||||
@@ -161,14 +157,25 @@ export const useEventAction = () => {
|
|||||||
const updateTimer = useCallback(
|
const updateTimer = useCallback(
|
||||||
async (eventId: string, field: TimeField, value: string) => {
|
async (eventId: string, field: TimeField, value: string) => {
|
||||||
const getPreviousEnd = (): number => {
|
const getPreviousEnd = (): number => {
|
||||||
const rundown = queryClient.getQueryData<GetRundownCached>(RUNDOWN)?.rundown ?? [];
|
const cachedRundown = queryClient.getQueryData<RundownCached>(RUNDOWN);
|
||||||
if (rundown) {
|
|
||||||
const { previousEvent } = getPreviousEvent(rundown, eventId);
|
if (!cachedRundown?.order || !cachedRundown?.rundown) {
|
||||||
if (previousEvent) {
|
return 0;
|
||||||
return previousEvent.timeEnd;
|
}
|
||||||
|
|
||||||
|
const index = cachedRundown.order.indexOf(eventId);
|
||||||
|
if (index === 0) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
let previousEnd = 0;
|
||||||
|
for (let i = index - 1; i >= 0; i--) {
|
||||||
|
const event = cachedRundown.rundown[cachedRundown.order[i]];
|
||||||
|
if (isOntimeEvent(event)) {
|
||||||
|
previousEnd = event.timeEnd;
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return 0;
|
return previousEnd;
|
||||||
};
|
};
|
||||||
|
|
||||||
let newValMillis = 0;
|
let newValMillis = 0;
|
||||||
@@ -209,25 +216,26 @@ export const useEventAction = () => {
|
|||||||
await queryClient.cancelQueries({ queryKey: RUNDOWN });
|
await queryClient.cancelQueries({ queryKey: RUNDOWN });
|
||||||
|
|
||||||
// Snapshot the previous value
|
// Snapshot the previous value
|
||||||
const previousEvents = queryClient.getQueryData<GetRundownCached>(RUNDOWN);
|
const previousEvents = queryClient.getQueryData<RundownCached>(RUNDOWN);
|
||||||
|
|
||||||
if (previousEvents) {
|
if (previousEvents) {
|
||||||
const updatedEvents = previousEvents.rundown.map((event) => {
|
const eventIds = new Set(ids);
|
||||||
const isEventEdited = ids.includes(event.id);
|
const newRundown = { ...previousEvents.rundown };
|
||||||
|
|
||||||
if (isEventEdited && isOntimeEvent(event)) {
|
eventIds.forEach((eventId) => {
|
||||||
return {
|
if (Object.hasOwn(newRundown, eventId)) {
|
||||||
...event,
|
const event = newRundown[eventId];
|
||||||
...data,
|
if (isOntimeEvent(event)) {
|
||||||
};
|
newRundown[eventId] = {
|
||||||
|
...event,
|
||||||
|
...data,
|
||||||
|
};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return event;
|
|
||||||
});
|
});
|
||||||
|
|
||||||
queryClient.setQueryData(RUNDOWN, { rundown: updatedEvents, revision: -1 });
|
queryClient.setQueryData(RUNDOWN, { order: previousEvents.order, rundown: newRundown, revision: -1 });
|
||||||
}
|
}
|
||||||
|
|
||||||
// Return a context with the previous and new events
|
// Return a context with the previous and new events
|
||||||
return { previousEvents };
|
return { previousEvents };
|
||||||
},
|
},
|
||||||
@@ -241,7 +249,7 @@ export const useEventAction = () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const batchUpdateEvents = useCallback(
|
const batchUpdateEvents = useCallback(
|
||||||
async (data: Partial<OntimeRundownEntry>, eventIds: string[]) => {
|
async (data: Partial<OntimeEvent>, eventIds: string[]) => {
|
||||||
try {
|
try {
|
||||||
await _batchUpdateEventsMutation.mutateAsync({ ids: eventIds, data });
|
await _batchUpdateEventsMutation.mutateAsync({ ids: eventIds, data });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -263,20 +271,19 @@ export const useEventAction = () => {
|
|||||||
await queryClient.cancelQueries({ queryKey: RUNDOWN });
|
await queryClient.cancelQueries({ queryKey: RUNDOWN });
|
||||||
|
|
||||||
// Snapshot the previous value
|
// Snapshot the previous value
|
||||||
const previousData = queryClient.getQueryData<GetRundownCached>(RUNDOWN);
|
const previousData = queryClient.getQueryData<RundownCached>(RUNDOWN);
|
||||||
|
|
||||||
if (previousData) {
|
if (previousData) {
|
||||||
// optimistically update object
|
// optimistically update object
|
||||||
const optimisticRundown = [...previousData.rundown];
|
const newOrder = previousData.order.filter((id) => id !== eventId);
|
||||||
const index = optimisticRundown.findIndex((event) => event.id === eventId);
|
const newRundown = { ...previousData.rundown };
|
||||||
if (index > -1) {
|
delete newRundown[eventId];
|
||||||
optimisticRundown.splice(index, 1);
|
|
||||||
|
|
||||||
queryClient.setQueryData(RUNDOWN, {
|
queryClient.setQueryData(RUNDOWN, {
|
||||||
rundown: optimisticRundown,
|
order: newOrder,
|
||||||
revision: -1,
|
rundown: newRundown,
|
||||||
});
|
revision: -1,
|
||||||
}
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Return a context with the previous and new events
|
// Return a context with the previous and new events
|
||||||
@@ -321,10 +328,10 @@ export const useEventAction = () => {
|
|||||||
await queryClient.cancelQueries({ queryKey: RUNDOWN });
|
await queryClient.cancelQueries({ queryKey: RUNDOWN });
|
||||||
|
|
||||||
// Snapshot the previous value
|
// Snapshot the previous value
|
||||||
const previousData = queryClient.getQueryData<GetRundownCached>(RUNDOWN);
|
const previousData = queryClient.getQueryData<RundownCached>(RUNDOWN);
|
||||||
|
|
||||||
// optimistically update object
|
// optimistically update object
|
||||||
queryClient.setQueryData(RUNDOWN, { rundown: [], revision: -1 });
|
queryClient.setQueryData(RUNDOWN, { rundown: {}, order: [], revision: -1 });
|
||||||
|
|
||||||
// Return a context with the previous and new events
|
// Return a context with the previous and new events
|
||||||
return { previousData };
|
return { previousData };
|
||||||
@@ -392,15 +399,13 @@ export const useEventAction = () => {
|
|||||||
await queryClient.cancelQueries({ queryKey: RUNDOWN });
|
await queryClient.cancelQueries({ queryKey: RUNDOWN });
|
||||||
|
|
||||||
// Snapshot the previous value
|
// Snapshot the previous value
|
||||||
const previousData = queryClient.getQueryData<GetRundownCached>(RUNDOWN);
|
const previousData = queryClient.getQueryData<RundownCached>(RUNDOWN);
|
||||||
|
|
||||||
if (previousData) {
|
if (previousData) {
|
||||||
// optimistically update object
|
// optimistically update object
|
||||||
const optimisticRundown = [...previousData.rundown];
|
const newOrder = reorderArray(previousData.order, data.from, data.to);
|
||||||
const [reorderedItem] = optimisticRundown.splice(data.from, 1);
|
|
||||||
optimisticRundown.splice(data.to, 0, reorderedItem);
|
|
||||||
|
|
||||||
queryClient.setQueryData(RUNDOWN, { rundown: optimisticRundown, revision: -1 });
|
queryClient.setQueryData(RUNDOWN, { order: newOrder, rundown: previousData.rundown, revision: -1 });
|
||||||
}
|
}
|
||||||
|
|
||||||
// Return a context with the previous and new events
|
// Return a context with the previous and new events
|
||||||
@@ -450,15 +455,22 @@ export const useEventAction = () => {
|
|||||||
await queryClient.cancelQueries({ queryKey: RUNDOWN });
|
await queryClient.cancelQueries({ queryKey: RUNDOWN });
|
||||||
|
|
||||||
// Snapshot the previous value
|
// Snapshot the previous value
|
||||||
const previousData = queryClient.getQueryData<GetRundownCached>(RUNDOWN);
|
const previousData = queryClient.getQueryData<RundownCached>(RUNDOWN);
|
||||||
if (previousData) {
|
if (previousData) {
|
||||||
// optimistically update object
|
// optimistically update object
|
||||||
const fromEventIndex = previousData.rundown.findIndex((event) => event.id === from);
|
const newRundown = { ...previousData.rundown };
|
||||||
const toEventIndex = previousData.rundown.findIndex((event) => event.id === to);
|
const eventA = previousData.rundown[from];
|
||||||
|
const eventB = previousData.rundown[to];
|
||||||
|
|
||||||
const optimisticRundown = swapOntimeEvents(previousData.rundown, fromEventIndex, toEventIndex);
|
if (!isOntimeEvent(eventA) || !isOntimeEvent(eventB)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
queryClient.setQueryData(RUNDOWN, { rundown: optimisticRundown, revision: -1 });
|
const { newA, newB } = swapEventData(eventA, eventB);
|
||||||
|
newRundown[from] = newA;
|
||||||
|
newRundown[to] = newB;
|
||||||
|
|
||||||
|
queryClient.setQueryData(RUNDOWN, { order: previousData.order, rundown: newRundown, revision: -1 });
|
||||||
}
|
}
|
||||||
|
|
||||||
// Return a context with the previous events
|
// Return a context with the previous events
|
||||||
@@ -482,8 +494,6 @@ export const useEventAction = () => {
|
|||||||
*/
|
*/
|
||||||
const swapEvents = useCallback(
|
const swapEvents = useCallback(
|
||||||
async ({ from, to }: SwapEntry) => {
|
async ({ from, to }: SwapEntry) => {
|
||||||
// TODO: before calling `/swapEvents`,
|
|
||||||
// we should determine the events are of type `OntimeEvent`
|
|
||||||
try {
|
try {
|
||||||
await _swapEvents.mutateAsync({ from, to });
|
await _swapEvents.mutateAsync({ from, to });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|||||||
@@ -1,12 +1,11 @@
|
|||||||
import { useCallback, useEffect, useMemo, useState } from 'react';
|
import { useCallback, useEffect, useMemo } from 'react';
|
||||||
import { OntimeRundownEntry, ProjectData } from 'ontime-types';
|
import { OntimeRundownEntry, ProjectData } from 'ontime-types';
|
||||||
|
|
||||||
import Empty from '../../common/components/state/Empty';
|
import Empty from '../../common/components/state/Empty';
|
||||||
import { useEventAction } from '../../common/hooks/useEventAction';
|
import { useEventAction } from '../../common/hooks/useEventAction';
|
||||||
import { useCuesheet } from '../../common/hooks/useSocket';
|
import { useCuesheet } from '../../common/hooks/useSocket';
|
||||||
import useRundown from '../../common/hooks-query/useRundown';
|
import { useFlatRundown } from '../../common/hooks-query/useRundown';
|
||||||
import useUserFields from '../../common/hooks-query/useUserFields';
|
import useUserFields from '../../common/hooks-query/useUserFields';
|
||||||
import ExportModal, { ExportType } from '../modals/export-modal/ExportModal';
|
|
||||||
|
|
||||||
import CuesheetProgress from './cuesheet-progress/CuesheetProgress';
|
import CuesheetProgress from './cuesheet-progress/CuesheetProgress';
|
||||||
import CuesheetTableHeader from './cuesheet-table-header/CuesheetTableHeader';
|
import CuesheetTableHeader from './cuesheet-table-header/CuesheetTableHeader';
|
||||||
@@ -17,13 +16,12 @@ import { makeCSV, makeTable } from './cuesheetUtils';
|
|||||||
import styles from './CuesheetWrapper.module.scss';
|
import styles from './CuesheetWrapper.module.scss';
|
||||||
|
|
||||||
export default function CuesheetWrapper() {
|
export default function CuesheetWrapper() {
|
||||||
const { data: rundown } = useRundown();
|
// TODO: can we use the normalised rundown for the table?
|
||||||
|
const { data: flatRundown, status: rundownStatus } = useFlatRundown();
|
||||||
const { data: userFields } = useUserFields();
|
const { data: userFields } = useUserFields();
|
||||||
const { updateEvent } = useEventAction();
|
const { updateEvent } = useEventAction();
|
||||||
const featureData = useCuesheet();
|
const featureData = useCuesheet();
|
||||||
const columns = useMemo(() => makeCuesheetColumns(userFields), [userFields]);
|
const columns = useMemo(() => makeCuesheetColumns(userFields), [userFields]);
|
||||||
const [isModalOpen, setIsModalOpen] = useState(false);
|
|
||||||
const [headerData, setheaderData] = useState<ProjectData | null>(null);
|
|
||||||
|
|
||||||
// Set window title
|
// Set window title
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -32,7 +30,7 @@ export default function CuesheetWrapper() {
|
|||||||
|
|
||||||
const handleUpdate = useCallback(
|
const handleUpdate = useCallback(
|
||||||
async (rowIndex: number, accessor: keyof OntimeRundownEntry, payload: unknown) => {
|
async (rowIndex: number, accessor: keyof OntimeRundownEntry, payload: unknown) => {
|
||||||
if (!rundown) {
|
if (!flatRundown || rundownStatus !== 'success') {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -41,7 +39,7 @@ export default function CuesheetWrapper() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// check if value is the same
|
// check if value is the same
|
||||||
const event = rundown[rowIndex];
|
const event = flatRundown[rowIndex];
|
||||||
if (!event) {
|
if (!event) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -69,41 +67,21 @@ export default function CuesheetWrapper() {
|
|||||||
console.error(error);
|
console.error(error);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[updateEvent, rundown],
|
[flatRundown, rundownStatus, updateEvent],
|
||||||
);
|
);
|
||||||
|
|
||||||
const exportHandler = useCallback(
|
const exportHandler = useCallback(
|
||||||
(headerData: ProjectData, exportType: ExportType) => {
|
(headerData: ProjectData) => {
|
||||||
if (!headerData || !rundown || !userFields) {
|
if (!userFields || !flatRundown || rundownStatus !== 'success') {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
const sheetData = makeTable(headerData, flatRundown, userFields);
|
||||||
|
const csvContent = makeCSV(sheetData);
|
||||||
|
|
||||||
let fileName = '';
|
const fileName = 'ontime rundown.csv';
|
||||||
let url = '';
|
|
||||||
|
|
||||||
if (exportType === 'json') {
|
const blob = new Blob([csvContent], { type: 'text/csv;charset=utf-8;' });
|
||||||
const jsonContent = JSON.stringify({
|
const url = URL.createObjectURL(blob);
|
||||||
headerData,
|
|
||||||
rundown,
|
|
||||||
userFields,
|
|
||||||
});
|
|
||||||
|
|
||||||
fileName = 'ontime export.json';
|
|
||||||
|
|
||||||
const blob = new Blob([jsonContent], { type: 'application/json;charset=utf-8;' });
|
|
||||||
url = URL.createObjectURL(blob);
|
|
||||||
} else if (exportType === 'csv') {
|
|
||||||
const sheetData = makeTable(headerData, rundown, userFields);
|
|
||||||
const csvContent = makeCSV(sheetData);
|
|
||||||
|
|
||||||
fileName = 'ontime export.csv';
|
|
||||||
|
|
||||||
const blob = new Blob([csvContent], { type: 'text/csv;charset=utf-8;' });
|
|
||||||
url = URL.createObjectURL(blob);
|
|
||||||
} else {
|
|
||||||
console.error('Invalid export type: ', exportType);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const link = document.createElement('a');
|
const link = document.createElement('a');
|
||||||
link.setAttribute('href', url);
|
link.setAttribute('href', url);
|
||||||
@@ -114,36 +92,23 @@ export default function CuesheetWrapper() {
|
|||||||
URL.revokeObjectURL(url);
|
URL.revokeObjectURL(url);
|
||||||
return;
|
return;
|
||||||
},
|
},
|
||||||
[rundown, userFields],
|
[flatRundown, rundownStatus, userFields],
|
||||||
);
|
);
|
||||||
|
|
||||||
const onModalClose = (exportType?: ExportType) => {
|
if (!userFields || !flatRundown || rundownStatus !== 'success') {
|
||||||
setIsModalOpen(false);
|
|
||||||
|
|
||||||
if (!exportType) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (headerData) {
|
|
||||||
exportHandler(headerData, exportType);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleOpenModal = (projectData: ProjectData) => {
|
|
||||||
setheaderData(projectData);
|
|
||||||
setIsModalOpen(true);
|
|
||||||
};
|
|
||||||
|
|
||||||
if (!rundown || !userFields) {
|
|
||||||
return <Empty text='Loading...' />;
|
return <Empty text='Loading...' />;
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={styles.tableWrapper} data-testid='cuesheet'>
|
<div className={styles.tableWrapper} data-testid='cuesheet'>
|
||||||
<CuesheetTableHeader handleExport={handleOpenModal} featureData={featureData} />
|
<CuesheetTableHeader handleExport={exportHandler} featureData={featureData} />
|
||||||
<CuesheetProgress />
|
<CuesheetProgress />
|
||||||
<Cuesheet data={rundown} columns={columns} handleUpdate={handleUpdate} selectedId={featureData.selectedEventId} />
|
<Cuesheet
|
||||||
<ExportModal isOpen={isModalOpen} onClose={onModalClose} />
|
data={flatRundown}
|
||||||
|
columns={columns}
|
||||||
|
handleUpdate={handleUpdate}
|
||||||
|
selectedId={featureData.selectedEventId}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,25 +3,14 @@
|
|||||||
exports[`makeTable() > returns array of arrays with given fields 1`] = `
|
exports[`makeTable() > returns array of arrays with given fields 1`] = `
|
||||||
[
|
[
|
||||||
[
|
[
|
||||||
"Ontime · Schedule Template",
|
"Ontime · Rundown export",
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
"Project Title",
|
"Project title: test title",
|
||||||
"",
|
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
"Project Description",
|
"Project description: test description",
|
||||||
"",
|
|
||||||
],
|
],
|
||||||
[
|
|
||||||
"Public URL",
|
|
||||||
"",
|
|
||||||
],
|
|
||||||
[
|
|
||||||
"Backstage URL",
|
|
||||||
"",
|
|
||||||
],
|
|
||||||
[],
|
|
||||||
[
|
[
|
||||||
"Time Start",
|
"Time Start",
|
||||||
"Time End",
|
"Time End",
|
||||||
|
|||||||
@@ -59,7 +59,10 @@ describe('parseField()', () => {
|
|||||||
|
|
||||||
describe('makeTable()', () => {
|
describe('makeTable()', () => {
|
||||||
it('returns array of arrays with given fields', () => {
|
it('returns array of arrays with given fields', () => {
|
||||||
const headerData = {};
|
const headerData = {
|
||||||
|
title: 'test title',
|
||||||
|
description: 'test description',
|
||||||
|
};
|
||||||
const tableData = [
|
const tableData = [
|
||||||
{
|
{
|
||||||
title: 'test title 1',
|
title: 'test title 1',
|
||||||
|
|||||||
+9
-5
@@ -9,7 +9,7 @@ $active-colour: $gray-500;
|
|||||||
}
|
}
|
||||||
|
|
||||||
@mixin time {
|
@mixin time {
|
||||||
font-family: "Open Sans Light", $ontime-font-family;
|
font-family: 'Open Sans Light', $ontime-font-family;
|
||||||
font-size: 2rem;
|
font-size: 2rem;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
@@ -22,8 +22,7 @@ $active-colour: $gray-500;
|
|||||||
height: max-content;
|
height: max-content;
|
||||||
column-gap: 2rem;
|
column-gap: 2rem;
|
||||||
|
|
||||||
grid-template-areas:
|
grid-template-areas: 'event playback timer clock actions';
|
||||||
'event playback timer clock actions';
|
|
||||||
grid-template-columns: 1fr auto auto auto auto;
|
grid-template-columns: 1fr auto auto auto auto;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-items: center;
|
justify-items: center;
|
||||||
@@ -91,11 +90,12 @@ $active-colour: $gray-500;
|
|||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 0.5rem;
|
gap: 0.5rem;
|
||||||
font-size: 1.125rem;
|
|
||||||
color: $label-colour;
|
color: $label-colour;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
|
font-size: 1rem;
|
||||||
|
|
||||||
.actionIcon {
|
.actionIcon,
|
||||||
|
.actionText {
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
|
||||||
&.enabled {
|
&.enabled {
|
||||||
@@ -106,6 +106,10 @@ $active-colour: $gray-500;
|
|||||||
color: $active-colour;
|
color: $active-colour;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.actionIcon {
|
||||||
|
font-size: 1.25rem;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (min-width: 1200px) {
|
@media (min-width: 1200px) {
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import { Playback, ProjectData } from 'ontime-types';
|
|||||||
import PlaybackIcon from '../../../common/components/playback-icon/PlaybackIcon';
|
import PlaybackIcon from '../../../common/components/playback-icon/PlaybackIcon';
|
||||||
import useFullscreen from '../../../common/hooks/useFullscreen';
|
import useFullscreen from '../../../common/hooks/useFullscreen';
|
||||||
import useProjectData from '../../../common/hooks-query/useProjectData';
|
import useProjectData from '../../../common/hooks-query/useProjectData';
|
||||||
|
import { cx } from '../../../common/utils/styleUtils';
|
||||||
import { tooltipDelayFast } from '../../../ontimeConfig';
|
import { tooltipDelayFast } from '../../../ontimeConfig';
|
||||||
import { useCuesheetSettings } from '../store/CuesheetSettings';
|
import { useCuesheetSettings } from '../store/CuesheetSettings';
|
||||||
|
|
||||||
@@ -58,12 +59,18 @@ export default function CuesheetTableHeader({ handleExport, featureData }: Cuesh
|
|||||||
<CuesheetTableHeaderTimers />
|
<CuesheetTableHeaderTimers />
|
||||||
<div className={style.headerActions}>
|
<div className={style.headerActions}>
|
||||||
<Tooltip openDelay={tooltipDelayFast} label='Toggle follow'>
|
<Tooltip openDelay={tooltipDelayFast} label='Toggle follow'>
|
||||||
<span onClick={() => toggleFollow()} className={`${style.actionIcon} ${followSelected ? style.enabled : ''}`}>
|
<span
|
||||||
|
onClick={() => toggleFollow()}
|
||||||
|
className={cx([style.actionIcon, followSelected ? style.enabled : null])}
|
||||||
|
>
|
||||||
<IoLocate />
|
<IoLocate />
|
||||||
</span>
|
</span>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
<Tooltip openDelay={tooltipDelayFast} label='Toggle settings'>
|
<Tooltip openDelay={tooltipDelayFast} label='Toggle settings'>
|
||||||
<span onClick={() => toggleSettings()} className={`${style.actionIcon} ${showSettings ? style.enabled : ''}`}>
|
<span
|
||||||
|
onClick={() => toggleSettings()}
|
||||||
|
className={cx([style.actionIcon, showSettings ? style.enabled : null])}
|
||||||
|
>
|
||||||
<IoSettingsOutline />
|
<IoSettingsOutline />
|
||||||
</span>
|
</span>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
@@ -73,8 +80,8 @@ export default function CuesheetTableHeader({ handleExport, featureData }: Cuesh
|
|||||||
</span>
|
</span>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
<Tooltip openDelay={tooltipDelayFast} label='Export rundown'>
|
<Tooltip openDelay={tooltipDelayFast} label='Export rundown'>
|
||||||
<span className={style.actionIcon} onClick={exportProject}>
|
<span className={style.actionText} onClick={exportProject}>
|
||||||
Export
|
Export CSV
|
||||||
</span>
|
</span>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -39,14 +39,9 @@ export const parseField = <T extends OntimeEntryCommonKeys>(field: T, data: unkn
|
|||||||
* @return {(string[])[]}
|
* @return {(string[])[]}
|
||||||
*/
|
*/
|
||||||
export const makeTable = (headerData: ProjectData, rundown: OntimeRundown, userFields: UserFields): string[][] => {
|
export const makeTable = (headerData: ProjectData, rundown: OntimeRundown, userFields: UserFields): string[][] => {
|
||||||
const data = [
|
const data = [['Ontime · Rundown export']];
|
||||||
['Ontime · Schedule Template'],
|
if (headerData.title) data.push([`Project title: ${headerData.title}`]);
|
||||||
['Project Title', headerData?.title || ''],
|
if (headerData.description) data.push([`Project description: ${headerData.description}`]);
|
||||||
['Project Description', headerData?.description || ''],
|
|
||||||
['Public URL', headerData?.publicUrl || ''],
|
|
||||||
['Backstage URL', headerData?.backstageUrl || ''],
|
|
||||||
[],
|
|
||||||
];
|
|
||||||
|
|
||||||
const fieldOrder: OntimeEntryCommonKeys[] = [
|
const fieldOrder: OntimeEntryCommonKeys[] = [
|
||||||
'timeStart',
|
'timeStart',
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ import ErrorBoundary from '../../common/components/error-boundary/ErrorBoundary'
|
|||||||
import AppSettings from '../app-settings/AppSettings';
|
import AppSettings from '../app-settings/AppSettings';
|
||||||
import { SettingsOptionId, useSettingsStore } from '../app-settings/settingsStore';
|
import { SettingsOptionId, useSettingsStore } from '../app-settings/settingsStore';
|
||||||
import MenuBar from '../menu/MenuBar';
|
import MenuBar from '../menu/MenuBar';
|
||||||
import AboutModal from '../modals/about-modal/AboutModal';
|
|
||||||
import QuickStart from '../modals/quick-start/QuickStart';
|
import QuickStart from '../modals/quick-start/QuickStart';
|
||||||
import SheetsModal from '../modals/sheets-modal/SheetsModal';
|
import SheetsModal from '../modals/sheets-modal/SheetsModal';
|
||||||
import UploadModal from '../modals/upload-modal/UploadModal';
|
import UploadModal from '../modals/upload-modal/UploadModal';
|
||||||
@@ -16,7 +15,6 @@ import styles from './Editor.module.scss';
|
|||||||
const Rundown = lazy(() => import('../rundown/RundownExport'));
|
const Rundown = lazy(() => import('../rundown/RundownExport'));
|
||||||
const TimerControl = lazy(() => import('../control/playback/TimerControlExport'));
|
const TimerControl = lazy(() => import('../control/playback/TimerControlExport'));
|
||||||
const MessageControl = lazy(() => import('../control/message/MessageControlExport'));
|
const MessageControl = lazy(() => import('../control/message/MessageControlExport'));
|
||||||
|
|
||||||
const IntegrationModal = lazy(() => import('../modals/integration-modal/IntegrationModal'));
|
const IntegrationModal = lazy(() => import('../modals/integration-modal/IntegrationModal'));
|
||||||
const SettingsModal = lazy(() => import('../modals/settings-modal/SettingsModal'));
|
const SettingsModal = lazy(() => import('../modals/settings-modal/SettingsModal'));
|
||||||
|
|
||||||
@@ -35,7 +33,6 @@ export default function Editor() {
|
|||||||
onOpen: onIntegrationModalOpen,
|
onOpen: onIntegrationModalOpen,
|
||||||
onClose: onIntegrationModalClose,
|
onClose: onIntegrationModalClose,
|
||||||
} = useDisclosure();
|
} = useDisclosure();
|
||||||
const { isOpen: isAboutModalOpen, onOpen: onAboutModalOpen, onClose: onAboutModalClose } = useDisclosure();
|
|
||||||
const { isOpen: isQuickStartOpen, onOpen: onQuickStartOpen, onClose: onQuickStartClose } = useDisclosure();
|
const { isOpen: isQuickStartOpen, onOpen: onQuickStartOpen, onClose: onQuickStartClose } = useDisclosure();
|
||||||
const { isOpen: isSheetsOpen, onOpen: onSheetsOpen, onClose: onSheetsClose } = useDisclosure();
|
const { isOpen: isSheetsOpen, onOpen: onSheetsOpen, onClose: onSheetsClose } = useDisclosure();
|
||||||
|
|
||||||
@@ -52,7 +49,6 @@ export default function Editor() {
|
|||||||
<QuickStart onClose={onQuickStartClose} isOpen={isQuickStartOpen} />
|
<QuickStart onClose={onQuickStartClose} isOpen={isQuickStartOpen} />
|
||||||
<UploadModal onClose={onUploadModalClose} isOpen={isUploadModalOpen} />
|
<UploadModal onClose={onUploadModalClose} isOpen={isUploadModalOpen} />
|
||||||
<IntegrationModal onClose={onIntegrationModalClose} isOpen={isIntegrationModalOpen} />
|
<IntegrationModal onClose={onIntegrationModalClose} isOpen={isIntegrationModalOpen} />
|
||||||
<AboutModal onClose={onAboutModalClose} isOpen={isAboutModalOpen} />
|
|
||||||
<SettingsModal isOpen={isOldSettingsOpen} onClose={onSettingsClose} />
|
<SettingsModal isOpen={isOldSettingsOpen} onClose={onSettingsClose} />
|
||||||
<SheetsModal onClose={onSheetsClose} isOpen={isSheetsOpen} />
|
<SheetsModal onClose={onSheetsClose} isOpen={isSheetsOpen} />
|
||||||
</ErrorBoundary>
|
</ErrorBoundary>
|
||||||
@@ -66,8 +62,6 @@ export default function Editor() {
|
|||||||
onUploadOpen={onUploadModalOpen}
|
onUploadOpen={onUploadModalOpen}
|
||||||
isIntegrationOpen={isIntegrationModalOpen}
|
isIntegrationOpen={isIntegrationModalOpen}
|
||||||
onIntegrationOpen={onIntegrationModalOpen}
|
onIntegrationOpen={onIntegrationModalOpen}
|
||||||
isAboutOpen={isAboutModalOpen}
|
|
||||||
onAboutOpen={onAboutModalOpen}
|
|
||||||
isQuickStartOpen={isQuickStartOpen}
|
isQuickStartOpen={isQuickStartOpen}
|
||||||
onQuickStartOpen={onQuickStartOpen}
|
onQuickStartOpen={onQuickStartOpen}
|
||||||
openSettings={handleSettings}
|
openSettings={handleSettings}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { memo, useCallback, useEffect, useState } from 'react';
|
import { memo, useCallback, useEffect } from 'react';
|
||||||
import { IconButton, MenuButton, Tooltip } from '@chakra-ui/react';
|
import { IconButton, MenuButton, Tooltip } from '@chakra-ui/react';
|
||||||
import { IoAdd } from '@react-icons/all-files/io5/IoAdd';
|
import { IoAdd } from '@react-icons/all-files/io5/IoAdd';
|
||||||
import { IoCloud } from '@react-icons/all-files/io5/IoCloud';
|
import { IoCloud } from '@react-icons/all-files/io5/IoCloud';
|
||||||
@@ -6,21 +6,17 @@ import { IoCloudOutline } from '@react-icons/all-files/io5/IoCloudOutline';
|
|||||||
import { IoColorWand } from '@react-icons/all-files/io5/IoColorWand';
|
import { IoColorWand } from '@react-icons/all-files/io5/IoColorWand';
|
||||||
import { IoExtensionPuzzle } from '@react-icons/all-files/io5/IoExtensionPuzzle';
|
import { IoExtensionPuzzle } from '@react-icons/all-files/io5/IoExtensionPuzzle';
|
||||||
import { IoExtensionPuzzleOutline } from '@react-icons/all-files/io5/IoExtensionPuzzleOutline';
|
import { IoExtensionPuzzleOutline } from '@react-icons/all-files/io5/IoExtensionPuzzleOutline';
|
||||||
import { IoHelp } from '@react-icons/all-files/io5/IoHelp';
|
|
||||||
import { IoOptions } from '@react-icons/all-files/io5/IoOptions';
|
import { IoOptions } from '@react-icons/all-files/io5/IoOptions';
|
||||||
import { IoPlay } from '@react-icons/all-files/io5/IoPlay';
|
import { IoPlay } from '@react-icons/all-files/io5/IoPlay';
|
||||||
import { IoPushOutline } from '@react-icons/all-files/io5/IoPushOutline';
|
import { IoPushOutline } from '@react-icons/all-files/io5/IoPushOutline';
|
||||||
import { IoSaveOutline } from '@react-icons/all-files/io5/IoSaveOutline';
|
|
||||||
import { IoSettingsOutline } from '@react-icons/all-files/io5/IoSettingsOutline';
|
import { IoSettingsOutline } from '@react-icons/all-files/io5/IoSettingsOutline';
|
||||||
import { IoSnowOutline } from '@react-icons/all-files/io5/IoSnowOutline';
|
import { IoSnowOutline } from '@react-icons/all-files/io5/IoSnowOutline';
|
||||||
|
|
||||||
import { downloadCSV, downloadRundown } from '../../common/api/ontimeApi';
|
|
||||||
import QuitIconBtn from '../../common/components/buttons/QuitIconBtn';
|
import QuitIconBtn from '../../common/components/buttons/QuitIconBtn';
|
||||||
import TooltipActionBtn from '../../common/components/buttons/TooltipActionBtn';
|
import TooltipActionBtn from '../../common/components/buttons/TooltipActionBtn';
|
||||||
import useElectronEvent from '../../common/hooks/useElectronEvent';
|
import useElectronEvent from '../../common/hooks/useElectronEvent';
|
||||||
import { AppMode, useAppMode } from '../../common/stores/appModeStore';
|
import { AppMode, useAppMode } from '../../common/stores/appModeStore';
|
||||||
import { cx } from '../../common/utils/styleUtils';
|
import { cx } from '../../common/utils/styleUtils';
|
||||||
import ExportModal, { ExportType } from '../modals/export-modal/ExportModal';
|
|
||||||
|
|
||||||
import RundownMenu from './RundownMenu';
|
import RundownMenu from './RundownMenu';
|
||||||
|
|
||||||
@@ -34,8 +30,6 @@ interface MenuBarProps {
|
|||||||
onUploadOpen: () => void;
|
onUploadOpen: () => void;
|
||||||
isIntegrationOpen: boolean;
|
isIntegrationOpen: boolean;
|
||||||
onIntegrationOpen: () => void;
|
onIntegrationOpen: () => void;
|
||||||
isAboutOpen: boolean;
|
|
||||||
onAboutOpen: () => void;
|
|
||||||
isQuickStartOpen: boolean;
|
isQuickStartOpen: boolean;
|
||||||
onQuickStartOpen: () => void;
|
onQuickStartOpen: () => void;
|
||||||
isSheetsOpen: boolean;
|
isSheetsOpen: boolean;
|
||||||
@@ -65,8 +59,6 @@ const MenuBar = (props: MenuBarProps) => {
|
|||||||
onUploadOpen,
|
onUploadOpen,
|
||||||
isIntegrationOpen,
|
isIntegrationOpen,
|
||||||
onIntegrationOpen,
|
onIntegrationOpen,
|
||||||
isAboutOpen,
|
|
||||||
onAboutOpen,
|
|
||||||
isQuickStartOpen,
|
isQuickStartOpen,
|
||||||
onQuickStartOpen,
|
onQuickStartOpen,
|
||||||
openSettings,
|
openSettings,
|
||||||
@@ -116,22 +108,6 @@ const MenuBar = (props: MenuBarProps) => {
|
|||||||
};
|
};
|
||||||
}, [handleKeyPress, isElectron]);
|
}, [handleKeyPress, isElectron]);
|
||||||
|
|
||||||
const [isModalOpen, setIsModalOpen] = useState(false);
|
|
||||||
|
|
||||||
const onModalClose = (exportType?: ExportType) => {
|
|
||||||
setIsModalOpen(false);
|
|
||||||
|
|
||||||
if (!exportType) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (exportType === 'json') {
|
|
||||||
downloadRundown();
|
|
||||||
} else if (exportType === 'csv') {
|
|
||||||
downloadCSV();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={style.menu}>
|
<div className={style.menu}>
|
||||||
<QuitIconBtn disabled={!isElectron} clickHandler={sendShutdown} />
|
<QuitIconBtn disabled={!isElectron} clickHandler={sendShutdown} />
|
||||||
@@ -154,15 +130,6 @@ const MenuBar = (props: MenuBarProps) => {
|
|||||||
tooltip='Import project file'
|
tooltip='Import project file'
|
||||||
aria-label='Import project file'
|
aria-label='Import project file'
|
||||||
/>
|
/>
|
||||||
<TooltipActionBtn
|
|
||||||
{...buttonStyle}
|
|
||||||
icon={<IoSaveOutline />}
|
|
||||||
isDisabled={appMode === AppMode.Run}
|
|
||||||
clickHandler={() => setIsModalOpen(true)}
|
|
||||||
tooltip='Export project file'
|
|
||||||
aria-label='Export project file'
|
|
||||||
/>
|
|
||||||
<ExportModal onClose={onModalClose} isOpen={isModalOpen} />
|
|
||||||
<div className={style.gap} />
|
<div className={style.gap} />
|
||||||
<RundownMenu>
|
<RundownMenu>
|
||||||
<Tooltip label='Rundown...'>
|
<Tooltip label='Rundown...'>
|
||||||
@@ -221,17 +188,7 @@ const MenuBar = (props: MenuBarProps) => {
|
|||||||
tooltip='Settings'
|
tooltip='Settings'
|
||||||
aria-label='Settings'
|
aria-label='Settings'
|
||||||
/>
|
/>
|
||||||
<div className={style.gap} />
|
|
||||||
<TooltipActionBtn
|
|
||||||
{...buttonStyle}
|
|
||||||
className={isAboutOpen ? style.open : ''}
|
|
||||||
icon={<IoHelp />}
|
|
||||||
clickHandler={onAboutOpen}
|
|
||||||
tooltip='About'
|
|
||||||
aria-label='About'
|
|
||||||
size='sm'
|
|
||||||
/>
|
|
||||||
<div className={style.gap} />
|
|
||||||
<TooltipActionBtn
|
<TooltipActionBtn
|
||||||
{...buttonStyle}
|
{...buttonStyle}
|
||||||
className={cx([isSettingsOpen ? style.open : null, style.bottom])}
|
className={cx([isSettingsOpen ? style.open : null, style.bottom])}
|
||||||
|
|||||||
@@ -1,67 +0,0 @@
|
|||||||
import { Modal, ModalBody, ModalCloseButton, ModalContent, ModalHeader, ModalOverlay } from '@chakra-ui/react';
|
|
||||||
|
|
||||||
import { version } from '../../../../package.json';
|
|
||||||
import OntimeLogo from '../../../assets/images/ontime-logo.svg?react';
|
|
||||||
import { gitbookUrl, githubUrl } from '../../../externals';
|
|
||||||
import ModalLink from '../ModalLink';
|
|
||||||
|
|
||||||
import UpdateChecker from './UpdateChecker';
|
|
||||||
|
|
||||||
import styles from '../Modal.module.scss';
|
|
||||||
|
|
||||||
interface AboutModalProps {
|
|
||||||
isOpen: boolean;
|
|
||||||
onClose: () => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function AboutModal(props: AboutModalProps) {
|
|
||||||
const { isOpen, onClose } = props;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Modal
|
|
||||||
size='sm'
|
|
||||||
isOpen={isOpen}
|
|
||||||
onClose={onClose}
|
|
||||||
closeOnOverlayClick={false}
|
|
||||||
motionPreset='slideInBottom'
|
|
||||||
scrollBehavior='inside'
|
|
||||||
preserveScrollBarGap
|
|
||||||
variant='ontime-small'
|
|
||||||
>
|
|
||||||
<ModalOverlay />
|
|
||||||
<ModalContent>
|
|
||||||
<ModalHeader>
|
|
||||||
About Ontime
|
|
||||||
<ModalCloseButton />
|
|
||||||
</ModalHeader>
|
|
||||||
|
|
||||||
<ModalBody className={styles.body}>
|
|
||||||
<div className={styles.twoColumn}>
|
|
||||||
<OntimeLogo className={styles.logo} />
|
|
||||||
<div>
|
|
||||||
<div className={styles.padBottom}>
|
|
||||||
<span className={styles.sectionTitle}>Ontime</span>
|
|
||||||
Free Open Source Software for managing rundowns and event timers
|
|
||||||
<ModalLink href='https://www.getontime.no'>www.getontime.no</ModalLink>
|
|
||||||
</div>
|
|
||||||
<div className={styles.padBottom}>
|
|
||||||
<span className={styles.sectionTitle}>Current version</span>
|
|
||||||
|
|
||||||
{`You are currently using Ontime ${version}`}
|
|
||||||
</div>
|
|
||||||
<div className={styles.padBottom}>
|
|
||||||
<span className={styles.sectionTitle}>Docs</span>
|
|
||||||
<ModalLink href={gitbookUrl}>Read the docs over at GitBook</ModalLink>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<span className={styles.sectionTitle}>Github</span>
|
|
||||||
<ModalLink href={githubUrl}>Follow the project on GitHub</ModalLink>
|
|
||||||
</div>
|
|
||||||
<UpdateChecker version={version} />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</ModalBody>
|
|
||||||
</ModalContent>
|
|
||||||
</Modal>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,72 +0,0 @@
|
|||||||
import { useState } from 'react';
|
|
||||||
import { Button } from '@chakra-ui/react';
|
|
||||||
|
|
||||||
import { getLatestVersion, HasUpdate } from '../../../common/api/ontimeApi';
|
|
||||||
import ModalLink from '../ModalLink';
|
|
||||||
|
|
||||||
import styles from '../Modal.module.scss';
|
|
||||||
|
|
||||||
interface UpdateCheckerProps {
|
|
||||||
version: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
type CheckFail = {
|
|
||||||
error: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
type CheckIsLatest = {
|
|
||||||
latest: true;
|
|
||||||
};
|
|
||||||
|
|
||||||
type CheckRemote = CheckFail | CheckIsLatest | HasUpdate;
|
|
||||||
|
|
||||||
export default function UpdateChecker(props: UpdateCheckerProps) {
|
|
||||||
const { version } = props;
|
|
||||||
const [updateMessage, setUpdateMessage] = useState<CheckRemote | null>(null);
|
|
||||||
const [isFetching, setIsFetching] = useState(false);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Handles version comparison and returns component with message
|
|
||||||
*/
|
|
||||||
const versionCheck = async () => {
|
|
||||||
setIsFetching(true);
|
|
||||||
|
|
||||||
try {
|
|
||||||
const latest = await getLatestVersion();
|
|
||||||
|
|
||||||
if (!latest.version.includes(version)) {
|
|
||||||
// new version, pass data to component
|
|
||||||
setUpdateMessage(latest);
|
|
||||||
} else {
|
|
||||||
setUpdateMessage({ latest: true });
|
|
||||||
}
|
|
||||||
} catch {
|
|
||||||
setUpdateMessage({ error: 'Error reaching server' });
|
|
||||||
} finally {
|
|
||||||
setIsFetching(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const disableButton = Boolean(updateMessage && 'version' in updateMessage);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className={styles.updateSection}>
|
|
||||||
<Button onClick={versionCheck} variant='ontime-filled' isLoading={isFetching} isDisabled={disableButton}>
|
|
||||||
Check for updates
|
|
||||||
</Button>
|
|
||||||
<ResolveUpdateMessage updateMessage={updateMessage} />
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function ResolveUpdateMessage(props: { updateMessage: CheckRemote | null }) {
|
|
||||||
const { updateMessage } = props;
|
|
||||||
|
|
||||||
if (updateMessage && 'error' in updateMessage) {
|
|
||||||
return <span className={styles.error}>{updateMessage.error}</span>;
|
|
||||||
}
|
|
||||||
if (updateMessage && 'url' in updateMessage) {
|
|
||||||
return <ModalLink href={updateMessage?.url}>{`New version available: ${updateMessage.version}`}</ModalLink>;
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
.buttonRow {
|
|
||||||
justify-content: space-between;
|
|
||||||
margin-top: $section-spacing;
|
|
||||||
display: flex;
|
|
||||||
gap: $section-spacing;
|
|
||||||
margin: 0 0.25rem;
|
|
||||||
}
|
|
||||||
@@ -1,32 +0,0 @@
|
|||||||
import { Button, Modal, ModalBody, ModalCloseButton, ModalContent, ModalHeader, ModalOverlay } from '@chakra-ui/react';
|
|
||||||
|
|
||||||
import styles from './ExportModal.module.scss';
|
|
||||||
|
|
||||||
export type ExportType = 'csv' | 'json';
|
|
||||||
|
|
||||||
interface ExportModalProps {
|
|
||||||
isOpen: boolean;
|
|
||||||
onClose: (type?: ExportType) => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function ExportModal(props: ExportModalProps) {
|
|
||||||
const { isOpen, onClose } = props;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Modal isOpen={isOpen} onClose={onClose} motionPreset='slideInBottom' size='xl' variant='ontime-small'>
|
|
||||||
<ModalOverlay />
|
|
||||||
<ModalContent>
|
|
||||||
<ModalHeader className={styles.modalHeader}>Download options</ModalHeader>
|
|
||||||
<ModalCloseButton />
|
|
||||||
<ModalBody className={styles.buttonRow}>
|
|
||||||
<Button onClick={() => onClose('csv')} variant='ontime-subtle-on-light' width='100%'>
|
|
||||||
Rundown as CSV
|
|
||||||
</Button>
|
|
||||||
<Button onClick={() => onClose('json')} variant='ontime-filled' width='100%'>
|
|
||||||
Project file
|
|
||||||
</Button>
|
|
||||||
</ModalBody>
|
|
||||||
</ModalContent>
|
|
||||||
</Modal>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -150,6 +150,8 @@ export default function UploadModal({ onClose, isOpen }: UploadModalProps) {
|
|||||||
setSubmitting(true);
|
setSubmitting(true);
|
||||||
try {
|
try {
|
||||||
await patchData({ rundown, userFields });
|
await patchData({ rundown, userFields });
|
||||||
|
// TODO: broken :(
|
||||||
|
// we need to normalise the data here
|
||||||
queryClient.setQueryData(RUNDOWN, { rundown, revision: -1 });
|
queryClient.setQueryData(RUNDOWN, { rundown, revision: -1 });
|
||||||
queryClient.setQueryData(USERFIELDS, userFields);
|
queryClient.setQueryData(USERFIELDS, userFields);
|
||||||
await queryClient.invalidateQueries({
|
await queryClient.invalidateQueries({
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { useCallback, useEffect, useRef, useState } from 'react';
|
import { useCallback, useEffect, useRef, useState } from 'react';
|
||||||
import { useSearchParams } from 'react-router-dom';
|
import { useSearchParams } from 'react-router-dom';
|
||||||
import { isOntimeEvent, OntimeEvent, SupportedEvent, UserFields } from 'ontime-types';
|
import { isOntimeEvent, OntimeEvent, SupportedEvent, UserFields } from 'ontime-types';
|
||||||
import { getFirstEvent, getLastEvent } from 'ontime-utils';
|
import { getFirstEventNormal, getLastEventNormal } from 'ontime-utils';
|
||||||
|
|
||||||
import NavigationMenu from '../../common/components/navigation-menu/NavigationMenu';
|
import NavigationMenu from '../../common/components/navigation-menu/NavigationMenu';
|
||||||
import Empty from '../../common/components/state/Empty';
|
import Empty from '../../common/components/state/Empty';
|
||||||
@@ -137,8 +137,8 @@ export default function Operator() {
|
|||||||
let isPast = Boolean(featureData.selectedEventId);
|
let isPast = Boolean(featureData.selectedEventId);
|
||||||
const hidePast = isStringBoolean(searchParams.get('hidepast'));
|
const hidePast = isStringBoolean(searchParams.get('hidepast'));
|
||||||
|
|
||||||
const { firstEvent } = getFirstEvent(data);
|
const { firstEvent } = getFirstEventNormal(data.rundown, data.order);
|
||||||
const { lastEvent } = getLastEvent(data);
|
const { lastEvent } = getLastEventNormal(data.rundown, data.order);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={style.operatorContainer}>
|
<div className={style.operatorContainer}>
|
||||||
@@ -163,7 +163,8 @@ export default function Operator() {
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
<div className={style.operatorEvents} onWheel={handleScroll} onTouchMove={handleScroll} ref={scrollRef}>
|
<div className={style.operatorEvents} onWheel={handleScroll} onTouchMove={handleScroll} ref={scrollRef}>
|
||||||
{data.map((entry) => {
|
{data.order.map((eventId) => {
|
||||||
|
const entry = data.rundown[eventId];
|
||||||
if (isOntimeEvent(entry)) {
|
if (isOntimeEvent(entry)) {
|
||||||
const isSelected = featureData.selectedEventId === entry.id;
|
const isSelected = featureData.selectedEventId === entry.id;
|
||||||
if (isSelected) {
|
if (isSelected) {
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import { Fragment, lazy, useCallback, useEffect, useRef, useState } from 'react';
|
import { Fragment, lazy, useCallback, useEffect, useRef, useState } from 'react';
|
||||||
import { closestCenter, DndContext, DragEndEvent, PointerSensor, useSensor, useSensors } from '@dnd-kit/core';
|
import { closestCenter, DndContext, DragEndEvent, PointerSensor, useSensor, useSensors } from '@dnd-kit/core';
|
||||||
import { arrayMove, SortableContext, verticalListSortingStrategy } from '@dnd-kit/sortable';
|
import { arrayMove, SortableContext, verticalListSortingStrategy } from '@dnd-kit/sortable';
|
||||||
import { isOntimeBlock, isOntimeDelay, isOntimeEvent, OntimeRundown, Playback, SupportedEvent } from 'ontime-types';
|
import { isOntimeBlock, isOntimeDelay, isOntimeEvent, Playback, RundownCached, SupportedEvent } from 'ontime-types';
|
||||||
import { getFirst, getNext, getPrevious } from 'ontime-utils';
|
import { getFirstNormal, getNextNormal, getPreviousNormal } from 'ontime-utils';
|
||||||
|
|
||||||
import { useEventAction } from '../../common/hooks/useEventAction';
|
import { useEventAction } from '../../common/hooks/useEventAction';
|
||||||
import useFollowComponent from '../../common/hooks/useFollowComponent';
|
import useFollowComponent from '../../common/hooks/useFollowComponent';
|
||||||
@@ -19,12 +19,12 @@ import style from './Rundown.module.scss';
|
|||||||
const RundownEntry = lazy(() => import('./RundownEntry'));
|
const RundownEntry = lazy(() => import('./RundownEntry'));
|
||||||
|
|
||||||
interface RundownProps {
|
interface RundownProps {
|
||||||
entries: OntimeRundown;
|
data: RundownCached;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function Rundown(props: RundownProps) {
|
export default function Rundown({ data }: RundownProps) {
|
||||||
const { entries } = props;
|
const { order, rundown } = data;
|
||||||
const [statefulEntries, setStatefulEntries] = useState(entries);
|
const [statefulEntries, setStatefulEntries] = useState(order);
|
||||||
|
|
||||||
const featureData = useRundownEditor();
|
const featureData = useRundownEditor();
|
||||||
const { addEvent, reorderEvent } = useEventAction();
|
const { addEvent, reorderEvent } = useEventAction();
|
||||||
@@ -56,7 +56,7 @@ export default function Rundown(props: RundownProps) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (type === 'clone') {
|
if (type === 'clone') {
|
||||||
const cursorEvent = entries.find((event) => event.id === cursor);
|
const cursorEvent = rundown[cursor];
|
||||||
if (cursorEvent?.type === SupportedEvent.Event) {
|
if (cursorEvent?.type === SupportedEvent.Event) {
|
||||||
const newEvent = cloneEvent(cursorEvent, cursorEvent.id);
|
const newEvent = cloneEvent(cursorEvent, cursorEvent.id);
|
||||||
addEvent(newEvent);
|
addEvent(newEvent);
|
||||||
@@ -76,7 +76,7 @@ export default function Rundown(props: RundownProps) {
|
|||||||
addEvent({ type }, { after: cursor });
|
addEvent({ type }, { after: cursor });
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[addEvent, defaultPublic, entries, startTimeIsLastEnd],
|
[addEvent, rundown, defaultPublic, startTimeIsLastEnd],
|
||||||
);
|
);
|
||||||
|
|
||||||
// Handle keyboard shortcuts
|
// Handle keyboard shortcuts
|
||||||
@@ -91,21 +91,23 @@ export default function Rundown(props: RundownProps) {
|
|||||||
if (modKeysAlt) {
|
if (modKeysAlt) {
|
||||||
switch (event.code) {
|
switch (event.code) {
|
||||||
case 'ArrowDown': {
|
case 'ArrowDown': {
|
||||||
if (entries.length < 1) {
|
if (order.length < 1) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const nextEvent = cursor == null ? getFirst(entries) : getNext(entries, cursor)?.nextEvent;
|
const nextEvent =
|
||||||
|
cursor == null ? getFirstNormal(rundown, order) : getNextNormal(rundown, order, cursor)?.nextEvent;
|
||||||
if (nextEvent) {
|
if (nextEvent) {
|
||||||
// moveCursorTo(nextEvent.id, nextEvent.type === SupportedEvent.Event);
|
// moveCursorTo(nextEvent.id, nextEvent.type === SupportedEvent.Event);
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case 'ArrowUp': {
|
case 'ArrowUp': {
|
||||||
if (entries.length < 1) {
|
if (order.length < 1) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion -- we check for this before
|
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion -- we check for this before
|
||||||
const previousEvent = cursor == null ? getFirst(entries) : getPrevious(entries, cursor).previousEvent;
|
const previousEvent =
|
||||||
|
cursor == null ? getFirstNormal(rundown, order) : getPreviousNormal(rundown, order, cursor).previousEvent;
|
||||||
if (previousEvent) {
|
if (previousEvent) {
|
||||||
// moveCursorTo(previousEvent.id, previousEvent.type === SupportedEvent.Event);
|
// moveCursorTo(previousEvent.id, previousEvent.type === SupportedEvent.Event);
|
||||||
}
|
}
|
||||||
@@ -133,32 +135,30 @@ export default function Rundown(props: RundownProps) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else if (modKeysCtrlAlt) {
|
} else if (modKeysCtrlAlt) {
|
||||||
if (entries.length < 2 || cursor == null) {
|
if (order.length < 2 || cursor == null) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (event.code == 'ArrowDown') {
|
if (event.code == 'ArrowDown') {
|
||||||
const { nextEvent, nextIndex } = getNext(entries, cursor);
|
const { nextEvent, nextIndex } = getNextNormal(rundown, order, cursor);
|
||||||
if (nextEvent && nextIndex !== null) {
|
if (nextEvent && nextIndex !== null) {
|
||||||
reorderEvent(cursor, nextIndex - 1, nextIndex);
|
reorderEvent(cursor, nextIndex - 1, nextIndex);
|
||||||
}
|
}
|
||||||
} else if (event.code == 'ArrowUp') {
|
} else if (event.code == 'ArrowUp') {
|
||||||
const { previousEvent, previousIndex } = getPrevious(entries, cursor);
|
const { previousEvent, previousIndex } = getPreviousNormal(rundown, order, cursor);
|
||||||
if (previousEvent && previousIndex !== null) {
|
if (previousEvent && previousIndex !== null) {
|
||||||
reorderEvent(cursor, previousIndex + 1, previousIndex);
|
reorderEvent(cursor, previousIndex + 1, previousIndex);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[cursor, entries, insertAtCursor, reorderEvent],
|
[cursor, insertAtCursor, order, rundown, reorderEvent],
|
||||||
);
|
);
|
||||||
|
|
||||||
// we copy the state from the store here
|
// we copy the state from the store here
|
||||||
// to workaround async updates on the drag mutations
|
// to workaround async updates on the drag mutations
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (entries) {
|
setStatefulEntries(order);
|
||||||
setStatefulEntries(entries);
|
}, [order]);
|
||||||
}
|
|
||||||
}, [entries]);
|
|
||||||
|
|
||||||
// listen to keys
|
// listen to keys
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -193,7 +193,7 @@ export default function Rundown(props: RundownProps) {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
if (statefulEntries?.length < 1) {
|
if (statefulEntries.length < 1) {
|
||||||
return <RundownEmpty handleAddNew={() => insertAtCursor(SupportedEvent.Event, null)} />;
|
return <RundownEmpty handleAddNew={() => insertAtCursor(SupportedEvent.Event, null)} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -208,39 +208,46 @@ export default function Rundown(props: RundownProps) {
|
|||||||
<DndContext onDragEnd={handleOnDragEnd} sensors={sensors} collisionDetection={closestCenter}>
|
<DndContext onDragEnd={handleOnDragEnd} sensors={sensors} collisionDetection={closestCenter}>
|
||||||
<SortableContext items={statefulEntries} strategy={verticalListSortingStrategy}>
|
<SortableContext items={statefulEntries} strategy={verticalListSortingStrategy}>
|
||||||
<div className={style.list}>
|
<div className={style.list}>
|
||||||
{statefulEntries.map((entry, index) => {
|
{statefulEntries.map((eventId, index) => {
|
||||||
|
// we iterate through a stateful copy of order to make the operations smoother
|
||||||
|
// this means that this can be out of sync with order until the useEffect runs
|
||||||
|
// instead of writing all the logic guards, we simply short circuit rendering here
|
||||||
|
const event = rundown[eventId];
|
||||||
|
if (!event) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
if (index === 0) {
|
if (index === 0) {
|
||||||
eventIndex = 0;
|
eventIndex = 0;
|
||||||
}
|
}
|
||||||
let isFirstEvent = false;
|
let isFirstEvent = false;
|
||||||
if (isOntimeEvent(entry)) {
|
if (isOntimeEvent(event)) {
|
||||||
isFirstEvent = eventIndex === 0;
|
isFirstEvent = eventIndex === 0;
|
||||||
// event indexes are 1 based in frontend
|
// event indexes are 1 based in frontend
|
||||||
eventIndex++;
|
eventIndex++;
|
||||||
if (!isFirstEvent) {
|
if (!isFirstEvent) {
|
||||||
previousEnd = thisEnd;
|
previousEnd = thisEnd;
|
||||||
}
|
}
|
||||||
thisEnd = entry.timeEnd;
|
thisEnd = event.timeEnd;
|
||||||
previousEventId = entry.id;
|
previousEventId = event.id;
|
||||||
}
|
}
|
||||||
const isLast = index === entries.length - 1;
|
const isLast = index === order.length - 1;
|
||||||
const isSelected = featureData?.selectedEventId === entry.id;
|
const isSelected = featureData?.selectedEventId === event.id;
|
||||||
const isNext = featureData?.nextEventId === entry.id;
|
const isNext = featureData?.nextEventId === event.id;
|
||||||
const hasCursor = entry.id === cursor;
|
const hasCursor = event.id === cursor;
|
||||||
if (isSelected) {
|
if (isSelected) {
|
||||||
isPast = false;
|
isPast = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Fragment key={entry.id}>
|
<Fragment key={event.id}>
|
||||||
<div className={style.entryWrapper} data-testid={`entry-${eventIndex}`}>
|
<div className={style.entryWrapper} data-testid={`entry-${eventIndex}`}>
|
||||||
{entry.type === SupportedEvent.Event && <div className={style.entryIndex}>{eventIndex}</div>}
|
{isOntimeEvent(event) && <div className={style.entryIndex}>{eventIndex}</div>}
|
||||||
<div className={style.entry} key={entry.id} ref={hasCursor ? cursorRef : undefined}>
|
<div className={style.entry} key={event.id} ref={hasCursor ? cursorRef : undefined}>
|
||||||
<RundownEntry
|
<RundownEntry
|
||||||
type={entry.type}
|
type={event.type}
|
||||||
isPast={isPast}
|
isPast={isPast}
|
||||||
eventIndex={eventIndex}
|
eventIndex={eventIndex}
|
||||||
data={entry}
|
data={event}
|
||||||
selected={isSelected}
|
selected={isSelected}
|
||||||
hasCursor={hasCursor}
|
hasCursor={hasCursor}
|
||||||
next={isNext}
|
next={isNext}
|
||||||
@@ -254,10 +261,10 @@ export default function Rundown(props: RundownProps) {
|
|||||||
{((showQuickEntry && hasCursor) || isLast) && (
|
{((showQuickEntry && hasCursor) || isLast) && (
|
||||||
<QuickAddBlock
|
<QuickAddBlock
|
||||||
showKbd={hasCursor}
|
showKbd={hasCursor}
|
||||||
eventId={entry.id}
|
eventId={event.id}
|
||||||
previousEventId={previousEventId}
|
previousEventId={previousEventId}
|
||||||
disableAddDelay={isOntimeDelay(entry)}
|
disableAddDelay={isOntimeDelay(event)}
|
||||||
disableAddBlock={isOntimeBlock(entry)}
|
disableAddBlock={isOntimeBlock(event)}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</Fragment>
|
</Fragment>
|
||||||
|
|||||||
@@ -1,18 +1,8 @@
|
|||||||
import { useCallback } from 'react';
|
import { useCallback } from 'react';
|
||||||
import {
|
import { MaybeNumber, OntimeEvent, OntimeRundownEntry, Playback, SupportedEvent } from 'ontime-types';
|
||||||
GetRundownCached,
|
|
||||||
isOntimeEvent,
|
|
||||||
MaybeNumber,
|
|
||||||
OntimeEvent,
|
|
||||||
OntimeRundownEntry,
|
|
||||||
Playback,
|
|
||||||
SupportedEvent,
|
|
||||||
} from 'ontime-types';
|
|
||||||
|
|
||||||
import { RUNDOWN } from '../../common/api/apiConstants';
|
|
||||||
import { useEventAction } from '../../common/hooks/useEventAction';
|
import { useEventAction } from '../../common/hooks/useEventAction';
|
||||||
import useMemoisedFn from '../../common/hooks/useMemoisedFn';
|
import useMemoisedFn from '../../common/hooks/useMemoisedFn';
|
||||||
import { ontimeQueryClient } from '../../common/queryClient';
|
|
||||||
import { useAppMode } from '../../common/stores/appModeStore';
|
import { useAppMode } from '../../common/stores/appModeStore';
|
||||||
import { useEditorSettings } from '../../common/stores/editorSettings';
|
import { useEditorSettings } from '../../common/stores/editorSettings';
|
||||||
import { useEmitLog } from '../../common/stores/logger';
|
import { useEmitLog } from '../../common/stores/logger';
|
||||||
@@ -103,28 +93,20 @@ export default function RundownEntry(props: RundownEntryProps) {
|
|||||||
case 'update': {
|
case 'update': {
|
||||||
// Handles and filters update requests
|
// Handles and filters update requests
|
||||||
const { field, value } = payload as FieldValue;
|
const { field, value } = payload as FieldValue;
|
||||||
|
if (field === undefined || value === undefined) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
const newData: Partial<OntimeEvent> = { id: data.id };
|
const newData: Partial<OntimeEvent> = { id: data.id };
|
||||||
|
|
||||||
// if selected events are more than one
|
// if selected events are more than one
|
||||||
// we need to bulk edit
|
// we need to bulk edit
|
||||||
if (selectedEvents.size > 1) {
|
if (selectedEvents.size > 1) {
|
||||||
const changes: Partial<OntimeEvent> = { [field]: value };
|
const changes: Partial<OntimeEvent> = { [field]: value };
|
||||||
const rundown = ontimeQueryClient.getQueryData<GetRundownCached>(RUNDOWN)?.rundown ?? [];
|
batchUpdateEvents(changes, Array.from(selectedEvents));
|
||||||
const idsOfRundownEvents = rundown.filter(isOntimeEvent).map((event) => event.id);
|
|
||||||
|
|
||||||
const eventIds = [...selectedEvents.keys()];
|
|
||||||
// check every selected event id to see if they match rundown event ids
|
|
||||||
const areIdsValid = eventIds.every((eventId) => idsOfRundownEvents.includes(eventId));
|
|
||||||
|
|
||||||
if (!areIdsValid) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
batchUpdateEvents(changes, eventIds);
|
|
||||||
return clearSelectedEvents();
|
return clearSelectedEvents();
|
||||||
}
|
}
|
||||||
if (field in data) {
|
if (field in data) {
|
||||||
// @ts-expect-error not sure how to type this
|
// @ts-expect-error -- not sure how to type this
|
||||||
newData[field] = value;
|
newData[field] = value;
|
||||||
return updateEvent(newData);
|
return updateEvent(newData);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ export default function RundownWrapper() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={styles.rundownWrapper}>
|
<div className={styles.rundownWrapper}>
|
||||||
{status === 'success' && data ? <Rundown entries={data} /> : <Empty text='Connecting to server' />}
|
{status === 'success' && data ? <Rundown data={data} /> : <Empty text='Connecting to server' />}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,3 +22,10 @@
|
|||||||
@include drag-style;
|
@include drag-style;
|
||||||
grid-area: drag;
|
grid-area: drag;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.actionButtons {
|
||||||
|
grid-area: btns;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.5rem;
|
||||||
|
}
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
import { useEffect, useRef } from 'react';
|
import { useEffect, useRef } from 'react';
|
||||||
import { Button, HStack } from '@chakra-ui/react';
|
import { Button } from '@chakra-ui/react';
|
||||||
import { useSortable } from '@dnd-kit/sortable';
|
import { useSortable } from '@dnd-kit/sortable';
|
||||||
import { CSS } from '@dnd-kit/utilities';
|
import { CSS } from '@dnd-kit/utilities';
|
||||||
import { IoCheckmark } from '@react-icons/all-files/io5/IoCheckmark';
|
import { IoCheckmark } from '@react-icons/all-files/io5/IoCheckmark';
|
||||||
@@ -72,7 +72,7 @@ export default function DelayBlock(props: DelayBlockProps) {
|
|||||||
<IoReorderTwo />
|
<IoReorderTwo />
|
||||||
</span>
|
</span>
|
||||||
<DelayInput eventId={data.id} duration={data.duration} />
|
<DelayInput eventId={data.id} duration={data.duration} />
|
||||||
<HStack spacing='8px' className={style.actionOverlay}>
|
<div className={style.actionButtons}>
|
||||||
<Button onClick={applyDelayHandler} size='sm' leftIcon={<IoCheckmark />} variant='ontime-subtle-white'>
|
<Button onClick={applyDelayHandler} size='sm' leftIcon={<IoCheckmark />} variant='ontime-subtle-white'>
|
||||||
Apply
|
Apply
|
||||||
</Button>
|
</Button>
|
||||||
@@ -80,7 +80,7 @@ export default function DelayBlock(props: DelayBlockProps) {
|
|||||||
Cancel
|
Cancel
|
||||||
</Button>
|
</Button>
|
||||||
<BlockActionMenu enableDelete actionHandler={actionHandler} />
|
<BlockActionMenu enableDelete actionHandler={actionHandler} />
|
||||||
</HStack>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,31 +10,17 @@ import { IoSwapVertical } from '@react-icons/all-files/io5/IoSwapVertical';
|
|||||||
import { EndAction, MaybeNumber, OntimeEvent, Playback, TimerType } from 'ontime-types';
|
import { EndAction, MaybeNumber, OntimeEvent, Playback, TimerType } from 'ontime-types';
|
||||||
|
|
||||||
import { useContextMenu } from '../../../common/hooks/useContextMenu';
|
import { useContextMenu } from '../../../common/hooks/useContextMenu';
|
||||||
import useRundown from '../../../common/hooks-query/useRundown';
|
|
||||||
import copyToClipboard from '../../../common/utils/copyToClipboard';
|
import copyToClipboard from '../../../common/utils/copyToClipboard';
|
||||||
import { isMacOS } from '../../../common/utils/deviceUtils';
|
|
||||||
import { cx, getAccessibleColour } from '../../../common/utils/styleUtils';
|
import { cx, getAccessibleColour } from '../../../common/utils/styleUtils';
|
||||||
import type { EventItemActions } from '../RundownEntry';
|
import type { EventItemActions } from '../RundownEntry';
|
||||||
import { useEventIdSwapping } from '../useEventIdSwapping';
|
import { useEventIdSwapping } from '../useEventIdSwapping';
|
||||||
import { EditMode, useEventSelection } from '../useEventSelection';
|
import { getSelectionMode, useEventSelection } from '../useEventSelection';
|
||||||
|
|
||||||
import EventBlockInner from './EventBlockInner';
|
import EventBlockInner from './EventBlockInner';
|
||||||
import RundownIndicators from './RundownIndicators';
|
import RundownIndicators from './RundownIndicators';
|
||||||
|
|
||||||
import style from './EventBlock.module.scss';
|
import style from './EventBlock.module.scss';
|
||||||
|
|
||||||
const getEditMode = (event: MouseEvent): EditMode => {
|
|
||||||
if ((isMacOS() && event.metaKey) || event.ctrlKey) {
|
|
||||||
return 'ctrl';
|
|
||||||
}
|
|
||||||
|
|
||||||
if (event.shiftKey) {
|
|
||||||
return 'shift';
|
|
||||||
}
|
|
||||||
|
|
||||||
return 'click';
|
|
||||||
};
|
|
||||||
|
|
||||||
interface EventBlockProps {
|
interface EventBlockProps {
|
||||||
cue: string;
|
cue: string;
|
||||||
timeStart: number;
|
timeStart: number;
|
||||||
@@ -95,7 +81,6 @@ export default function EventBlock(props: EventBlockProps) {
|
|||||||
} = props;
|
} = props;
|
||||||
const { selectedEventId, setSelectedEventId, clearSelectedEventId } = useEventIdSwapping();
|
const { selectedEventId, setSelectedEventId, clearSelectedEventId } = useEventIdSwapping();
|
||||||
const { selectedEvents, setSelectedEvents } = useEventSelection();
|
const { selectedEvents, setSelectedEvents } = useEventSelection();
|
||||||
const { data: rundown = [] } = useRundown();
|
|
||||||
const handleRef = useRef<null | HTMLSpanElement>(null);
|
const handleRef = useRef<null | HTMLSpanElement>(null);
|
||||||
const [isVisible, setIsVisible] = useState(false);
|
const [isVisible, setIsVisible] = useState(false);
|
||||||
|
|
||||||
@@ -235,8 +220,10 @@ export default function EventBlock(props: EventBlockProps) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const editMode = getEditMode(event);
|
// UI indexes are 1 based
|
||||||
return setSelectedEvents({ id: eventId, index: eventIndex, rundown, editMode });
|
const index = eventIndex - 1;
|
||||||
|
const editMode = getSelectionMode(event);
|
||||||
|
return setSelectedEvents({ id: eventId, index, selectMode: editMode });
|
||||||
|
|
||||||
// moveCursorTo(eventId, true);
|
// moveCursorTo(eventId, true);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -36,24 +36,30 @@ export type EditorUpdateFields =
|
|||||||
export default function EventEditor() {
|
export default function EventEditor() {
|
||||||
const selectedEvents = useEventSelection((state) => state.selectedEvents);
|
const selectedEvents = useEventSelection((state) => state.selectedEvents);
|
||||||
const { data } = useRundown();
|
const { data } = useRundown();
|
||||||
|
const { order, rundown } = data;
|
||||||
const { updateEvent } = useEventAction();
|
const { updateEvent } = useEventAction();
|
||||||
|
|
||||||
const [event, setEvent] = useState<OntimeEvent | null>(null);
|
const [event, setEvent] = useState<OntimeEvent | null>(null);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!data) {
|
if (order.length === 0) {
|
||||||
setEvent(null);
|
setEvent(null);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const event = data.find((event) => selectedEvents.has(event.id));
|
const selectedEventId = order.find((eventId) => selectedEvents.has(eventId));
|
||||||
|
if (!selectedEventId) {
|
||||||
|
setEvent(null);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const event = rundown[selectedEventId];
|
||||||
|
|
||||||
if (event && isOntimeEvent(event)) {
|
if (event && isOntimeEvent(event)) {
|
||||||
setEvent(event);
|
setEvent(event);
|
||||||
} else {
|
} else {
|
||||||
setEvent(null);
|
setEvent(null);
|
||||||
}
|
}
|
||||||
}, [data, selectedEvents]);
|
}, [order, rundown, selectedEvents]);
|
||||||
|
|
||||||
const handleSubmit = useCallback(
|
const handleSubmit = useCallback(
|
||||||
(field: EditorUpdateFields, value: string) => {
|
(field: EditorUpdateFields, value: string) => {
|
||||||
|
|||||||
@@ -1,12 +1,17 @@
|
|||||||
import { isOntimeEvent, OntimeRundown } from 'ontime-types';
|
import { MouseEvent } from 'react';
|
||||||
|
import { isOntimeEvent, OntimeEvent, RundownCached } from 'ontime-types';
|
||||||
import { create } from 'zustand';
|
import { create } from 'zustand';
|
||||||
|
|
||||||
export type EditMode = 'shift' | 'click' | 'ctrl';
|
import { RUNDOWN } from '../../common/api/apiConstants';
|
||||||
|
import { ontimeQueryClient } from '../../common/queryClient';
|
||||||
|
import { isMacOS } from '../../common/utils/deviceUtils';
|
||||||
|
|
||||||
|
export type SelectionMode = 'shift' | 'click' | 'ctrl';
|
||||||
|
|
||||||
interface EventSelectionStore {
|
interface EventSelectionStore {
|
||||||
selectedEvents: Set<string>;
|
selectedEvents: Set<string>;
|
||||||
anchoredIndex: number | null;
|
anchoredIndex: number | null;
|
||||||
setSelectedEvents: (selectionArgs: { id: string; index: number; rundown: OntimeRundown; editMode: EditMode }) => void;
|
setSelectedEvents: (selectionArgs: { id: string; index: number; selectMode: SelectionMode }) => void;
|
||||||
clearSelectedEvents: () => void;
|
clearSelectedEvents: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -14,72 +19,79 @@ export const useEventSelection = create<EventSelectionStore>()((set, get) => ({
|
|||||||
selectedEvents: new Set(),
|
selectedEvents: new Set(),
|
||||||
anchoredIndex: null,
|
anchoredIndex: null,
|
||||||
setSelectedEvents: (selectionArgs) => {
|
setSelectedEvents: (selectionArgs) => {
|
||||||
const { id, index: eventIndex, rundown, editMode } = selectionArgs;
|
const { id, index, selectMode } = selectionArgs;
|
||||||
// event indexes are not 0 based
|
|
||||||
const index = eventIndex - 1;
|
|
||||||
|
|
||||||
const { selectedEvents, anchoredIndex } = get();
|
const { selectedEvents, anchoredIndex } = get();
|
||||||
|
|
||||||
if (editMode === 'click') {
|
// on click, we replace selection with event
|
||||||
|
if (selectMode === 'click') {
|
||||||
return set({ selectedEvents: new Set([id]), anchoredIndex: index });
|
return set({ selectedEvents: new Set([id]), anchoredIndex: index });
|
||||||
}
|
}
|
||||||
|
|
||||||
if (editMode === 'ctrl') {
|
// on ctrl + click, we toggle the selection of that event
|
||||||
if (selectedEvents.has(id)) {
|
if (selectMode === 'ctrl') {
|
||||||
const eventIds = rundown.reduce(
|
const rundownData = ontimeQueryClient.getQueryData<RundownCached>(RUNDOWN);
|
||||||
(newRundown, event, i) => {
|
if (!rundownData) return;
|
||||||
if (isOntimeEvent(event) && selectedEvents.has(id)) {
|
|
||||||
return newRundown.concat({ id: event.id, index: i });
|
|
||||||
}
|
|
||||||
|
|
||||||
return newRundown;
|
|
||||||
},
|
|
||||||
[] as { id: string; index: number }[],
|
|
||||||
);
|
|
||||||
|
|
||||||
// find the next available higher index
|
|
||||||
// if unavailable, then grab the last index of events
|
|
||||||
const newAnchoredIndex = eventIds.find(({ index: eventIndex }) => eventIndex > index) ?? eventIds.at(-1);
|
|
||||||
|
|
||||||
selectedEvents.delete(id);
|
|
||||||
|
|
||||||
|
// if it doesnt exist, simply add to the list and set an anchor
|
||||||
|
if (!selectedEvents.has(id)) {
|
||||||
return set({
|
return set({
|
||||||
selectedEvents,
|
selectedEvents: selectedEvents.add(id),
|
||||||
anchoredIndex: newAnchoredIndex?.index ?? 0,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return set({
|
|
||||||
selectedEvents: selectedEvents.add(id),
|
|
||||||
anchoredIndex: index,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (editMode === 'shift') {
|
|
||||||
const eventIds = rundown.filter(isOntimeEvent);
|
|
||||||
|
|
||||||
if (anchoredIndex === null) {
|
|
||||||
const eventsUntilIndex = eventIds.slice(0, eventIndex).map((event) => event.id);
|
|
||||||
|
|
||||||
return set({ selectedEvents: new Set(eventsUntilIndex), anchoredIndex: index });
|
|
||||||
}
|
|
||||||
|
|
||||||
if (anchoredIndex > index) {
|
|
||||||
const eventsFromIndex = eventIds.slice(index, anchoredIndex + 1).map((event) => event.id);
|
|
||||||
|
|
||||||
return set({
|
|
||||||
selectedEvents: new Set([...selectedEvents, ...eventsFromIndex]),
|
|
||||||
anchoredIndex: index,
|
anchoredIndex: index,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const eventsUntilIndex = eventIds.slice(anchoredIndex, eventIndex).map((event) => event.id);
|
// if event is already selected, we remove it from selection
|
||||||
|
// and set the anchor to the event after
|
||||||
|
selectedEvents.delete(id);
|
||||||
|
|
||||||
|
const nextIndex = rundownData.order.findIndex(
|
||||||
|
(eventId, i) => i > index && isOntimeEvent(rundownData.rundown[eventId]) && selectedEvents.has(eventId),
|
||||||
|
);
|
||||||
|
|
||||||
|
// if we didnt find anything after, set the anchor to the last event
|
||||||
|
return set({
|
||||||
|
selectedEvents,
|
||||||
|
anchoredIndex: nextIndex < 0 ? rundownData.order.length - 1 : nextIndex,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// on shift + click, we select a range of events up to the clicked event
|
||||||
|
if (selectMode === 'shift') {
|
||||||
|
const rundownData = ontimeQueryClient.getQueryData<RundownCached>(RUNDOWN);
|
||||||
|
if (!rundownData) return;
|
||||||
|
|
||||||
|
// get list of rundown with only ontime events
|
||||||
|
const events: OntimeEvent[] = [];
|
||||||
|
rundownData.order.forEach((eventId) => {
|
||||||
|
const event = rundownData.rundown[eventId];
|
||||||
|
if (isOntimeEvent(event)) {
|
||||||
|
events.push(event);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const start = anchoredIndex === null ? 0 : Math.min(anchoredIndex, index);
|
||||||
|
const end = anchoredIndex === null ? index : Math.max(anchoredIndex, index + 1);
|
||||||
|
|
||||||
|
// create new set with range of ids from start to end
|
||||||
|
const selectedEventIds = events.slice(start, end).map((event) => event.id);
|
||||||
|
|
||||||
return set({
|
return set({
|
||||||
selectedEvents: new Set([...selectedEvents, ...eventsUntilIndex]),
|
selectedEvents: new Set([...selectedEvents, ...selectedEventIds]),
|
||||||
anchoredIndex: index,
|
anchoredIndex: index,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
clearSelectedEvents: () => set({ selectedEvents: new Set() }),
|
clearSelectedEvents: () => set({ selectedEvents: new Set() }),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
export function getSelectionMode(event: MouseEvent): SelectionMode {
|
||||||
|
if ((isMacOS() && event.metaKey) || event.ctrlKey) {
|
||||||
|
return 'ctrl';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (event.shiftKey) {
|
||||||
|
return 'shift';
|
||||||
|
}
|
||||||
|
|
||||||
|
return 'click';
|
||||||
|
}
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import { Message, OntimeEvent, ProjectData, Settings, SupportedEvent, TimerMessa
|
|||||||
import { useStore } from 'zustand';
|
import { useStore } from 'zustand';
|
||||||
|
|
||||||
import useProjectData from '../../common/hooks-query/useProjectData';
|
import useProjectData from '../../common/hooks-query/useProjectData';
|
||||||
import useRundown from '../../common/hooks-query/useRundown';
|
import { useFlatRundown } 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 { runtimeStore } from '../../common/stores/runtime';
|
import { runtimeStore } from '../../common/stores/runtime';
|
||||||
@@ -42,7 +42,7 @@ const withData = <P extends WithDataProps>(Component: ComponentType<P>) => {
|
|||||||
const isMirrored = useViewOptionsStore((state) => state.mirror);
|
const isMirrored = useViewOptionsStore((state) => state.mirror);
|
||||||
|
|
||||||
// HTTP API data
|
// HTTP API data
|
||||||
const { data: rundownData } = useRundown();
|
const { data: rundownData } = useFlatRundown();
|
||||||
const { data: project } = useProjectData();
|
const { data: project } = useProjectData();
|
||||||
const { data: viewSettings } = useViewSettings();
|
const { data: viewSettings } = useViewSettings();
|
||||||
const { data: settings } = useSettings();
|
const { data: settings } = useSettings();
|
||||||
|
|||||||
@@ -47,18 +47,19 @@
|
|||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"addversion": "node -p \"'export const ONTIME_VERSION = ' + JSON.stringify(require('../../package.json').version) + ';'\" > src/ONTIME_VERSION.js",
|
"addversion": "node -p \"'export const ONTIME_VERSION = ' + JSON.stringify(require('../../package.json').version) + ';'\" > src/ONTIME_VERSION.js",
|
||||||
"setdb": "shx cp ../../demo-db/db.json src/preloaded-db/db.json",
|
"set:demoproject": "shx cp ../../demo-db/db.json src/preloaded-db/db.json",
|
||||||
"postinstall": "pnpm addversion && pnpm setdb",
|
"set:testproject": "shx cp ../../demo-db/db.json test-db/db.json",
|
||||||
|
"postinstall": "pnpm addversion && pnpm set:demoproject && pnpm set:testproject",
|
||||||
"dev": "cross-env NODE_ENV=development nodemon --exec \"ts-node-esm\" ./src/index.ts",
|
"dev": "cross-env NODE_ENV=development nodemon --exec \"ts-node-esm\" ./src/index.ts",
|
||||||
"dev:inspect": "cross-env NODE_ENV=development nodemon --exec \"node --inspect --loader ts-node/esm\" ./src/index.ts",
|
"dev:inspect": "cross-env NODE_ENV=development nodemon --exec \"node --inspect --loader ts-node/esm\" ./src/index.ts",
|
||||||
"dev:test": "cross-env IS_TEST=true nodemon --exec \"ts-node-esm\" ./src/index.ts",
|
"dev:test": "cross-env IS_TEST=true nodemon --exec \"ts-node-esm\" ./src/index.ts",
|
||||||
"prebuild": "pnpm setdb",
|
"prebuild": "pnpm set:demoproject",
|
||||||
"build": "pnpm prebuild && esbuild src/app.ts --log-level=error --platform=node --format=cjs --bundle --minify --outfile=dist/index.cjs",
|
"build": "pnpm prebuild && esbuild src/app.ts --log-level=error --platform=node --format=cjs --bundle --minify --legal-comments=external --outfile=dist/index.cjs",
|
||||||
"build:electron": "pnpm prebuild && esbuild src/app.ts --log-level=error --platform=node --format=cjs --bundle --minify --outfile=dist/index.cjs",
|
"build:electron": "pnpm prebuild && esbuild src/app.ts --log-level=error --platform=node --format=cjs --bundle --minify --legal-comments=external --outfile=dist/index.cjs",
|
||||||
"build:local": "pnpm prebuild && esbuild src/index.ts --log-level=error --platform=node --format=cjs --bundle --minify --outfile=dist/index.cjs",
|
"build:local": "pnpm prebuild && esbuild src/index.ts --log-level=error --platform=node --format=cjs --bundle --minify --legal-comments=external --outfile=dist/index.cjs",
|
||||||
"build:docker": "pnpm prebuild && esbuild src/index.ts --log-level=error --platform=node --format=cjs --minify --bundle --outfile=dist/docker.cjs",
|
"build:docker": "pnpm prebuild && esbuild src/index.ts --log-level=error --platform=node --format=cjs --minify --bundle --legal-comments=external --outfile=dist/docker.cjs",
|
||||||
"build:localdocker": "NODE_ENV=local pnpm prebuild && esbuild src/index.ts --log-level=error --platform=node --format=cjs --minify --bundle --outfile=dist/docker.cjs",
|
"build:localdocker": "NODE_ENV=local pnpm prebuild && esbuild src/index.ts --log-level=error --platform=node --format=cjs --minify --bundle --legal-comments=external --outfile=dist/docker.cjs",
|
||||||
"build:debug": "pnpm prebuild && esbuild src/app.ts --platform=node --format=cjs --bundle --outfile=dist/index.cjs",
|
"build:debug": "pnpm prebuild && esbuild src/app.ts --platform=node --format=cjs --bundle --legal-comments=external --outfile=dist/index.cjs",
|
||||||
"lint": "eslint . --quiet",
|
"lint": "eslint . --quiet",
|
||||||
"lint-staged": "eslint",
|
"lint-staged": "eslint",
|
||||||
"test": "cross-env IS_TEST=true vitest",
|
"test": "cross-env IS_TEST=true vitest",
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
import { LogOrigin, OntimeEvent, isKeyOfType, isOntimeEvent } from 'ontime-types';
|
import { OntimeEvent, isKeyOfType, isOntimeEvent } from 'ontime-types';
|
||||||
import { editEvent, getEventWithId } from '../services/rundown-service/RundownService.js';
|
import { editEvent, getEventWithId } from '../services/rundown-service/RundownService.js';
|
||||||
import { coerceString, coerceNumber, coerceBoolean, coerceColour } from '../utils/coerceType.js';
|
import { coerceString, coerceNumber, coerceBoolean, coerceColour } from '../utils/coerceType.js';
|
||||||
import { logger } from '../classes/Logger.js';
|
|
||||||
|
|
||||||
const whitelistedPayload = {
|
const whitelistedPayload = {
|
||||||
title: coerceString,
|
title: coerceString,
|
||||||
@@ -62,9 +61,7 @@ export function updateEvent(
|
|||||||
propertiesToUpdate.timeEnd = event.timeStart + propertiesToUpdate.duration;
|
propertiesToUpdate.timeEnd = event.timeStart + propertiesToUpdate.duration;
|
||||||
}
|
}
|
||||||
|
|
||||||
editEvent({ id: eventId, ...propertiesToUpdate }).then(() => {
|
editEvent({ id: eventId, ...propertiesToUpdate });
|
||||||
logger.info(LogOrigin.Playback, `Updated ${propertyName} of event with ID ${eventId} to ${newValue}`);
|
|
||||||
});
|
|
||||||
} else {
|
} else {
|
||||||
throw new Error(`Event with ID ${eventId} not found`);
|
throw new Error(`Event with ID ${eventId} not found`);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -32,9 +32,7 @@ import {
|
|||||||
import { oscIntegration } from '../services/integration-service/OscIntegration.js';
|
import { oscIntegration } from '../services/integration-service/OscIntegration.js';
|
||||||
import { httpIntegration } from '../services/integration-service/HttpIntegration.js';
|
import { httpIntegration } from '../services/integration-service/HttpIntegration.js';
|
||||||
import { logger } from '../classes/Logger.js';
|
import { logger } from '../classes/Logger.js';
|
||||||
import { deleteAllEvents, notifyChanges } from '../services/rundown-service/RundownService.js';
|
import { deleteAllEvents, notifyChanges, setRundown } from '../services/rundown-service/RundownService.js';
|
||||||
import { runtimeCacheStore } from '../stores/cachingStore.js';
|
|
||||||
import { delayedRundownCacheKey } from '../services/rundown-service/rundownCache.js';
|
|
||||||
import { integrationService } from '../services/integration-service/IntegrationService.js';
|
import { integrationService } from '../services/integration-service/IntegrationService.js';
|
||||||
import { getProjectFiles } from '../utils/getFileListFromFolder.js';
|
import { getProjectFiles } from '../utils/getFileListFromFolder.js';
|
||||||
import { configService } from '../services/ConfigService.js';
|
import { configService } from '../services/ConfigService.js';
|
||||||
@@ -101,12 +99,14 @@ const parseAndApply = async (file, _req, res, options) => {
|
|||||||
runtimeService.stop();
|
runtimeService.stop();
|
||||||
|
|
||||||
const newRundown = result.rundown || [];
|
const newRundown = result.rundown || [];
|
||||||
|
const { rundown, ...rest } = result;
|
||||||
if (options?.onlyRundown === 'true') {
|
if (options?.onlyRundown === 'true') {
|
||||||
await DataProvider.setRundown(newRundown);
|
setRundown(newRundown ?? []);
|
||||||
} else {
|
} else {
|
||||||
await DataProvider.mergeIntoData(result);
|
await DataProvider.mergeIntoData(rest);
|
||||||
|
setRundown(rundown ?? []);
|
||||||
}
|
}
|
||||||
notifyChanges({ timer: true, external: true, reset: true });
|
notifyChanges({ timer: true, external: true });
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -403,15 +403,14 @@ export async function patchPartialProjectFile(req, res) {
|
|||||||
osc: req.body?.osc,
|
osc: req.body?.osc,
|
||||||
aliases: req.body?.aliases,
|
aliases: req.body?.aliases,
|
||||||
userFields: req.body?.userFields,
|
userFields: req.body?.userFields,
|
||||||
rundown: req.body?.rundown,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const maybeRundown = req.body?.rundown;
|
||||||
await DataProvider.mergeIntoData(patchDb);
|
await DataProvider.mergeIntoData(patchDb);
|
||||||
if (patchDb.rundown !== undefined) {
|
if (maybeRundown !== undefined) {
|
||||||
// it is likely cheaper to invalidate cache than to calculate diff
|
// it is likely cheaper to invalidate cache than to calculate diff
|
||||||
runtimeService.stop();
|
runtimeService.stop();
|
||||||
runtimeCacheStore.invalidate(delayedRundownCacheKey);
|
await setRundown(maybeRundown);
|
||||||
notifyChanges({ external: true, reset: true });
|
|
||||||
}
|
}
|
||||||
res.status(200).send();
|
res.status(200).send();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -453,7 +452,6 @@ export async function previewExcel(req, res) {
|
|||||||
const data = await parseFile(file, req, res, options);
|
const data = await parseFile(file, req, res, options);
|
||||||
res.status(200).send(data);
|
res.status(200).send(data);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.log(error)
|
|
||||||
res.status(500).send({ message: error.toString() });
|
res.status(500).send({ message: error.toString() });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -626,6 +624,7 @@ export const createProjectFile: RequestHandler = async (req, res) => {
|
|||||||
return res.status(409).send({ message: errors.join(', ') });
|
return res.status(409).send({ message: errors.join(', ') });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
console.log(`----------------> Creating directory createProjectFile: ${projectFilePath}`);
|
||||||
await writeFile(projectFilePath, JSON.stringify(dbModel));
|
await writeFile(projectFilePath, JSON.stringify(dbModel));
|
||||||
|
|
||||||
res.status(200).send({
|
res.status(200).send({
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { GetRundownCached } from 'ontime-types';
|
import { RundownCached } from 'ontime-types';
|
||||||
|
|
||||||
import { Request, Response, RequestHandler } from 'express';
|
import { Request, Response, RequestHandler } from 'express';
|
||||||
|
|
||||||
@@ -13,19 +13,20 @@ import {
|
|||||||
reorderEvent,
|
reorderEvent,
|
||||||
swapEvents,
|
swapEvents,
|
||||||
} from '../services/rundown-service/RundownService.js';
|
} from '../services/rundown-service/RundownService.js';
|
||||||
import { getDelayedRundown, getRundownCache } from '../services/rundown-service/rundownCache.js';
|
import { get } from '../services/rundown-service/rundownCache.js';
|
||||||
|
import { DataProvider } from '../classes/data-provider/DataProvider.js';
|
||||||
|
|
||||||
// Create controller for GET request to '/events'
|
// Create controller for GET request to '/events'
|
||||||
// Returns -
|
// Returns -
|
||||||
export const rundownGetAll: RequestHandler = async (_req, res) => {
|
export const rundownGetAll: RequestHandler = async (_req, res) => {
|
||||||
const delayedRundown = getDelayedRundown();
|
const rundown = DataProvider.getRundown();
|
||||||
res.json(delayedRundown);
|
res.json(rundown);
|
||||||
};
|
};
|
||||||
|
|
||||||
// Create controller for GET request to '/events/cached'
|
// Create controller for GET request to '/events/cached'
|
||||||
// Returns -
|
// Returns -
|
||||||
export const rundownGetCached: RequestHandler = async (_req: Request, res: Response<GetRundownCached>) => {
|
export const rundownGetCached: RequestHandler = async (_req: Request, res: Response<RundownCached>) => {
|
||||||
const cachedRundown = getRundownCache();
|
const cachedRundown = get();
|
||||||
res.json(cachedRundown);
|
res.json(cachedRundown);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -32,7 +32,6 @@ export const event: Omit<OntimeEvent, 'id' | 'delay' | 'cue'> = {
|
|||||||
export const delay: Omit<OntimeDelay, 'id'> = {
|
export const delay: Omit<OntimeDelay, 'id'> = {
|
||||||
duration: 0,
|
duration: 0,
|
||||||
type: SupportedEvent.Delay,
|
type: SupportedEvent.Delay,
|
||||||
revision: 0,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const block: Omit<OntimeBlock, 'id'> = {
|
export const block: Omit<OntimeBlock, 'id'> = {
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
This directory holds the demo file shipped with Ontime
|
||||||
|
|||||||
@@ -1,119 +1,87 @@
|
|||||||
import {
|
import {
|
||||||
LogOrigin,
|
LogOrigin,
|
||||||
OntimeBaseEvent,
|
|
||||||
OntimeBlock,
|
OntimeBlock,
|
||||||
OntimeDelay,
|
OntimeDelay,
|
||||||
OntimeEvent,
|
OntimeEvent,
|
||||||
SupportedEvent,
|
OntimeRundown,
|
||||||
|
OntimeRundownEntry,
|
||||||
|
isOntimeBlock,
|
||||||
|
isOntimeDelay,
|
||||||
isOntimeEvent,
|
isOntimeEvent,
|
||||||
} from 'ontime-types';
|
} from 'ontime-types';
|
||||||
import { generateId, getCueCandidate } from 'ontime-utils';
|
import { getCueCandidate } from 'ontime-utils';
|
||||||
|
|
||||||
import { DataProvider } from '../../classes/data-provider/DataProvider.js';
|
import { DataProvider } from '../../classes/data-provider/DataProvider.js';
|
||||||
import { block as blockDef, delay as delayDef } from '../../models/eventsDefinition.js';
|
import { block as blockDef, delay as delayDef } from '../../models/eventsDefinition.js';
|
||||||
import { sendRefetch } from '../../adapters/websocketAux.js';
|
import { sendRefetch } from '../../adapters/websocketAux.js';
|
||||||
import { runtimeCacheStore } from '../../stores/cachingStore.js';
|
|
||||||
import {
|
|
||||||
cachedAdd,
|
|
||||||
cachedApplyDelay,
|
|
||||||
cachedClear,
|
|
||||||
cachedDelete,
|
|
||||||
cachedEdit,
|
|
||||||
cachedBatchEdit,
|
|
||||||
cachedReorder,
|
|
||||||
cachedSwap,
|
|
||||||
delayedRundownCacheKey,
|
|
||||||
} from './rundownCache.js';
|
|
||||||
import { logger } from '../../classes/Logger.js';
|
import { logger } from '../../classes/Logger.js';
|
||||||
import { createEvent } from '../../utils/parser.js';
|
import { createEvent } from '../../utils/parser.js';
|
||||||
import { updateNumEvents } from '../../stores/runtimeState.js';
|
import { updateNumEvents } from '../../stores/runtimeState.js';
|
||||||
import { runtimeService } from '../runtime-service/RuntimeService.js';
|
import { runtimeService } from '../runtime-service/RuntimeService.js';
|
||||||
|
|
||||||
/**
|
import * as cache from './rundownCache.js';
|
||||||
* Forces rundown to be recalculated
|
|
||||||
* To be used when we know the rundown has changed completely
|
function generateEvent(eventData: Partial<OntimeEvent> | Partial<OntimeDelay> | Partial<OntimeBlock>) {
|
||||||
*/
|
// we discard any UI provided events and add our own
|
||||||
export function forceReset() {
|
const id = cache.getUniqueId();
|
||||||
runtimeService.reset();
|
|
||||||
runtimeCacheStore.invalidate(delayedRundownCacheKey);
|
if (isOntimeEvent(eventData)) {
|
||||||
|
return createEvent(eventData, getCueCandidate(DataProvider.getRundown(), eventData?.after)) as OntimeEvent;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isOntimeDelay(eventData)) {
|
||||||
|
return { ...delayDef, duration: eventData.duration ?? 0, id } as OntimeDelay;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isOntimeBlock(eventData)) {
|
||||||
|
return { ...blockDef, title: eventData?.title ?? '', id } as OntimeBlock;
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new Error('Invalid event type');
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @description creates a new event with given data
|
* @description creates a new event with given data
|
||||||
* @param {object} eventData
|
* @param {object} eventData
|
||||||
* @return {unknown[]}
|
* @return {OntimeRundownEntry}
|
||||||
*/
|
*/
|
||||||
export async function addEvent(eventData: Partial<OntimeEvent> | Partial<OntimeDelay> | Partial<OntimeBlock>) {
|
export async function addEvent(eventData: Partial<OntimeEvent> | Partial<OntimeDelay> | Partial<OntimeBlock>) {
|
||||||
let newEvent: Partial<OntimeBaseEvent> = {};
|
// if the user didnt provide an index, we add the event to start
|
||||||
const id = generateId();
|
let atIndex = 0;
|
||||||
|
|
||||||
let insertIndex = 0;
|
|
||||||
if (eventData?.after !== undefined) {
|
if (eventData?.after !== undefined) {
|
||||||
const index = DataProvider.getIndexOf(eventData.after);
|
const previousIndex = cache.getIndexOf(eventData.after);
|
||||||
if (index < 0) {
|
if (previousIndex < 0) {
|
||||||
logger.warning(LogOrigin.Server, `Could not find event with id ${eventData.after}`);
|
logger.warning(LogOrigin.Server, `Could not find event with id ${eventData.after}`);
|
||||||
} else {
|
} else {
|
||||||
insertIndex = index + 1;
|
atIndex = previousIndex + 1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
switch (eventData.type) {
|
// generate a fully formed event from the patch
|
||||||
case SupportedEvent.Event: {
|
const eventToAdd = generateEvent(eventData);
|
||||||
newEvent = createEvent(eventData, getCueCandidate(DataProvider.getRundown(), eventData?.after)) as OntimeEvent;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case SupportedEvent.Delay:
|
|
||||||
newEvent = { ...delayDef, duration: eventData.duration, id } as OntimeDelay;
|
|
||||||
break;
|
|
||||||
case SupportedEvent.Block:
|
|
||||||
newEvent = { ...blockDef, title: eventData.title, id } as OntimeBlock;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
delete eventData.after;
|
|
||||||
|
|
||||||
// modify rundown
|
// modify rundown
|
||||||
await cachedAdd(insertIndex, newEvent as OntimeEvent | OntimeDelay | OntimeBlock);
|
const scopedMutation = cache.mutateCache(cache.add);
|
||||||
|
const { newEvent } = await scopedMutation({ atIndex, event: eventToAdd as OntimeRundownEntry });
|
||||||
notifyChanges({ timer: [id], external: true });
|
|
||||||
|
|
||||||
// notify event loader that rundown size has changed
|
|
||||||
updateChangeNumEvents();
|
|
||||||
|
|
||||||
return newEvent;
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function editEvent(eventData: Partial<OntimeEvent> | Partial<OntimeBlock> | Partial<OntimeDelay>) {
|
|
||||||
if (!eventData?.id) {
|
|
||||||
throw new Error('Event misses ID');
|
|
||||||
}
|
|
||||||
if (isOntimeEvent(eventData) && eventData?.cue === '') {
|
|
||||||
throw new Error('Cue value invalid');
|
|
||||||
}
|
|
||||||
|
|
||||||
const newEvent = await cachedEdit(eventData.id, eventData);
|
|
||||||
|
|
||||||
notifyChanges({ timer: [newEvent.id], external: true });
|
notifyChanges({ timer: [newEvent.id], external: true });
|
||||||
|
|
||||||
|
// notify runtime that rundown size has changed
|
||||||
|
updateChangeNumEvents();
|
||||||
|
|
||||||
return newEvent;
|
return newEvent;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function batchEditEvents(ids: string[], data: Partial<OntimeEvent>) {
|
|
||||||
await cachedBatchEdit(ids, data);
|
|
||||||
|
|
||||||
// notify runtime service of changed events
|
|
||||||
runtimeService.update(ids);
|
|
||||||
|
|
||||||
// advice socket subscribers of change
|
|
||||||
sendRefetch();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* deletes event by its ID
|
* deletes event by its ID
|
||||||
* @param eventId
|
* @param eventId
|
||||||
* @returns {Promise<void>}
|
* @returns {Promise<void>}
|
||||||
*/
|
*/
|
||||||
export async function deleteEvent(eventId: string) {
|
export async function deleteEvent(eventId: string) {
|
||||||
await cachedDelete(eventId);
|
const scopedMutation = cache.mutateCache(cache.remove);
|
||||||
|
await scopedMutation({ eventId });
|
||||||
|
|
||||||
notifyChanges({ timer: [eventId], external: true });
|
notifyChanges({ timer: [eventId], external: true });
|
||||||
|
|
||||||
// notify event loader that rundown size has changed
|
// notify event loader that rundown size has changed
|
||||||
updateChangeNumEvents();
|
updateChangeNumEvents();
|
||||||
}
|
}
|
||||||
@@ -123,10 +91,31 @@ export async function deleteEvent(eventId: string) {
|
|||||||
* @returns {Promise<void>}
|
* @returns {Promise<void>}
|
||||||
*/
|
*/
|
||||||
export async function deleteAllEvents() {
|
export async function deleteAllEvents() {
|
||||||
await cachedClear();
|
const scopedMutation = cache.mutateCache(cache.removeAll);
|
||||||
|
await scopedMutation({});
|
||||||
|
|
||||||
// no need to modify timer since we will reset
|
// no need to modify timer since we will reset
|
||||||
notifyChanges({ external: true, reset: true });
|
notifyChanges({ external: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function editEvent(patch: Partial<OntimeEvent> | Partial<OntimeBlock> | Partial<OntimeDelay>) {
|
||||||
|
if (isOntimeEvent(patch) && patch?.cue === '') {
|
||||||
|
throw new Error('Cue value invalid');
|
||||||
|
}
|
||||||
|
|
||||||
|
const scopedMutation = cache.mutateCache(cache.edit);
|
||||||
|
const { newEvent } = await scopedMutation({ patch, eventId: patch.id });
|
||||||
|
|
||||||
|
notifyChanges({ timer: [patch.id], external: true });
|
||||||
|
|
||||||
|
return newEvent;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function batchEditEvents(ids: string[], data: Partial<OntimeEvent>) {
|
||||||
|
const scopedMutation = cache.mutateCache(cache.batchEdit);
|
||||||
|
await scopedMutation({ patch: data, eventIds: ids });
|
||||||
|
|
||||||
|
notifyChanges({ timer: ids, external: true });
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -137,7 +126,8 @@ export async function deleteAllEvents() {
|
|||||||
* @returns {Promise<void>}
|
* @returns {Promise<void>}
|
||||||
*/
|
*/
|
||||||
export async function reorderEvent(eventId: string, from: number, to: number) {
|
export async function reorderEvent(eventId: string, from: number, to: number) {
|
||||||
const reorderedItem = await cachedReorder(eventId, from, to);
|
const scopedMutation = cache.mutateCache(cache.reorder);
|
||||||
|
const reorderedItem = await scopedMutation({ eventId, from, to });
|
||||||
|
|
||||||
notifyChanges({ timer: true, external: true });
|
notifyChanges({ timer: true, external: true });
|
||||||
|
|
||||||
@@ -145,7 +135,8 @@ export async function reorderEvent(eventId: string, from: number, to: number) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export async function applyDelay(eventId: string) {
|
export async function applyDelay(eventId: string) {
|
||||||
await cachedApplyDelay(eventId);
|
const scopedMutation = cache.mutateCache(cache.applyDelay);
|
||||||
|
await scopedMutation({ eventId });
|
||||||
|
|
||||||
notifyChanges({ timer: true, external: true });
|
notifyChanges({ timer: true, external: true });
|
||||||
}
|
}
|
||||||
@@ -157,7 +148,8 @@ export async function applyDelay(eventId: string) {
|
|||||||
* @returns {Promise<void>}
|
* @returns {Promise<void>}
|
||||||
*/
|
*/
|
||||||
export async function swapEvents(from: string, to: string) {
|
export async function swapEvents(from: string, to: string) {
|
||||||
await cachedSwap(from, to);
|
const scopedMutation = cache.mutateCache(cache.swap);
|
||||||
|
await scopedMutation({ fromId: from, toId: to });
|
||||||
|
|
||||||
notifyChanges({ timer: true, external: true });
|
notifyChanges({ timer: true, external: true });
|
||||||
}
|
}
|
||||||
@@ -174,7 +166,7 @@ function updateChangeNumEvents() {
|
|||||||
/**
|
/**
|
||||||
* Notify services of changes in the rundown
|
* Notify services of changes in the rundown
|
||||||
*/
|
*/
|
||||||
export function notifyChanges(options: { timer?: boolean | string[]; external?: boolean; reset?: boolean }) {
|
export function notifyChanges(options: { timer?: boolean | string[]; external?: boolean }) {
|
||||||
if (options.timer) {
|
if (options.timer) {
|
||||||
// notify timer service of changed events
|
// notify timer service of changed events
|
||||||
// timer can be true or an array of changed IDs
|
// timer can be true or an array of changed IDs
|
||||||
@@ -184,17 +176,20 @@ export function notifyChanges(options: { timer?: boolean | string[]; external?:
|
|||||||
runtimeService.update();
|
runtimeService.update();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (options.reset) {
|
|
||||||
// force rundown to be recalculated
|
|
||||||
forceReset();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (options.external) {
|
if (options.external) {
|
||||||
// advice socket subscribers of change
|
// advice socket subscribers of change
|
||||||
sendRefetch();
|
sendRefetch();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* returns entire unfiltered rundown
|
||||||
|
* @return {array}
|
||||||
|
*/
|
||||||
|
export function getRundown(): OntimeRundown {
|
||||||
|
return DataProvider.getRundown();
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* returns all events of type OntimeEvent
|
* returns all events of type OntimeEvent
|
||||||
* @return {array}
|
* @return {array}
|
||||||
@@ -290,3 +285,9 @@ export function findNext(currentEventId?: string): OntimeEvent | null {
|
|||||||
const nextEvent = timedEvents.at(newIndex);
|
const nextEvent = timedEvents.at(newIndex);
|
||||||
return nextEvent ?? null;
|
return nextEvent ?? null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function setRundown(rundown: OntimeRundown) {
|
||||||
|
await DataProvider.setRundown(rundown);
|
||||||
|
cache.init(rundown);
|
||||||
|
notifyChanges({ timer: true });
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { OntimeBlock, OntimeDelay, OntimeEvent, OntimeRundown, SupportedEvent } from 'ontime-types';
|
import { OntimeBlock, OntimeDelay, OntimeEvent, OntimeRundown, SupportedEvent } from 'ontime-types';
|
||||||
import { applyDelay } from '../delayUtils.js';
|
import { apply } from '../delayUtils.js';
|
||||||
|
|
||||||
describe('_applyDelay() ', () => {
|
describe('apply() ', () => {
|
||||||
describe('in a rundown without the delay field, persisted rundown', () => {
|
describe('in a rundown without the delay field, persisted rundown', () => {
|
||||||
it('applies delays', () => {
|
it('applies delays', () => {
|
||||||
const delayId = '1';
|
const delayId = '1';
|
||||||
@@ -20,7 +20,7 @@ describe('_applyDelay() ', () => {
|
|||||||
{ id: '5', type: SupportedEvent.Event, timeStart: 0, timeEnd: 10, duration: 10, revision: 1 } as OntimeEvent,
|
{ id: '5', type: SupportedEvent.Event, timeStart: 0, timeEnd: 10, duration: 10, revision: 1 } as OntimeEvent,
|
||||||
];
|
];
|
||||||
|
|
||||||
const updatedRundown = applyDelay(delayId, testRundown);
|
const updatedRundown = apply(delayId, testRundown);
|
||||||
expect(updatedRundown).toStrictEqual(expected);
|
expect(updatedRundown).toStrictEqual(expected);
|
||||||
});
|
});
|
||||||
it('applies negative delays', () => {
|
it('applies negative delays', () => {
|
||||||
@@ -40,7 +40,7 @@ describe('_applyDelay() ', () => {
|
|||||||
{ id: '5', type: SupportedEvent.Event, timeStart: 0, timeEnd: 10, duration: 10, revision: 1 } as OntimeEvent,
|
{ id: '5', type: SupportedEvent.Event, timeStart: 0, timeEnd: 10, duration: 10, revision: 1 } as OntimeEvent,
|
||||||
];
|
];
|
||||||
|
|
||||||
const updatedRundown = applyDelay(delayId, testRundown);
|
const updatedRundown = apply(delayId, testRundown);
|
||||||
expect(updatedRundown).toStrictEqual(expected);
|
expect(updatedRundown).toStrictEqual(expected);
|
||||||
});
|
});
|
||||||
it('maintains constant duration', () => {
|
it('maintains constant duration', () => {
|
||||||
@@ -56,7 +56,7 @@ describe('_applyDelay() ', () => {
|
|||||||
{ id: '3', type: SupportedEvent.Event, timeStart: 0, timeEnd: 20, duration: 20, revision: 2 } as OntimeEvent,
|
{ id: '3', type: SupportedEvent.Event, timeStart: 0, timeEnd: 20, duration: 20, revision: 2 } as OntimeEvent,
|
||||||
];
|
];
|
||||||
|
|
||||||
const updatedRundown = applyDelay(delayId, testRundown);
|
const updatedRundown = apply(delayId, testRundown);
|
||||||
expect(updatedRundown).toStrictEqual(expected);
|
expect(updatedRundown).toStrictEqual(expected);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -126,7 +126,7 @@ describe('_applyDelay() ', () => {
|
|||||||
} as OntimeEvent,
|
} as OntimeEvent,
|
||||||
];
|
];
|
||||||
|
|
||||||
const updatedRundown = applyDelay(delayId, testRundown);
|
const updatedRundown = apply(delayId, testRundown);
|
||||||
expect(updatedRundown).toStrictEqual(expected);
|
expect(updatedRundown).toStrictEqual(expected);
|
||||||
});
|
});
|
||||||
it('applies negative delays', () => {
|
it('applies negative delays', () => {
|
||||||
@@ -194,7 +194,7 @@ describe('_applyDelay() ', () => {
|
|||||||
} as OntimeEvent,
|
} as OntimeEvent,
|
||||||
];
|
];
|
||||||
|
|
||||||
const updatedRundown = applyDelay(delayId, testRundown);
|
const updatedRundown = apply(delayId, testRundown);
|
||||||
expect(updatedRundown).toStrictEqual(expected);
|
expect(updatedRundown).toStrictEqual(expected);
|
||||||
});
|
});
|
||||||
it('maintains constant duration', () => {
|
it('maintains constant duration', () => {
|
||||||
@@ -242,7 +242,7 @@ describe('_applyDelay() ', () => {
|
|||||||
} as OntimeEvent,
|
} as OntimeEvent,
|
||||||
];
|
];
|
||||||
|
|
||||||
const updatedRundown = applyDelay(delayId, testRundown);
|
const updatedRundown = apply(delayId, testRundown);
|
||||||
expect(updatedRundown).toStrictEqual(expected);
|
expect(updatedRundown).toStrictEqual(expected);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,6 +1,131 @@
|
|||||||
import { EndAction, OntimeEvent, OntimeRundown, SupportedEvent, TimerType } from 'ontime-types';
|
import { EndAction, OntimeEvent, OntimeRundown, SupportedEvent, TimerType } from 'ontime-types';
|
||||||
|
|
||||||
import { calculateRuntimeDelays, getDelayAt, calculateRuntimeDelaysFrom } from '../delayUtils.js';
|
import { calculateRuntimeDelays, getDelayAt, calculateRuntimeDelaysFrom } from '../delayUtils.js';
|
||||||
|
import { add, batchEdit, edit, remove, reorder, swap } from '../rundownCache.js';
|
||||||
|
|
||||||
|
describe('add() mutation', () => {
|
||||||
|
test('adds an event to the rundown', () => {
|
||||||
|
const mockEvent = { id: 'mock', cue: 'mock', type: SupportedEvent.Event } as OntimeEvent;
|
||||||
|
const testRundown: OntimeRundown = [];
|
||||||
|
const { newRundown } = add({ atIndex: 0, event: mockEvent, persistedRundown: testRundown });
|
||||||
|
expect(newRundown.length).toBe(1);
|
||||||
|
expect(newRundown[0]).toMatchObject(mockEvent);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('remove() mutation', () => {
|
||||||
|
test('deletes an event from the rundown', () => {
|
||||||
|
const mockEvent = { id: 'mock', cue: 'mock', type: SupportedEvent.Event } as OntimeEvent;
|
||||||
|
const testRundown: OntimeRundown = [mockEvent];
|
||||||
|
const { newRundown } = remove({ eventId: mockEvent.id, persistedRundown: testRundown });
|
||||||
|
expect(newRundown.length).toBe(0);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('edit() mutation', () => {
|
||||||
|
test('edits an event in the rundown', () => {
|
||||||
|
const mockEvent = { id: 'mock', cue: 'mock', type: SupportedEvent.Event } as OntimeEvent;
|
||||||
|
const mockEventPatch = { cue: 'patched' } as OntimeEvent;
|
||||||
|
const testRundown: OntimeRundown = [mockEvent];
|
||||||
|
const { newRundown, newEvent } = edit({
|
||||||
|
eventId: mockEvent.id,
|
||||||
|
patch: mockEventPatch,
|
||||||
|
persistedRundown: testRundown,
|
||||||
|
});
|
||||||
|
expect(newRundown.length).toBe(1);
|
||||||
|
expect(newEvent).toMatchObject({
|
||||||
|
id: 'mock',
|
||||||
|
cue: 'patched',
|
||||||
|
type: SupportedEvent.Event,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('batchEdit() mutation', () => {
|
||||||
|
it('should correctly apply the patch to the events with the given IDs', () => {
|
||||||
|
const persistedRundown: OntimeRundown = [
|
||||||
|
{ id: '1', type: SupportedEvent.Event, cue: 'data1' } as OntimeEvent,
|
||||||
|
{ id: '2', type: SupportedEvent.Event, cue: 'data2' } as OntimeEvent,
|
||||||
|
{ id: '3', type: SupportedEvent.Event, cue: 'data3' } as OntimeEvent,
|
||||||
|
];
|
||||||
|
const eventIds = ['1', '3'];
|
||||||
|
const patch = { cue: 'newData' };
|
||||||
|
|
||||||
|
const { newRundown } = batchEdit({ persistedRundown, eventIds, patch });
|
||||||
|
|
||||||
|
expect(newRundown).toMatchObject([
|
||||||
|
{ id: '1', type: SupportedEvent.Event, cue: 'newData' },
|
||||||
|
{ id: '2', type: SupportedEvent.Event, cue: 'data2' },
|
||||||
|
{ id: '3', type: SupportedEvent.Event, cue: 'newData' },
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('reorder() mutation', () => {
|
||||||
|
it('should correctly reorder two events', () => {
|
||||||
|
const persistedRundown: OntimeRundown = [
|
||||||
|
{ id: '1', type: SupportedEvent.Event, cue: 'data1', revision: 0 } as OntimeEvent,
|
||||||
|
{ id: '2', type: SupportedEvent.Event, cue: 'data2', revision: 0 } as OntimeEvent,
|
||||||
|
{ id: '3', type: SupportedEvent.Event, cue: 'data3', revision: 0 } as OntimeEvent,
|
||||||
|
];
|
||||||
|
const { newRundown } = reorder({
|
||||||
|
persistedRundown,
|
||||||
|
eventId: persistedRundown[0].id,
|
||||||
|
from: 0,
|
||||||
|
to: persistedRundown.length - 1,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(newRundown).toMatchObject([
|
||||||
|
{ id: '2', type: SupportedEvent.Event, cue: 'data2', revision: 1 },
|
||||||
|
{ id: '3', type: SupportedEvent.Event, cue: 'data3', revision: 1 },
|
||||||
|
{ id: '1', type: SupportedEvent.Event, cue: 'data1', revision: 1 },
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('swap() mutation', () => {
|
||||||
|
it('should correctly swap data between events', () => {
|
||||||
|
const persistedRundown: OntimeRundown = [
|
||||||
|
{ id: '1', type: SupportedEvent.Event, cue: 'data1', timeStart: 1, revision: 0 } as OntimeEvent,
|
||||||
|
{ id: '2', type: SupportedEvent.Event, cue: 'data2', timeStart: 2, revision: 0 } as OntimeEvent,
|
||||||
|
{ id: '3', type: SupportedEvent.Event, cue: 'data3', timeStart: 3, revision: 0 } as OntimeEvent,
|
||||||
|
];
|
||||||
|
const { newRundown } = swap({
|
||||||
|
persistedRundown,
|
||||||
|
fromId: persistedRundown[0].id,
|
||||||
|
toId: persistedRundown[1].id,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect((newRundown[0] as OntimeEvent).id).toBe('1');
|
||||||
|
expect((newRundown[0] as OntimeEvent).cue).toBe('data2');
|
||||||
|
expect((newRundown[0] as OntimeEvent).timeStart).toBe(1);
|
||||||
|
expect((newRundown[0] as OntimeEvent).revision).toBe(1);
|
||||||
|
|
||||||
|
expect((newRundown[1] as OntimeEvent).id).toBe('2');
|
||||||
|
expect((newRundown[1] as OntimeEvent).cue).toBe('data1');
|
||||||
|
expect((newRundown[1] as OntimeEvent).timeStart).toBe(2);
|
||||||
|
expect((newRundown[1] as OntimeEvent).revision).toBe(1);
|
||||||
|
|
||||||
|
expect((newRundown[2] as OntimeEvent).id).toBe('3');
|
||||||
|
expect((newRundown[2] as OntimeEvent).cue).toBe('data3');
|
||||||
|
expect((newRundown[2] as OntimeEvent).timeStart).toBe(3);
|
||||||
|
expect((newRundown[2] as OntimeEvent).revision).toBe(0);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
*
|
||||||
|
*
|
||||||
|
*
|
||||||
|
*
|
||||||
|
*
|
||||||
|
*
|
||||||
|
*
|
||||||
|
*
|
||||||
|
*
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
|
||||||
describe('calculateRuntimeDelays', () => {
|
describe('calculateRuntimeDelays', () => {
|
||||||
it('calculates all delays in a given rundown', () => {
|
it('calculates all delays in a given rundown', () => {
|
||||||
@@ -38,7 +163,6 @@ describe('calculateRuntimeDelays', () => {
|
|||||||
{
|
{
|
||||||
duration: 600000,
|
duration: 600000,
|
||||||
type: SupportedEvent.Delay,
|
type: SupportedEvent.Delay,
|
||||||
revision: 0,
|
|
||||||
id: '07986',
|
id: '07986',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -74,7 +198,6 @@ describe('calculateRuntimeDelays', () => {
|
|||||||
{
|
{
|
||||||
duration: 1200000,
|
duration: 1200000,
|
||||||
type: SupportedEvent.Delay,
|
type: SupportedEvent.Delay,
|
||||||
revision: 0,
|
|
||||||
id: '7db42',
|
id: '7db42',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -190,7 +313,6 @@ describe('getDelayAt()', () => {
|
|||||||
{
|
{
|
||||||
duration: 600000,
|
duration: 600000,
|
||||||
type: SupportedEvent.Delay,
|
type: SupportedEvent.Delay,
|
||||||
revision: 0,
|
|
||||||
id: '07986',
|
id: '07986',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -227,7 +349,6 @@ describe('getDelayAt()', () => {
|
|||||||
{
|
{
|
||||||
duration: 1200000,
|
duration: 1200000,
|
||||||
type: SupportedEvent.Delay,
|
type: SupportedEvent.Delay,
|
||||||
revision: 0,
|
|
||||||
id: '7db42',
|
id: '7db42',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -362,7 +483,6 @@ describe('calculateRuntimeDelaysFrom()', () => {
|
|||||||
{
|
{
|
||||||
duration: 600000,
|
duration: 600000,
|
||||||
type: SupportedEvent.Delay,
|
type: SupportedEvent.Delay,
|
||||||
revision: 0,
|
|
||||||
id: '07986',
|
id: '07986',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -399,7 +519,6 @@ describe('calculateRuntimeDelaysFrom()', () => {
|
|||||||
{
|
{
|
||||||
duration: 1200000,
|
duration: 1200000,
|
||||||
type: SupportedEvent.Delay,
|
type: SupportedEvent.Delay,
|
||||||
revision: 0,
|
|
||||||
id: '7db42',
|
id: '7db42',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { OntimeRundown, isOntimeDelay, isOntimeBlock, isOntimeEvent } from 'ontime-types';
|
import { OntimeRundown, isOntimeDelay, isOntimeBlock, isOntimeEvent } from 'ontime-types';
|
||||||
|
|
||||||
import { deleteAtIndex } from '../../utils/arrayUtils.js';
|
import { deleteAtIndex } from '../../../../../packages/utils/src/array-utils/arrayUtils.js';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Calculates all delays in a given rundown
|
* Calculates all delays in a given rundown
|
||||||
@@ -94,9 +94,10 @@ export function getDelayAt(eventIndex: number, rundown: OntimeRundown): number {
|
|||||||
* Applies delay from given event ID, deletes the delay event after
|
* Applies delay from given event ID, deletes the delay event after
|
||||||
* @param eventId
|
* @param eventId
|
||||||
* @param rundown
|
* @param rundown
|
||||||
|
* @throws {Error} if event ID not found or is not a delay
|
||||||
* @returns
|
* @returns
|
||||||
*/
|
*/
|
||||||
export function applyDelay(eventId: string, rundown: OntimeRundown): OntimeRundown {
|
export function apply(eventId: string, rundown: OntimeRundown): OntimeRundown {
|
||||||
const delayIndex = rundown.findIndex((event) => event.id === eventId);
|
const delayIndex = rundown.findIndex((event) => event.id === eventId);
|
||||||
const delayEvent = rundown.at(delayIndex);
|
const delayEvent = rundown.at(delayIndex);
|
||||||
|
|
||||||
|
|||||||
@@ -1,288 +1,260 @@
|
|||||||
import {
|
import {
|
||||||
GetRundownCached,
|
|
||||||
isOntimeBlock,
|
isOntimeBlock,
|
||||||
isOntimeDelay,
|
isOntimeDelay,
|
||||||
isOntimeEvent,
|
isOntimeEvent,
|
||||||
OntimeBlock,
|
|
||||||
OntimeDelay,
|
|
||||||
OntimeEvent,
|
OntimeEvent,
|
||||||
OntimeRundown,
|
OntimeRundown,
|
||||||
OntimeRundownEntry,
|
OntimeRundownEntry,
|
||||||
} from 'ontime-types';
|
} from 'ontime-types';
|
||||||
import { swapOntimeEvents } from 'ontime-utils';
|
import { generateId, deleteAtIndex, insertAtIndex, reorderArray, swapEventData } from 'ontime-utils';
|
||||||
|
|
||||||
import { DataProvider } from '../../classes/data-provider/DataProvider.js';
|
import { DataProvider } from '../../classes/data-provider/DataProvider.js';
|
||||||
import { getCached, runtimeCacheStore } from '../../stores/cachingStore.js';
|
|
||||||
import { isProduction } from '../../setup.js';
|
|
||||||
import { deleteAtIndex, insertAtIndex, reorderArray } from '../../utils/arrayUtils.js';
|
|
||||||
import { createPatch } from '../../utils/parser.js';
|
import { createPatch } from '../../utils/parser.js';
|
||||||
import { applyDelay, calculateRuntimeDelays, calculateRuntimeDelaysFromIndex, getDelayAt } from './delayUtils.js';
|
import { apply } from './delayUtils.js';
|
||||||
|
|
||||||
|
type NormalisedRundown = Record<string, OntimeRundownEntry>;
|
||||||
|
|
||||||
|
let rundown: NormalisedRundown = {};
|
||||||
|
let order: string[] = [];
|
||||||
|
let revision = 0;
|
||||||
|
let isStale = true;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Keep incremental revision number of rundown for runtime
|
* Utility initialises cache
|
||||||
|
* @param persistedRundown
|
||||||
*/
|
*/
|
||||||
let rundownRevision = 0;
|
export function init(persistedRundown: Readonly<OntimeRundown>) {
|
||||||
|
// we decided to try and re-write this dataset for every change
|
||||||
|
// instead of maintaining logic to update it
|
||||||
|
rundown = {};
|
||||||
|
order = [];
|
||||||
|
|
||||||
/**
|
let accumulatedDelay = 0;
|
||||||
* Key of rundown in cache
|
for (let i = 0; i < persistedRundown.length; i++) {
|
||||||
*/
|
const event = persistedRundown[i];
|
||||||
export const delayedRundownCacheKey = 'delayed-rundown';
|
|
||||||
|
|
||||||
/**
|
// calculate delays
|
||||||
* Invalidates the cached rundown when an inconsistency is found
|
if (isOntimeDelay(event)) {
|
||||||
* will throw when not in production
|
accumulatedDelay += event.duration;
|
||||||
* @param errorMessage
|
} else if (isOntimeBlock(event)) {
|
||||||
*/
|
accumulatedDelay = 0;
|
||||||
export function invalidateFromError(errorMessage = 'Found mismatch between store and cache') {
|
} else if (isOntimeEvent(event)) {
|
||||||
if (isProduction) {
|
event.delay = accumulatedDelay;
|
||||||
runtimeCacheStore.invalidate(delayedRundownCacheKey);
|
}
|
||||||
} else {
|
|
||||||
throw new Error(errorMessage);
|
order.push(event.id);
|
||||||
|
rundown[event.id] = { ...event };
|
||||||
}
|
}
|
||||||
|
isStale = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns rundown with calculated delays
|
* Returns an ID guaranteed to be unique
|
||||||
* Ensures request goes through the caching layer
|
* @returns
|
||||||
*/
|
*/
|
||||||
export function getRundownCache(): GetRundownCached {
|
export function getUniqueId(persistedRundown: Readonly<OntimeRundown> = getPersistedRundown()): string {
|
||||||
function calculateRundown() {
|
let id = '';
|
||||||
const rundown = DataProvider.getRundown();
|
do {
|
||||||
return calculateRuntimeDelays(rundown);
|
id = generateId();
|
||||||
|
} while (!isIdUnique(persistedRundown, id));
|
||||||
|
return id;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isIdUnique(persistedRundown: Readonly<OntimeRundown>, eventId: string) {
|
||||||
|
if (isStale) {
|
||||||
|
init(persistedRundown);
|
||||||
}
|
}
|
||||||
|
return !Object.hasOwn(rundown, eventId);
|
||||||
|
}
|
||||||
|
|
||||||
const cached = getCached(delayedRundownCacheKey, calculateRundown);
|
export function getIndexOf(eventId: string) {
|
||||||
|
if (isStale) {
|
||||||
|
init(getPersistedRundown());
|
||||||
|
}
|
||||||
|
return order.indexOf(eventId);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Utility function gets rundown from DataProvider
|
||||||
|
* @returns {OntimeRundown}
|
||||||
|
*/
|
||||||
|
export const getPersistedRundown = (): OntimeRundown => DataProvider.getRundown();
|
||||||
|
|
||||||
|
type RundownCache = {
|
||||||
|
rundown: NormalisedRundown;
|
||||||
|
order: string[];
|
||||||
|
revision: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns cached data
|
||||||
|
* @returns {RundownCache}
|
||||||
|
*/
|
||||||
|
export function get(): Readonly<RundownCache> {
|
||||||
|
if (isStale) {
|
||||||
|
console.time('rundownCache__init');
|
||||||
|
init(getPersistedRundown());
|
||||||
|
console.timeEnd('rundownCache__init');
|
||||||
|
}
|
||||||
return {
|
return {
|
||||||
rundown: cached,
|
rundown,
|
||||||
revision: rundownRevision,
|
order,
|
||||||
|
revision,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type CommonParams = { persistedRundown: OntimeRundown };
|
||||||
|
type MutationParams<T> = T & Partial<CommonParams>;
|
||||||
|
type MutatingReturn = {
|
||||||
|
newRundown: OntimeRundown;
|
||||||
|
newEvent?: OntimeRundownEntry;
|
||||||
|
};
|
||||||
|
type MutatingFn<T extends object> = (params: MutationParams<T>) => MutatingReturn;
|
||||||
/**
|
/**
|
||||||
* Returns rundown with calculated delays
|
* Decorators injects data into mutation
|
||||||
* Ensures request goes through the caching layer
|
* @param mutation
|
||||||
|
* @returns
|
||||||
*/
|
*/
|
||||||
export function getDelayedRundown() {
|
export function mutateCache<T extends object>(mutation: MutatingFn<T>) {
|
||||||
function calculateRundown() {
|
async function scopedMutation(params: T) {
|
||||||
const rundown = DataProvider.getRundown();
|
const persistedRundown = getPersistedRundown();
|
||||||
return calculateRuntimeDelays(rundown);
|
const { newEvent, newRundown } = mutation({ ...params, persistedRundown });
|
||||||
}
|
|
||||||
|
|
||||||
return getCached(delayedRundownCacheKey, calculateRundown);
|
revision = revision + 1;
|
||||||
|
isStale = true;
|
||||||
|
|
||||||
|
DataProvider.setRundown(newRundown);
|
||||||
|
// schedule the update to the next tick
|
||||||
|
|
||||||
|
process.nextTick(() => {
|
||||||
|
console.time('rundownCache__init');
|
||||||
|
init(newRundown);
|
||||||
|
console.timeEnd('rundownCache__init');
|
||||||
|
});
|
||||||
|
|
||||||
|
// TODO: could we return a patch object?
|
||||||
|
return { newEvent };
|
||||||
|
}
|
||||||
|
return scopedMutation;
|
||||||
|
}
|
||||||
|
|
||||||
|
type AddArgs = MutationParams<{ atIndex: number; event: OntimeRundownEntry }>;
|
||||||
|
export function add({ persistedRundown, atIndex, event }: AddArgs): Required<MutatingReturn> {
|
||||||
|
const newEvent: OntimeRundownEntry = { ...event };
|
||||||
|
const newRundown = insertAtIndex(atIndex, newEvent, persistedRundown);
|
||||||
|
|
||||||
|
return { newRundown, newEvent };
|
||||||
|
}
|
||||||
|
|
||||||
|
type RemoveArgs = MutationParams<{ eventId: string }>;
|
||||||
|
export function remove({ persistedRundown, eventId }: RemoveArgs): MutatingReturn {
|
||||||
|
const atIndex = persistedRundown.findIndex((event) => event.id === eventId);
|
||||||
|
const newRundown = deleteAtIndex(atIndex, persistedRundown);
|
||||||
|
|
||||||
|
return { newRundown };
|
||||||
|
}
|
||||||
|
|
||||||
|
export function removeAll(): { newRundown: OntimeRundown } {
|
||||||
|
return { newRundown: [] };
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Adds an event in the rundown at given index, ensuring replication to delayed rundown cache
|
* Utility function for patching events
|
||||||
* @param eventIndex
|
* @param eventFromRundown
|
||||||
* @param event
|
* @param patch
|
||||||
|
* @returns
|
||||||
*/
|
*/
|
||||||
export async function cachedAdd(eventIndex: number, event: OntimeEvent | OntimeDelay | OntimeBlock) {
|
function makeEvent(eventFromRundown: OntimeRundownEntry, patch: Partial<OntimeRundownEntry>): OntimeRundownEntry {
|
||||||
// TODO: create wrapper function
|
if (isOntimeEvent(eventFromRundown)) {
|
||||||
const rundown = DataProvider.getRundown();
|
const newEvent = createPatch(eventFromRundown, patch as OntimeEvent);
|
||||||
const newRundown = insertAtIndex(eventIndex, event, rundown);
|
newEvent.revision++;
|
||||||
|
return newEvent;
|
||||||
const delayedRundown = getDelayedRundown();
|
|
||||||
let newDelayedRundown = insertAtIndex(eventIndex, event, delayedRundown);
|
|
||||||
|
|
||||||
// update delay cache
|
|
||||||
if (isOntimeEvent(event)) {
|
|
||||||
// if it is an event, we need its delay
|
|
||||||
(newDelayedRundown[eventIndex] as OntimeEvent).delay = getDelayAt(eventIndex, newDelayedRundown);
|
|
||||||
} else {
|
|
||||||
// if it is a block or delay, we invalidate from here
|
|
||||||
newDelayedRundown = calculateRuntimeDelaysFromIndex(eventIndex, newDelayedRundown);
|
|
||||||
}
|
}
|
||||||
|
// TODO: exhaustive check
|
||||||
runtimeCacheStore.setCached(delayedRundownCacheKey, newDelayedRundown);
|
return { ...eventFromRundown, ...patch } as OntimeRundownEntry;
|
||||||
// we need to delay updating this to ensure add operation happens on same dataset
|
|
||||||
await DataProvider.setRundown(newRundown);
|
|
||||||
|
|
||||||
rundownRevision++;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
type EditArgs = MutationParams<{ eventId: string; patch: Partial<OntimeRundownEntry> }>;
|
||||||
* Edits an event in rundown, ensuring replication to delayed rundown cache
|
export function edit({ persistedRundown, eventId, patch }: EditArgs): Required<MutatingReturn> {
|
||||||
* @param eventId
|
const indexAt = persistedRundown.findIndex((event) => event.id === eventId);
|
||||||
* @param patchObject
|
|
||||||
*/
|
if (indexAt < 0) {
|
||||||
export async function cachedEdit(
|
throw new Error('Event not found');
|
||||||
eventId: string,
|
}
|
||||||
patchObject: Partial<OntimeEvent> | Partial<OntimeBlock> | Partial<OntimeDelay>,
|
|
||||||
) {
|
if (patch?.type && persistedRundown[indexAt].type !== patch.type) {
|
||||||
const makeEvent = (eventFromRundown: OntimeRundownEntry): OntimeRundownEntry => {
|
throw new Error('Invalid event type');
|
||||||
if (isOntimeEvent(eventFromRundown)) {
|
}
|
||||||
const newEvent = createPatch(eventFromRundown, patchObject as OntimeEvent);
|
|
||||||
newEvent.revision++;
|
const eventInMemory = persistedRundown[indexAt];
|
||||||
return newEvent;
|
const newEvent = makeEvent(eventInMemory, patch);
|
||||||
|
const newRundown = [...persistedRundown];
|
||||||
|
newRundown[indexAt] = newEvent;
|
||||||
|
|
||||||
|
return { newRundown, newEvent };
|
||||||
|
}
|
||||||
|
|
||||||
|
type BatchEditArgs = MutationParams<{ eventIds: string[]; patch: Partial<OntimeRundownEntry> }>;
|
||||||
|
export function batchEdit({ persistedRundown, eventIds, patch }: BatchEditArgs): MutatingReturn {
|
||||||
|
const ids = new Set(eventIds);
|
||||||
|
|
||||||
|
const newRundown = [];
|
||||||
|
for (let i = 0; i < persistedRundown.length; i++) {
|
||||||
|
if (ids.has(persistedRundown[i].id)) {
|
||||||
|
if (patch?.type && persistedRundown[i].type !== patch.type) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const newEvent = makeEvent(persistedRundown[i], patch);
|
||||||
|
newRundown.push(newEvent);
|
||||||
|
} else {
|
||||||
|
newRundown.push(persistedRundown[i]);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
return { newRundown };
|
||||||
|
}
|
||||||
|
|
||||||
return { ...eventFromRundown, ...patchObject } as OntimeRundownEntry;
|
type ReorderArgs = MutationParams<{ eventId: string; from: number; to: number }>;
|
||||||
};
|
export function reorder({ persistedRundown, eventId, from, to }: ReorderArgs): Required<MutatingReturn> {
|
||||||
|
const event = persistedRundown[from];
|
||||||
const indexInMemory = DataProvider.getIndexOf(eventId);
|
if (!event || eventId !== event.id) {
|
||||||
if (indexInMemory < 0) {
|
throw new Error('Event not found');
|
||||||
throw new Error('No event with ID found');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const updatedRundown = DataProvider.getRundown();
|
const newRundown = reorderArray(persistedRundown, from, to);
|
||||||
const eventFromRundown = updatedRundown[indexInMemory];
|
for (let i = from; i <= to; i++) {
|
||||||
|
const event = newRundown.at(i);
|
||||||
const isPatchObjectDifferentFromRundownEvent = Object.entries(patchObject).some(
|
if (isOntimeEvent(event)) {
|
||||||
([key, value]) => eventFromRundown[key] !== value,
|
event.revision += 1;
|
||||||
);
|
|
||||||
|
|
||||||
if (!isPatchObjectDifferentFromRundownEvent) {
|
|
||||||
return eventFromRundown;
|
|
||||||
}
|
|
||||||
|
|
||||||
const newEvent = makeEvent(eventFromRundown);
|
|
||||||
updatedRundown[indexInMemory] = newEvent;
|
|
||||||
|
|
||||||
let newDelayedRundown = getDelayedRundown();
|
|
||||||
if (newDelayedRundown?.[indexInMemory].id !== newEvent.id) {
|
|
||||||
invalidateFromError();
|
|
||||||
} else {
|
|
||||||
newDelayedRundown[indexInMemory] = newEvent;
|
|
||||||
if (isOntimeEvent(newEvent)) {
|
|
||||||
(newDelayedRundown[indexInMemory] as OntimeEvent).delay = getDelayAt(indexInMemory, newDelayedRundown);
|
|
||||||
} else if (isOntimeDelay(newEvent)) {
|
|
||||||
// blocks have no reason to change the rundown, from delays we need to recalculate
|
|
||||||
newDelayedRundown = calculateRuntimeDelaysFromIndex(indexInMemory, newDelayedRundown);
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
return { newRundown, newEvent: newRundown.at(from) };
|
||||||
|
}
|
||||||
|
|
||||||
runtimeCacheStore.setCached(delayedRundownCacheKey, newDelayedRundown);
|
type ApplyDelayArgs = MutationParams<{ eventId: string }>;
|
||||||
|
export function applyDelay({ persistedRundown, eventId }: ApplyDelayArgs): MutatingReturn {
|
||||||
|
const newRundown = apply(eventId, persistedRundown);
|
||||||
|
return { newRundown };
|
||||||
|
}
|
||||||
|
|
||||||
|
type SwapArgs = MutationParams<{ fromId: string; toId: string }>;
|
||||||
|
export function swap({ persistedRundown, fromId, toId }: SwapArgs): MutatingReturn {
|
||||||
|
const indexA = persistedRundown.findIndex((event) => event.id === fromId);
|
||||||
|
const eventA = persistedRundown.at(indexA);
|
||||||
|
|
||||||
|
const indexB = persistedRundown.findIndex((event) => event.id === toId);
|
||||||
|
const eventB = persistedRundown.at(indexB);
|
||||||
|
|
||||||
|
if (!isOntimeEvent(eventA) || !isOntimeEvent(eventB)) {
|
||||||
|
throw new Error('Swap only available for OntimeEvents');
|
||||||
}
|
}
|
||||||
|
|
||||||
// we need to delay updating this to ensure edit operation happens on same dataset
|
const { newA, newB } = swapEventData(eventA, eventB);
|
||||||
await DataProvider.setRundown(updatedRundown);
|
const newRundown = [...persistedRundown];
|
||||||
|
|
||||||
rundownRevision++;
|
newRundown[indexA] = newA;
|
||||||
|
(newRundown[indexA] as OntimeEvent).revision += 1;
|
||||||
|
newRundown[indexB] = newB;
|
||||||
|
(newRundown[indexB] as OntimeEvent).revision += 1;
|
||||||
|
|
||||||
return newEvent;
|
return { newRundown };
|
||||||
}
|
|
||||||
|
|
||||||
export async function cachedBatchEdit(ids: string[], patchObject: Partial<OntimeEvent>) {
|
|
||||||
const cachedEdits = ids.map((id) => cachedEdit(id, patchObject));
|
|
||||||
|
|
||||||
await Promise.allSettled(cachedEdits);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Deletes an event with given id from rundown, ensuring replication to delayed rundown cache
|
|
||||||
* @param eventId
|
|
||||||
*/
|
|
||||||
export async function cachedDelete(eventId: string) {
|
|
||||||
const eventIndex = DataProvider.getIndexOf(eventId);
|
|
||||||
let delayedRundown = getDelayedRundown();
|
|
||||||
|
|
||||||
if (eventIndex < 0) {
|
|
||||||
if (delayedRundown.findIndex((event) => event.id === eventId) >= 0) {
|
|
||||||
invalidateFromError();
|
|
||||||
}
|
|
||||||
throw new Error(`Event with id ${eventId} not found`);
|
|
||||||
}
|
|
||||||
|
|
||||||
let updatedRundown = DataProvider.getRundown();
|
|
||||||
const eventBack = { ...updatedRundown[eventIndex] };
|
|
||||||
updatedRundown = deleteAtIndex(eventIndex, updatedRundown);
|
|
||||||
if (eventId !== delayedRundown[eventIndex].id) {
|
|
||||||
invalidateFromError();
|
|
||||||
} else {
|
|
||||||
delayedRundown = deleteAtIndex(eventIndex, delayedRundown);
|
|
||||||
if (isOntimeDelay(eventBack) || isOntimeBlock(eventBack)) {
|
|
||||||
// for events, we do not have to worry
|
|
||||||
// the following event, would have taken the place of the deleted event by now
|
|
||||||
delayedRundown = calculateRuntimeDelaysFromIndex(eventIndex, delayedRundown);
|
|
||||||
}
|
|
||||||
runtimeCacheStore.setCached(delayedRundownCacheKey, delayedRundown);
|
|
||||||
}
|
|
||||||
// we need to delay updating this to ensure edit operation happens on same dataset
|
|
||||||
await DataProvider.setRundown(updatedRundown);
|
|
||||||
|
|
||||||
rundownRevision++;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Reorders an event in the rundown, ensuring replication to delayed rundown cache
|
|
||||||
* @param eventId
|
|
||||||
* @param from
|
|
||||||
* @param to
|
|
||||||
*/
|
|
||||||
export async function cachedReorder(eventId: string, from: number, to: number) {
|
|
||||||
const indexCheck = DataProvider.getIndexOf(eventId);
|
|
||||||
if (indexCheck !== from) {
|
|
||||||
invalidateFromError();
|
|
||||||
throw new Error('ID not found at index');
|
|
||||||
}
|
|
||||||
|
|
||||||
let updatedRundown = DataProvider.getRundown();
|
|
||||||
const reorderedEvent = updatedRundown[from];
|
|
||||||
updatedRundown = reorderArray(updatedRundown, from, to);
|
|
||||||
|
|
||||||
const delayedRundown = getDelayedRundown();
|
|
||||||
if (eventId !== delayedRundown[from].id) {
|
|
||||||
invalidateFromError();
|
|
||||||
} else {
|
|
||||||
// TODO: could we be more granular about updates
|
|
||||||
// I fear we need to update both from and to, which could signify more iterations
|
|
||||||
runtimeCacheStore.invalidate(delayedRundownCacheKey);
|
|
||||||
}
|
|
||||||
|
|
||||||
// we need to delay updating this to ensure edit operation happens on same dataset
|
|
||||||
await DataProvider.setRundown(updatedRundown);
|
|
||||||
|
|
||||||
rundownRevision++;
|
|
||||||
|
|
||||||
return reorderedEvent;
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function cachedClear() {
|
|
||||||
await DataProvider.clearRundown();
|
|
||||||
runtimeCacheStore.setCached(delayedRundownCacheKey, []);
|
|
||||||
rundownRevision++;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Swaps two events
|
|
||||||
* @param {string} fromEventId
|
|
||||||
* @param {string} toEventId
|
|
||||||
*/
|
|
||||||
export async function cachedSwap(fromEventId: string, toEventId: string) {
|
|
||||||
const fromEventIndex = DataProvider.getIndexOf(fromEventId);
|
|
||||||
const toEventIndex = DataProvider.getIndexOf(toEventId);
|
|
||||||
|
|
||||||
const rundown = DataProvider.getRundown();
|
|
||||||
const rundownToUpdate = swapOntimeEvents(rundown, fromEventIndex, toEventIndex);
|
|
||||||
|
|
||||||
const delayedRundown = getDelayedRundown();
|
|
||||||
const fromCachedEvent = delayedRundown.at(fromEventIndex);
|
|
||||||
const toCachedEvent = delayedRundown.at(toEventIndex);
|
|
||||||
|
|
||||||
if (fromCachedEvent.id !== fromEventId || toCachedEvent.id !== toEventId) {
|
|
||||||
// something went wrong, we invalidate the cache
|
|
||||||
runtimeCacheStore.invalidate(delayedRundownCacheKey);
|
|
||||||
} else {
|
|
||||||
const delayedRundownToUpdate = swapOntimeEvents(delayedRundown, fromEventIndex, toEventIndex);
|
|
||||||
runtimeCacheStore.setCached(delayedRundownCacheKey, delayedRundownToUpdate);
|
|
||||||
}
|
|
||||||
|
|
||||||
await DataProvider.setRundown(rundownToUpdate);
|
|
||||||
|
|
||||||
rundownRevision++;
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function cachedApplyDelay(eventId: string) {
|
|
||||||
// update persisted rundown
|
|
||||||
const rundown: OntimeRundown = DataProvider.getRundown();
|
|
||||||
const persistedRundown = applyDelay(eventId, rundown);
|
|
||||||
|
|
||||||
const delayedRundown = getDelayedRundown();
|
|
||||||
const cachedRundown = applyDelay(eventId, delayedRundown);
|
|
||||||
|
|
||||||
// update
|
|
||||||
runtimeCacheStore.setCached(delayedRundownCacheKey, cachedRundown);
|
|
||||||
await DataProvider.setRundown(persistedRundown);
|
|
||||||
|
|
||||||
rundownRevision++;
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,7 +5,14 @@ import { TimerService } from '../TimerService.js';
|
|||||||
import { logger } from '../../classes/Logger.js';
|
import { logger } from '../../classes/Logger.js';
|
||||||
import { RestorePoint } from '../RestoreService.js';
|
import { RestorePoint } from '../RestoreService.js';
|
||||||
import * as runtimeState from '../../stores/runtimeState.js';
|
import * as runtimeState from '../../stores/runtimeState.js';
|
||||||
import { findNext, findPrevious, getEventAtIndex, getEventWithCue, getEventWithId, getPlayableEvents } from '../rundown-service/RundownService.js';
|
import {
|
||||||
|
findNext,
|
||||||
|
findPrevious,
|
||||||
|
getEventAtIndex,
|
||||||
|
getEventWithCue,
|
||||||
|
getEventWithId,
|
||||||
|
getPlayableEvents,
|
||||||
|
} from '../rundown-service/RundownService.js';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Service manages runtime status of app
|
* Service manages runtime status of app
|
||||||
@@ -142,7 +149,7 @@ class RuntimeService {
|
|||||||
|
|
||||||
const timedEvents = getPlayableEvents();
|
const timedEvents = getPlayableEvents();
|
||||||
const state = runtimeState.getState();
|
const state = runtimeState.getState();
|
||||||
// TODO: return success boolean from runtimeState
|
// TODO: return success boolean from runtimeState, when we work with optimising integrations
|
||||||
runtimeState.load(event, timedEvents);
|
runtimeState.load(event, timedEvents);
|
||||||
const success = event.id === state.eventNow?.id;
|
const success = event.id === state.eventNow?.id;
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
import { MaybeNumber, MaybeString, OntimeEvent, TimerType } from 'ontime-types';
|
import { MaybeNumber, MaybeString, OntimeEvent, TimerType } from 'ontime-types';
|
||||||
import { dayInMs } from 'ontime-utils';
|
import { dayInMs, sortArrayByProperty } from 'ontime-utils';
|
||||||
import { RuntimeState } from '../stores/runtimeState.js';
|
import { RuntimeState } from '../stores/runtimeState.js';
|
||||||
import { sortArrayByProperty } from '../utils/arrayUtils.js';
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* handle events that span over midnight
|
* handle events that span over midnight
|
||||||
|
|||||||
@@ -39,8 +39,8 @@ const env = process.env.NODE_ENV || 'production';
|
|||||||
|
|
||||||
export const isTest = Boolean(process.env.IS_TEST);
|
export const isTest = Boolean(process.env.IS_TEST);
|
||||||
export const environment = isTest ? 'test' : env;
|
export const environment = isTest ? 'test' : env;
|
||||||
export const isProduction = env === ('production' || 'docker') && !isTest;
|
|
||||||
export const isDocker = env === 'docker';
|
export const isDocker = env === 'docker';
|
||||||
|
export const isProduction = isDocker || (env === 'production' && !isTest);
|
||||||
|
|
||||||
// =================================================
|
// =================================================
|
||||||
// resolve path to external
|
// resolve path to external
|
||||||
@@ -71,7 +71,7 @@ export const currentDirectory = dirname(__dirname);
|
|||||||
|
|
||||||
const testDbStartDirectory = isTest ? '../' : getAppDataPath();
|
const testDbStartDirectory = isTest ? '../' : getAppDataPath();
|
||||||
export const externalsStartDirectory = isProduction ? getAppDataPath() : join(currentDirectory, 'external');
|
export const externalsStartDirectory = isProduction ? getAppDataPath() : join(currentDirectory, 'external');
|
||||||
//TODO: we only need one when they are all in the same folder
|
// TODO: we only need one when they are all in the same folder
|
||||||
export const resolveExternalsDirectory = join(isProduction ? getAppDataPath() : currentDirectory, 'external');
|
export const resolveExternalsDirectory = join(isProduction ? getAppDataPath() : currentDirectory, 'external');
|
||||||
|
|
||||||
// project files
|
// project files
|
||||||
@@ -89,13 +89,14 @@ const getLastLoadedProject = () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const lastLoadedProject = getLastLoadedProject();
|
const lastLoadedProject = isTest ? 'db.json' : getLastLoadedProject();
|
||||||
|
|
||||||
// path to public db
|
// path to public db
|
||||||
export const resolveDbDirectory = join(testDbStartDirectory, isTest ? config.database.testdb : 'uploads');
|
export const resolveDbDirectory = join(testDbStartDirectory, isTest ? `../${config.database.testdb}` : 'uploads');
|
||||||
export const resolveDbPath = join(resolveDbDirectory, lastLoadedProject ? lastLoadedProject : config.database.filename);
|
export const resolveDbPath = join(resolveDbDirectory, lastLoadedProject ? lastLoadedProject : config.database.filename);
|
||||||
|
|
||||||
export const pathToStartDb = isTest
|
export const pathToStartDb = isTest
|
||||||
? join(currentDirectory, '../', config.database.testdb, config.database.filename)
|
? join(currentDirectory, '..', config.database.testdb, config.database.filename)
|
||||||
: join(currentDirectory, '/preloaded-db/', config.database.filename);
|
: join(currentDirectory, '/preloaded-db/', config.database.filename);
|
||||||
|
|
||||||
// TODO: move all static files to the external directory
|
// TODO: move all static files to the external directory
|
||||||
@@ -108,7 +109,7 @@ export const pathToStartStyles = join(currentDirectory, '/external/styles/', con
|
|||||||
// path to public demo
|
// path to public demo
|
||||||
export const resolveDemoDirectory = join(
|
export const resolveDemoDirectory = join(
|
||||||
externalsStartDirectory,
|
externalsStartDirectory,
|
||||||
isProduction ? '/external/' : '', //move to external folde in production
|
isProduction ? '/external/' : '', // move to external folder in production
|
||||||
config.demo.directory,
|
config.demo.directory,
|
||||||
);
|
);
|
||||||
export const resolveDemoPath = config.demo.filename.map((file) => {
|
export const resolveDemoPath = config.demo.filename.map((file) => {
|
||||||
|
|||||||
@@ -1,67 +0,0 @@
|
|||||||
import { runtimeCacheStore } from '../cachingStore.js';
|
|
||||||
|
|
||||||
describe('cachingStore()', () => {
|
|
||||||
beforeEach(() => {
|
|
||||||
runtimeCacheStore.clear(); // Clear the cache before each test
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should check if an item is cached', () => {
|
|
||||||
// Add an item to the cache
|
|
||||||
runtimeCacheStore.setCached('key', 'value');
|
|
||||||
|
|
||||||
// Check if the item is cached
|
|
||||||
expect(runtimeCacheStore.checkCached('key')).toBe(true);
|
|
||||||
expect(runtimeCacheStore.checkCached('non-existent-key')).toBe(false);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should get an item from the cache', () => {
|
|
||||||
// Add an item to the cache
|
|
||||||
runtimeCacheStore.setCached('key', 'value');
|
|
||||||
|
|
||||||
// Get the item from the cache
|
|
||||||
const result = runtimeCacheStore.getCached('key', () => 'default-value');
|
|
||||||
|
|
||||||
// Check the returned value
|
|
||||||
expect(result).toBe('value');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should retrieve default value when item is not cached', () => {
|
|
||||||
// Get an item that is not in the cache
|
|
||||||
const result = runtimeCacheStore.getCached('non-existent-key', () => 'default-value');
|
|
||||||
|
|
||||||
// Check the returned value
|
|
||||||
expect(result).toBe('default-value');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should set an item in the cache', () => {
|
|
||||||
// Set an item in the cache
|
|
||||||
runtimeCacheStore.setCached('key', 'value');
|
|
||||||
|
|
||||||
// Check if the item is cached
|
|
||||||
expect(runtimeCacheStore.checkCached('key')).toBe(true);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should invalidate an item in the cache', () => {
|
|
||||||
// Add an item to the cache
|
|
||||||
runtimeCacheStore.setCached('key', 'value');
|
|
||||||
|
|
||||||
// Invalidate the item
|
|
||||||
runtimeCacheStore.invalidate('key');
|
|
||||||
|
|
||||||
// Check if the item is no longer cached
|
|
||||||
expect(runtimeCacheStore.checkCached('key')).toBe(false);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should clear the cache', () => {
|
|
||||||
// Add items to the cache
|
|
||||||
runtimeCacheStore.setCached('key1', 'value1');
|
|
||||||
runtimeCacheStore.setCached('key2', 'value2');
|
|
||||||
|
|
||||||
// Clear the cache
|
|
||||||
runtimeCacheStore.clear();
|
|
||||||
|
|
||||||
// Check if the cache is empty
|
|
||||||
expect(runtimeCacheStore.checkCached('key1')).toBe(false);
|
|
||||||
expect(runtimeCacheStore.checkCached('key2')).toBe(false);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,47 +0,0 @@
|
|||||||
interface CacheData {
|
|
||||||
data: unknown;
|
|
||||||
}
|
|
||||||
|
|
||||||
const runtimeCache: Map<string, CacheData> = new Map();
|
|
||||||
|
|
||||||
export function checkCached(key: string): boolean {
|
|
||||||
return runtimeCache.has(key);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getCached<T>(key: string, callback: () => T): T {
|
|
||||||
if (!runtimeCache.has(key)) {
|
|
||||||
try {
|
|
||||||
const data = callback();
|
|
||||||
runtimeCache.set(key, { data });
|
|
||||||
} catch (error) {
|
|
||||||
console.error(`Failed retrieving data from callback: ${error}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return runtimeCache.get(key).data as T;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function setCached<T>(key: string, value: T): T {
|
|
||||||
runtimeCache.set(key, { data: value });
|
|
||||||
return value;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function invalidate(key: string) {
|
|
||||||
runtimeCache.delete(key);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function clear() {
|
|
||||||
runtimeCache.clear();
|
|
||||||
}
|
|
||||||
|
|
||||||
function createCacheStore() {
|
|
||||||
return {
|
|
||||||
checkCached,
|
|
||||||
getCached,
|
|
||||||
setCached,
|
|
||||||
invalidate,
|
|
||||||
clear,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export const runtimeCacheStore = createCacheStore();
|
|
||||||
@@ -371,7 +371,8 @@ export function createPatch(originalEvent: OntimeEvent, patchEvent: Partial<Onti
|
|||||||
user8: makeString(patchEvent.user8, originalEvent.user8),
|
user8: makeString(patchEvent.user8, originalEvent.user8),
|
||||||
user9: makeString(patchEvent.user9, originalEvent.user9),
|
user9: makeString(patchEvent.user9, originalEvent.user9),
|
||||||
colour: makeString(patchEvent.colour, originalEvent.colour),
|
colour: makeString(patchEvent.colour, originalEvent.colour),
|
||||||
cue: makeString(patchEvent.cue, originalEvent.cue),
|
// short circuit empty string
|
||||||
|
cue: makeString(patchEvent.cue ?? null, originalEvent.cue),
|
||||||
revision: originalEvent.revision,
|
revision: originalEvent.revision,
|
||||||
timeWarning: patchEvent.timeWarning,
|
timeWarning: patchEvent.timeWarning,
|
||||||
timeDanger: patchEvent.timeDanger,
|
timeDanger: patchEvent.timeDanger,
|
||||||
|
|||||||
+383
-192
@@ -1,157 +1,16 @@
|
|||||||
{
|
{
|
||||||
"rundown": [
|
"rundown": [
|
||||||
{
|
{
|
||||||
"title": "Welcome to Ontime",
|
"title": "Albania",
|
||||||
"subtitle": "Subtitles are useful",
|
"subtitle": "Sekret",
|
||||||
"presenter": "cpvalente",
|
"presenter": "Ronela Hajati",
|
||||||
"note": "Maybe a running note for the operator?",
|
"note": "SF1.01",
|
||||||
"timeStart": 28800000,
|
|
||||||
"timeEnd": 30600000,
|
|
||||||
"endAction": "none",
|
|
||||||
"timerType": "count-down",
|
|
||||||
"duration": 1800000,
|
|
||||||
"isPublic": false,
|
|
||||||
"skip": false,
|
|
||||||
"colour": "",
|
|
||||||
"user0": "",
|
|
||||||
"user1": "",
|
|
||||||
"user2": "",
|
|
||||||
"user3": "",
|
|
||||||
"user4": "",
|
|
||||||
"user5": "",
|
|
||||||
"user6": "",
|
|
||||||
"user7": "",
|
|
||||||
"user8": "",
|
|
||||||
"user9": "",
|
|
||||||
"type": "event",
|
|
||||||
"revision": 0,
|
|
||||||
"id": "5946"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"title": "This is your event list",
|
|
||||||
"subtitle": "",
|
|
||||||
"presenter": "cpvalente",
|
|
||||||
"note": "",
|
|
||||||
"timeStart": 30600000,
|
|
||||||
"timeEnd": 34200000,
|
|
||||||
"endAction": "none",
|
|
||||||
"timerType": "count-down",
|
|
||||||
"duration": 3600000,
|
|
||||||
"isPublic": false,
|
|
||||||
"skip": false,
|
|
||||||
"colour": "",
|
|
||||||
"user0": "",
|
|
||||||
"user1": "",
|
|
||||||
"user2": "",
|
|
||||||
"user3": "",
|
|
||||||
"user4": "",
|
|
||||||
"user5": "",
|
|
||||||
"user6": "",
|
|
||||||
"user7": "",
|
|
||||||
"user8": "",
|
|
||||||
"user9": "",
|
|
||||||
"type": "event",
|
|
||||||
"revision": 0,
|
|
||||||
"id": "c2e7"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"title": "Events run in the list order",
|
|
||||||
"subtitle": "",
|
|
||||||
"presenter": "",
|
|
||||||
"note": "",
|
|
||||||
"timeStart": 34200000,
|
|
||||||
"timeEnd": 34800000,
|
|
||||||
"endAction": "none",
|
|
||||||
"timerType": "count-down",
|
|
||||||
"duration": 600000,
|
|
||||||
"isPublic": false,
|
|
||||||
"skip": false,
|
|
||||||
"colour": "",
|
|
||||||
"user0": "",
|
|
||||||
"user1": "",
|
|
||||||
"user2": "",
|
|
||||||
"user3": "",
|
|
||||||
"user4": "",
|
|
||||||
"user5": "",
|
|
||||||
"user6": "",
|
|
||||||
"user7": "",
|
|
||||||
"user8": "",
|
|
||||||
"user9": "",
|
|
||||||
"type": "event",
|
|
||||||
"revision": 0,
|
|
||||||
"id": "cc0f"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"title": "Unless recalled by the OSC address",
|
|
||||||
"subtitle": "",
|
|
||||||
"presenter": "",
|
|
||||||
"note": "In green, below",
|
|
||||||
"timeStart": 34800000,
|
|
||||||
"timeEnd": 35400000,
|
|
||||||
"endAction": "none",
|
|
||||||
"timerType": "count-down",
|
|
||||||
"duration": 600000,
|
|
||||||
"isPublic": false,
|
|
||||||
"skip": false,
|
|
||||||
"colour": "",
|
|
||||||
"user0": "",
|
|
||||||
"user1": "",
|
|
||||||
"user2": "",
|
|
||||||
"user3": "",
|
|
||||||
"user4": "",
|
|
||||||
"user5": "",
|
|
||||||
"user6": "",
|
|
||||||
"user7": "",
|
|
||||||
"user8": "",
|
|
||||||
"user9": "",
|
|
||||||
"type": "event",
|
|
||||||
"revision": 0,
|
|
||||||
"id": "8ee5"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"title": "Use simpler times to create a timer",
|
|
||||||
"subtitle": "",
|
|
||||||
"presenter": "",
|
|
||||||
"note": "",
|
|
||||||
"timeStart": 0,
|
|
||||||
"timeEnd": 600000,
|
|
||||||
"endAction": "none",
|
|
||||||
"timerType": "count-down",
|
|
||||||
"duration": 600000,
|
|
||||||
"isPublic": false,
|
|
||||||
"skip": false,
|
|
||||||
"colour": "",
|
|
||||||
"user0": "",
|
|
||||||
"user1": "",
|
|
||||||
"user2": "",
|
|
||||||
"user3": "",
|
|
||||||
"user4": "",
|
|
||||||
"user5": "",
|
|
||||||
"user6": "",
|
|
||||||
"user7": "",
|
|
||||||
"user8": "",
|
|
||||||
"user9": "",
|
|
||||||
"type": "event",
|
|
||||||
"revision": 0,
|
|
||||||
"id": "8222"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"duration": 900000,
|
|
||||||
"type": "delay",
|
|
||||||
"revision": 0,
|
|
||||||
"id": "a386"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"title": "Add delay blocks to affect all events",
|
|
||||||
"subtitle": "",
|
|
||||||
"presenter": "",
|
|
||||||
"note": "* Until a block is found",
|
|
||||||
"timeStart": 35400000,
|
|
||||||
"timeEnd": 36600000,
|
|
||||||
"endAction": "none",
|
"endAction": "none",
|
||||||
"timerType": "count-down",
|
"timerType": "count-down",
|
||||||
|
"timeStart": 36000000,
|
||||||
|
"timeEnd": 37200000,
|
||||||
"duration": 1200000,
|
"duration": 1200000,
|
||||||
"isPublic": false,
|
"isPublic": true,
|
||||||
"skip": false,
|
"skip": false,
|
||||||
"colour": "",
|
"colour": "",
|
||||||
"user0": "",
|
"user0": "",
|
||||||
@@ -166,19 +25,104 @@
|
|||||||
"user9": "",
|
"user9": "",
|
||||||
"type": "event",
|
"type": "event",
|
||||||
"revision": 0,
|
"revision": 0,
|
||||||
"id": "6dce"
|
"id": "32d31",
|
||||||
|
"cue": "SF1.01"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"title": "Add and remove events with [+] and [-]",
|
"title": "Latvia",
|
||||||
"subtitle": "",
|
"subtitle": "Eat Your Salad",
|
||||||
"presenter": "",
|
"presenter": "Citi Zeni",
|
||||||
"note": "Orange and red buttons on the right",
|
"note": "SF1.02",
|
||||||
"timeStart": 36600000,
|
"endAction": "none",
|
||||||
|
"timerType": "count-down",
|
||||||
|
"timeStart": 37500000,
|
||||||
|
"timeEnd": 38700000,
|
||||||
|
"duration": 1200000,
|
||||||
|
"isPublic": true,
|
||||||
|
"skip": false,
|
||||||
|
"colour": "",
|
||||||
|
"user0": "",
|
||||||
|
"user1": "",
|
||||||
|
"user2": "",
|
||||||
|
"user3": "",
|
||||||
|
"user4": "",
|
||||||
|
"user5": "",
|
||||||
|
"user6": "",
|
||||||
|
"user7": "",
|
||||||
|
"user8": "",
|
||||||
|
"user9": "",
|
||||||
|
"type": "event",
|
||||||
|
"revision": 0,
|
||||||
|
"id": "21cd2",
|
||||||
|
"cue": "SF1.02"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"title": "Lithuania",
|
||||||
|
"subtitle": "Sentimentai",
|
||||||
|
"presenter": "Monika Liu",
|
||||||
|
"note": "SF1.03",
|
||||||
|
"endAction": "none",
|
||||||
|
"timerType": "count-down",
|
||||||
|
"timeStart": 39000000,
|
||||||
|
"timeEnd": 40200000,
|
||||||
|
"duration": 1200000,
|
||||||
|
"isPublic": true,
|
||||||
|
"skip": false,
|
||||||
|
"colour": "",
|
||||||
|
"user0": "",
|
||||||
|
"user1": "",
|
||||||
|
"user2": "",
|
||||||
|
"user3": "",
|
||||||
|
"user4": "",
|
||||||
|
"user5": "",
|
||||||
|
"user6": "",
|
||||||
|
"user7": "",
|
||||||
|
"user8": "",
|
||||||
|
"user9": "",
|
||||||
|
"type": "event",
|
||||||
|
"revision": 0,
|
||||||
|
"id": "0b371",
|
||||||
|
"cue": "SF1.03"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"title": "Switzerland",
|
||||||
|
"subtitle": "Boys Do Cry",
|
||||||
|
"presenter": "Marius Bear",
|
||||||
|
"note": "SF1.04",
|
||||||
|
"endAction": "none",
|
||||||
|
"timerType": "count-down",
|
||||||
|
"timeStart": 40500000,
|
||||||
|
"timeEnd": 41700000,
|
||||||
|
"duration": 1200000,
|
||||||
|
"isPublic": true,
|
||||||
|
"skip": false,
|
||||||
|
"colour": "",
|
||||||
|
"user0": "",
|
||||||
|
"user1": "",
|
||||||
|
"user2": "",
|
||||||
|
"user3": "",
|
||||||
|
"user4": "",
|
||||||
|
"user5": "",
|
||||||
|
"user6": "",
|
||||||
|
"user7": "",
|
||||||
|
"user8": "",
|
||||||
|
"user9": "",
|
||||||
|
"type": "event",
|
||||||
|
"revision": 0,
|
||||||
|
"id": "3cd28",
|
||||||
|
"cue": "SF1.04"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"title": "Slovenia",
|
||||||
|
"subtitle": "Disko",
|
||||||
|
"presenter": "LPS",
|
||||||
|
"note": "SF1.05",
|
||||||
|
"endAction": "none",
|
||||||
|
"timerType": "count-down",
|
||||||
|
"timeStart": 42000000,
|
||||||
"timeEnd": 43200000,
|
"timeEnd": 43200000,
|
||||||
"endAction": "none",
|
"duration": 1200000,
|
||||||
"timerType": "count-down",
|
"isPublic": true,
|
||||||
"duration": 6600000,
|
|
||||||
"isPublic": false,
|
|
||||||
"skip": false,
|
"skip": false,
|
||||||
"colour": "",
|
"colour": "",
|
||||||
"user0": "",
|
"user0": "",
|
||||||
@@ -193,23 +137,25 @@
|
|||||||
"user9": "",
|
"user9": "",
|
||||||
"type": "event",
|
"type": "event",
|
||||||
"revision": 0,
|
"revision": 0,
|
||||||
"id": "2651"
|
"id": "e457f",
|
||||||
|
"cue": "SF1.05"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
"title": "Lunch break",
|
||||||
"type": "block",
|
"type": "block",
|
||||||
"id": "e6a1"
|
"id": "01e85"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"title": "And control whether they are public",
|
"title": "Ukraine",
|
||||||
"subtitle": "",
|
"subtitle": "Stefania",
|
||||||
"presenter": "",
|
"presenter": "Kalush Orchestra",
|
||||||
"note": "Blue button on the right",
|
"note": "SF1.06",
|
||||||
"timeStart": 46800000,
|
|
||||||
"timeEnd": 57600000,
|
|
||||||
"endAction": "none",
|
"endAction": "none",
|
||||||
"timerType": "count-down",
|
"timerType": "count-down",
|
||||||
"duration": 10800000,
|
"timeStart": 47100000,
|
||||||
"isPublic": false,
|
"timeEnd": 48300000,
|
||||||
|
"duration": 1200000,
|
||||||
|
"isPublic": true,
|
||||||
"skip": false,
|
"skip": false,
|
||||||
"colour": "",
|
"colour": "",
|
||||||
"user0": "",
|
"user0": "",
|
||||||
@@ -224,16 +170,246 @@
|
|||||||
"user9": "",
|
"user9": "",
|
||||||
"type": "event",
|
"type": "event",
|
||||||
"revision": 0,
|
"revision": 0,
|
||||||
"id": "1358"
|
"id": "1c420",
|
||||||
|
"cue": "SF1.06"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"title": "Bulgaria",
|
||||||
|
"subtitle": "Intention",
|
||||||
|
"presenter": "Intelligent Music Project",
|
||||||
|
"note": "SF1.07",
|
||||||
|
"endAction": "none",
|
||||||
|
"timerType": "count-down",
|
||||||
|
"timeStart": 48600000,
|
||||||
|
"timeEnd": 49800000,
|
||||||
|
"duration": 1200000,
|
||||||
|
"isPublic": true,
|
||||||
|
"skip": false,
|
||||||
|
"colour": "",
|
||||||
|
"user0": "",
|
||||||
|
"user1": "",
|
||||||
|
"user2": "",
|
||||||
|
"user3": "",
|
||||||
|
"user4": "",
|
||||||
|
"user5": "",
|
||||||
|
"user6": "",
|
||||||
|
"user7": "",
|
||||||
|
"user8": "",
|
||||||
|
"user9": "",
|
||||||
|
"type": "event",
|
||||||
|
"revision": 0,
|
||||||
|
"id": "b7737",
|
||||||
|
"cue": "SF1.07"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"title": "Netherlands",
|
||||||
|
"subtitle": "De Diepte",
|
||||||
|
"presenter": "S10",
|
||||||
|
"note": "SF1.08",
|
||||||
|
"endAction": "none",
|
||||||
|
"timerType": "count-down",
|
||||||
|
"timeStart": 50100000,
|
||||||
|
"timeEnd": 51300000,
|
||||||
|
"duration": 1200000,
|
||||||
|
"isPublic": true,
|
||||||
|
"skip": false,
|
||||||
|
"colour": "",
|
||||||
|
"user0": "",
|
||||||
|
"user1": "",
|
||||||
|
"user2": "",
|
||||||
|
"user3": "",
|
||||||
|
"user4": "",
|
||||||
|
"user5": "",
|
||||||
|
"user6": "",
|
||||||
|
"user7": "",
|
||||||
|
"user8": "",
|
||||||
|
"user9": "",
|
||||||
|
"type": "event",
|
||||||
|
"revision": 0,
|
||||||
|
"id": "d3a80",
|
||||||
|
"cue": "SF1.08"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"title": "Moldova",
|
||||||
|
"subtitle": "Trenuletul",
|
||||||
|
"presenter": "Zdob si Zdub",
|
||||||
|
"note": "SF1.09",
|
||||||
|
"endAction": "none",
|
||||||
|
"timerType": "count-down",
|
||||||
|
"timeStart": 51600000,
|
||||||
|
"timeEnd": 52800000,
|
||||||
|
"duration": 1200000,
|
||||||
|
"isPublic": true,
|
||||||
|
"skip": false,
|
||||||
|
"colour": "",
|
||||||
|
"user0": "",
|
||||||
|
"user1": "",
|
||||||
|
"user2": "",
|
||||||
|
"user3": "",
|
||||||
|
"user4": "",
|
||||||
|
"user5": "",
|
||||||
|
"user6": "",
|
||||||
|
"user7": "",
|
||||||
|
"user8": "",
|
||||||
|
"user9": "",
|
||||||
|
"type": "event",
|
||||||
|
"revision": 0,
|
||||||
|
"id": "8276c",
|
||||||
|
"cue": "SF1.09"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"title": "Portugal",
|
||||||
|
"subtitle": "Saudade Saudade",
|
||||||
|
"presenter": "Maro",
|
||||||
|
"note": "SF1.10",
|
||||||
|
"endAction": "none",
|
||||||
|
"timerType": "count-down",
|
||||||
|
"timeStart": 53100000,
|
||||||
|
"timeEnd": 54300000,
|
||||||
|
"duration": 1200000,
|
||||||
|
"isPublic": true,
|
||||||
|
"skip": false,
|
||||||
|
"colour": "",
|
||||||
|
"user0": "",
|
||||||
|
"user1": "",
|
||||||
|
"user2": "",
|
||||||
|
"user3": "",
|
||||||
|
"user4": "",
|
||||||
|
"user5": "",
|
||||||
|
"user6": "",
|
||||||
|
"user7": "",
|
||||||
|
"user8": "",
|
||||||
|
"user9": "",
|
||||||
|
"type": "event",
|
||||||
|
"revision": 0,
|
||||||
|
"id": "2340b",
|
||||||
|
"cue": "SF1.10"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"title": "Afternoon break",
|
||||||
|
"type": "block",
|
||||||
|
"id": "cb90b"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"title": "Croatia",
|
||||||
|
"subtitle": "Guilty Pleasure",
|
||||||
|
"presenter": "Mia Dimsic",
|
||||||
|
"note": "SF1.11",
|
||||||
|
"endAction": "none",
|
||||||
|
"timerType": "count-down",
|
||||||
|
"timeStart": 56100000,
|
||||||
|
"timeEnd": 57300000,
|
||||||
|
"duration": 1200000,
|
||||||
|
"isPublic": true,
|
||||||
|
"skip": false,
|
||||||
|
"colour": "",
|
||||||
|
"user0": "",
|
||||||
|
"user1": "",
|
||||||
|
"user2": "",
|
||||||
|
"user3": "",
|
||||||
|
"user4": "",
|
||||||
|
"user5": "",
|
||||||
|
"user6": "",
|
||||||
|
"user7": "",
|
||||||
|
"user8": "",
|
||||||
|
"user9": "",
|
||||||
|
"type": "event",
|
||||||
|
"revision": 0,
|
||||||
|
"id": "503c4",
|
||||||
|
"cue": "SF1.11"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"title": "Denmark",
|
||||||
|
"subtitle": "The Show",
|
||||||
|
"presenter": "Reddi",
|
||||||
|
"note": "SF1.12",
|
||||||
|
"endAction": "none",
|
||||||
|
"timerType": "count-down",
|
||||||
|
"timeStart": 57600000,
|
||||||
|
"timeEnd": 58800000,
|
||||||
|
"duration": 1200000,
|
||||||
|
"isPublic": true,
|
||||||
|
"skip": false,
|
||||||
|
"colour": "",
|
||||||
|
"user0": "",
|
||||||
|
"user1": "",
|
||||||
|
"user2": "",
|
||||||
|
"user3": "",
|
||||||
|
"user4": "",
|
||||||
|
"user5": "",
|
||||||
|
"user6": "",
|
||||||
|
"user7": "",
|
||||||
|
"user8": "",
|
||||||
|
"user9": "",
|
||||||
|
"type": "event",
|
||||||
|
"revision": 0,
|
||||||
|
"id": "5e965",
|
||||||
|
"cue": "SF1.12"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"title": "Austria",
|
||||||
|
"subtitle": "Halo",
|
||||||
|
"presenter": "LUM!X & Pia Maria",
|
||||||
|
"note": "SF1.13",
|
||||||
|
"endAction": "none",
|
||||||
|
"timerType": "count-down",
|
||||||
|
"timeStart": 59100000,
|
||||||
|
"timeEnd": 60300000,
|
||||||
|
"duration": 1200000,
|
||||||
|
"isPublic": true,
|
||||||
|
"skip": false,
|
||||||
|
"colour": "",
|
||||||
|
"user0": "",
|
||||||
|
"user1": "",
|
||||||
|
"user2": "",
|
||||||
|
"user3": "",
|
||||||
|
"user4": "",
|
||||||
|
"user5": "",
|
||||||
|
"user6": "",
|
||||||
|
"user7": "",
|
||||||
|
"user8": "",
|
||||||
|
"user9": "",
|
||||||
|
"type": "event",
|
||||||
|
"revision": 0,
|
||||||
|
"id": "bab4a",
|
||||||
|
"cue": "SF1.13"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"title": "Greece",
|
||||||
|
"subtitle": "Die Together",
|
||||||
|
"presenter": "Amanda Tenfjord",
|
||||||
|
"note": "SF1.14",
|
||||||
|
"endAction": "none",
|
||||||
|
"timerType": "count-down",
|
||||||
|
"timeStart": 60600000,
|
||||||
|
"timeEnd": 61800000,
|
||||||
|
"duration": 1200000,
|
||||||
|
"isPublic": true,
|
||||||
|
"skip": false,
|
||||||
|
"colour": "",
|
||||||
|
"user0": "",
|
||||||
|
"user1": "",
|
||||||
|
"user2": "",
|
||||||
|
"user3": "",
|
||||||
|
"user4": "",
|
||||||
|
"user5": "",
|
||||||
|
"user6": "",
|
||||||
|
"user7": "",
|
||||||
|
"user8": "",
|
||||||
|
"user9": "",
|
||||||
|
"type": "event",
|
||||||
|
"revision": 0,
|
||||||
|
"id": "d3eb1",
|
||||||
|
"cue": "SF1.14"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"project": {
|
"project": {
|
||||||
"title": "All about Carlos demo event",
|
"title": "Eurovision Song Contest",
|
||||||
"description": "Demo event for Ontime",
|
"description": "Turin 2022",
|
||||||
"publicUrl": "www.getontime.no",
|
"publicUrl": "www.getontime.no",
|
||||||
"publicInfo": "WiFi: demoproject \nPassword: ontimeproject",
|
"publicInfo": "Rehearsal Schedule - Turin 2022",
|
||||||
"backstageUrl": "www.getontime.no",
|
"backstageUrl": "www.github.com/cpvalente/ontime",
|
||||||
"backstageInfo": "WiFi: demobackstage\nPassword: ontimeproject"
|
"backstageInfo": "Rehearsal Schedule - Turin 2022\nAll performers to wear full costumes for 1st rehearsal"
|
||||||
},
|
},
|
||||||
"settings": {
|
"settings": {
|
||||||
"app": "ontime",
|
"app": "ontime",
|
||||||
@@ -253,23 +429,6 @@
|
|||||||
"dangerThreshold": 60000,
|
"dangerThreshold": 60000,
|
||||||
"endMessage": ""
|
"endMessage": ""
|
||||||
},
|
},
|
||||||
"osc": {
|
|
||||||
"port": 8888,
|
|
||||||
"portOut": 9999,
|
|
||||||
"targetIP": "127.0.0.1",
|
|
||||||
"enabled": true
|
|
||||||
},
|
|
||||||
"http": {
|
|
||||||
"enabledOut": false,
|
|
||||||
"subscriptions": {
|
|
||||||
"onLoad": [],
|
|
||||||
"onStart": [],
|
|
||||||
"onPause": [],
|
|
||||||
"onStop": [],
|
|
||||||
"onUpdate": [],
|
|
||||||
"onFinish": []
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"aliases": [
|
"aliases": [
|
||||||
{
|
{
|
||||||
"enabled": true,
|
"enabled": true,
|
||||||
@@ -288,5 +447,37 @@
|
|||||||
"user7": "user7",
|
"user7": "user7",
|
||||||
"user8": "user8",
|
"user8": "user8",
|
||||||
"user9": "user9"
|
"user9": "user9"
|
||||||
|
},
|
||||||
|
"osc": {
|
||||||
|
"portIn": 8888,
|
||||||
|
"portOut": 9999,
|
||||||
|
"targetIP": "127.0.0.1",
|
||||||
|
"enabledIn": true,
|
||||||
|
"enabledOut": true,
|
||||||
|
"subscriptions": {
|
||||||
|
"onLoad": [],
|
||||||
|
"onStart": [],
|
||||||
|
"onPause": [],
|
||||||
|
"onStop": [],
|
||||||
|
"onUpdate": [
|
||||||
|
{
|
||||||
|
"id": "10eea",
|
||||||
|
"enabled": true,
|
||||||
|
"message": "/ontime/update/{{timer.current}}"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"onFinish": []
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"http": {
|
||||||
|
"enabledOut": true,
|
||||||
|
"subscriptions": {
|
||||||
|
"onLoad": [],
|
||||||
|
"onStart": [],
|
||||||
|
"onPause": [],
|
||||||
|
"onStop": [],
|
||||||
|
"onUpdate": [],
|
||||||
|
"onFinish": []
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,483 +0,0 @@
|
|||||||
{
|
|
||||||
"rundown": [
|
|
||||||
{
|
|
||||||
"title": "Albania",
|
|
||||||
"subtitle": "Sekret",
|
|
||||||
"presenter": "Ronela Hajati",
|
|
||||||
"note": "SF1.01",
|
|
||||||
"endAction": "none",
|
|
||||||
"timerType": "count-down",
|
|
||||||
"timeStart": 36000000,
|
|
||||||
"timeEnd": 37200000,
|
|
||||||
"duration": 1200000,
|
|
||||||
"isPublic": true,
|
|
||||||
"skip": false,
|
|
||||||
"colour": "",
|
|
||||||
"user0": "",
|
|
||||||
"user1": "",
|
|
||||||
"user2": "",
|
|
||||||
"user3": "",
|
|
||||||
"user4": "",
|
|
||||||
"user5": "",
|
|
||||||
"user6": "",
|
|
||||||
"user7": "",
|
|
||||||
"user8": "",
|
|
||||||
"user9": "",
|
|
||||||
"type": "event",
|
|
||||||
"revision": 0,
|
|
||||||
"id": "32d31",
|
|
||||||
"cue": "SF1.01"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"title": "Latvia",
|
|
||||||
"subtitle": "Eat Your Salad",
|
|
||||||
"presenter": "Citi Zeni",
|
|
||||||
"note": "SF1.02",
|
|
||||||
"endAction": "none",
|
|
||||||
"timerType": "count-down",
|
|
||||||
"timeStart": 37500000,
|
|
||||||
"timeEnd": 38700000,
|
|
||||||
"duration": 1200000,
|
|
||||||
"isPublic": true,
|
|
||||||
"skip": false,
|
|
||||||
"colour": "",
|
|
||||||
"user0": "",
|
|
||||||
"user1": "",
|
|
||||||
"user2": "",
|
|
||||||
"user3": "",
|
|
||||||
"user4": "",
|
|
||||||
"user5": "",
|
|
||||||
"user6": "",
|
|
||||||
"user7": "",
|
|
||||||
"user8": "",
|
|
||||||
"user9": "",
|
|
||||||
"type": "event",
|
|
||||||
"revision": 0,
|
|
||||||
"id": "21cd2",
|
|
||||||
"cue": "SF1.02"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"title": "Lithuania",
|
|
||||||
"subtitle": "Sentimentai",
|
|
||||||
"presenter": "Monika Liu",
|
|
||||||
"note": "SF1.03",
|
|
||||||
"endAction": "none",
|
|
||||||
"timerType": "count-down",
|
|
||||||
"timeStart": 39000000,
|
|
||||||
"timeEnd": 40200000,
|
|
||||||
"duration": 1200000,
|
|
||||||
"isPublic": true,
|
|
||||||
"skip": false,
|
|
||||||
"colour": "",
|
|
||||||
"user0": "",
|
|
||||||
"user1": "",
|
|
||||||
"user2": "",
|
|
||||||
"user3": "",
|
|
||||||
"user4": "",
|
|
||||||
"user5": "",
|
|
||||||
"user6": "",
|
|
||||||
"user7": "",
|
|
||||||
"user8": "",
|
|
||||||
"user9": "",
|
|
||||||
"type": "event",
|
|
||||||
"revision": 0,
|
|
||||||
"id": "0b371",
|
|
||||||
"cue": "SF1.03"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"title": "Switzerland",
|
|
||||||
"subtitle": "Boys Do Cry",
|
|
||||||
"presenter": "Marius Bear",
|
|
||||||
"note": "SF1.04",
|
|
||||||
"endAction": "none",
|
|
||||||
"timerType": "count-down",
|
|
||||||
"timeStart": 40500000,
|
|
||||||
"timeEnd": 41700000,
|
|
||||||
"duration": 1200000,
|
|
||||||
"isPublic": true,
|
|
||||||
"skip": false,
|
|
||||||
"colour": "",
|
|
||||||
"user0": "",
|
|
||||||
"user1": "",
|
|
||||||
"user2": "",
|
|
||||||
"user3": "",
|
|
||||||
"user4": "",
|
|
||||||
"user5": "",
|
|
||||||
"user6": "",
|
|
||||||
"user7": "",
|
|
||||||
"user8": "",
|
|
||||||
"user9": "",
|
|
||||||
"type": "event",
|
|
||||||
"revision": 0,
|
|
||||||
"id": "3cd28",
|
|
||||||
"cue": "SF1.04"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"title": "Slovenia",
|
|
||||||
"subtitle": "Disko",
|
|
||||||
"presenter": "LPS",
|
|
||||||
"note": "SF1.05",
|
|
||||||
"endAction": "none",
|
|
||||||
"timerType": "count-down",
|
|
||||||
"timeStart": 42000000,
|
|
||||||
"timeEnd": 43200000,
|
|
||||||
"duration": 1200000,
|
|
||||||
"isPublic": true,
|
|
||||||
"skip": false,
|
|
||||||
"colour": "",
|
|
||||||
"user0": "",
|
|
||||||
"user1": "",
|
|
||||||
"user2": "",
|
|
||||||
"user3": "",
|
|
||||||
"user4": "",
|
|
||||||
"user5": "",
|
|
||||||
"user6": "",
|
|
||||||
"user7": "",
|
|
||||||
"user8": "",
|
|
||||||
"user9": "",
|
|
||||||
"type": "event",
|
|
||||||
"revision": 0,
|
|
||||||
"id": "e457f",
|
|
||||||
"cue": "SF1.05"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"title": "Lunch break",
|
|
||||||
"type": "block",
|
|
||||||
"id": "01e85"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"title": "Ukraine",
|
|
||||||
"subtitle": "Stefania",
|
|
||||||
"presenter": "Kalush Orchestra",
|
|
||||||
"note": "SF1.06",
|
|
||||||
"endAction": "none",
|
|
||||||
"timerType": "count-down",
|
|
||||||
"timeStart": 47100000,
|
|
||||||
"timeEnd": 48300000,
|
|
||||||
"duration": 1200000,
|
|
||||||
"isPublic": true,
|
|
||||||
"skip": false,
|
|
||||||
"colour": "",
|
|
||||||
"user0": "",
|
|
||||||
"user1": "",
|
|
||||||
"user2": "",
|
|
||||||
"user3": "",
|
|
||||||
"user4": "",
|
|
||||||
"user5": "",
|
|
||||||
"user6": "",
|
|
||||||
"user7": "",
|
|
||||||
"user8": "",
|
|
||||||
"user9": "",
|
|
||||||
"type": "event",
|
|
||||||
"revision": 0,
|
|
||||||
"id": "1c420",
|
|
||||||
"cue": "SF1.06"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"title": "Bulgaria",
|
|
||||||
"subtitle": "Intention",
|
|
||||||
"presenter": "Intelligent Music Project",
|
|
||||||
"note": "SF1.07",
|
|
||||||
"endAction": "none",
|
|
||||||
"timerType": "count-down",
|
|
||||||
"timeStart": 48600000,
|
|
||||||
"timeEnd": 49800000,
|
|
||||||
"duration": 1200000,
|
|
||||||
"isPublic": true,
|
|
||||||
"skip": false,
|
|
||||||
"colour": "",
|
|
||||||
"user0": "",
|
|
||||||
"user1": "",
|
|
||||||
"user2": "",
|
|
||||||
"user3": "",
|
|
||||||
"user4": "",
|
|
||||||
"user5": "",
|
|
||||||
"user6": "",
|
|
||||||
"user7": "",
|
|
||||||
"user8": "",
|
|
||||||
"user9": "",
|
|
||||||
"type": "event",
|
|
||||||
"revision": 0,
|
|
||||||
"id": "b7737",
|
|
||||||
"cue": "SF1.07"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"title": "Netherlands",
|
|
||||||
"subtitle": "De Diepte",
|
|
||||||
"presenter": "S10",
|
|
||||||
"note": "SF1.08",
|
|
||||||
"endAction": "none",
|
|
||||||
"timerType": "count-down",
|
|
||||||
"timeStart": 50100000,
|
|
||||||
"timeEnd": 51300000,
|
|
||||||
"duration": 1200000,
|
|
||||||
"isPublic": true,
|
|
||||||
"skip": false,
|
|
||||||
"colour": "",
|
|
||||||
"user0": "",
|
|
||||||
"user1": "",
|
|
||||||
"user2": "",
|
|
||||||
"user3": "",
|
|
||||||
"user4": "",
|
|
||||||
"user5": "",
|
|
||||||
"user6": "",
|
|
||||||
"user7": "",
|
|
||||||
"user8": "",
|
|
||||||
"user9": "",
|
|
||||||
"type": "event",
|
|
||||||
"revision": 0,
|
|
||||||
"id": "d3a80",
|
|
||||||
"cue": "SF1.08"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"title": "Moldova",
|
|
||||||
"subtitle": "Trenuletul",
|
|
||||||
"presenter": "Zdob si Zdub",
|
|
||||||
"note": "SF1.09",
|
|
||||||
"endAction": "none",
|
|
||||||
"timerType": "count-down",
|
|
||||||
"timeStart": 51600000,
|
|
||||||
"timeEnd": 52800000,
|
|
||||||
"duration": 1200000,
|
|
||||||
"isPublic": true,
|
|
||||||
"skip": false,
|
|
||||||
"colour": "",
|
|
||||||
"user0": "",
|
|
||||||
"user1": "",
|
|
||||||
"user2": "",
|
|
||||||
"user3": "",
|
|
||||||
"user4": "",
|
|
||||||
"user5": "",
|
|
||||||
"user6": "",
|
|
||||||
"user7": "",
|
|
||||||
"user8": "",
|
|
||||||
"user9": "",
|
|
||||||
"type": "event",
|
|
||||||
"revision": 0,
|
|
||||||
"id": "8276c",
|
|
||||||
"cue": "SF1.09"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"title": "Portugal",
|
|
||||||
"subtitle": "Saudade Saudade",
|
|
||||||
"presenter": "Maro",
|
|
||||||
"note": "SF1.10",
|
|
||||||
"endAction": "none",
|
|
||||||
"timerType": "count-down",
|
|
||||||
"timeStart": 53100000,
|
|
||||||
"timeEnd": 54300000,
|
|
||||||
"duration": 1200000,
|
|
||||||
"isPublic": true,
|
|
||||||
"skip": false,
|
|
||||||
"colour": "",
|
|
||||||
"user0": "",
|
|
||||||
"user1": "",
|
|
||||||
"user2": "",
|
|
||||||
"user3": "",
|
|
||||||
"user4": "",
|
|
||||||
"user5": "",
|
|
||||||
"user6": "",
|
|
||||||
"user7": "",
|
|
||||||
"user8": "",
|
|
||||||
"user9": "",
|
|
||||||
"type": "event",
|
|
||||||
"revision": 0,
|
|
||||||
"id": "2340b",
|
|
||||||
"cue": "SF1.10"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"title": "Afternoon break",
|
|
||||||
"type": "block",
|
|
||||||
"id": "cb90b"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"title": "Croatia",
|
|
||||||
"subtitle": "Guilty Pleasure",
|
|
||||||
"presenter": "Mia Dimsic",
|
|
||||||
"note": "SF1.11",
|
|
||||||
"endAction": "none",
|
|
||||||
"timerType": "count-down",
|
|
||||||
"timeStart": 56100000,
|
|
||||||
"timeEnd": 57300000,
|
|
||||||
"duration": 1200000,
|
|
||||||
"isPublic": true,
|
|
||||||
"skip": false,
|
|
||||||
"colour": "",
|
|
||||||
"user0": "",
|
|
||||||
"user1": "",
|
|
||||||
"user2": "",
|
|
||||||
"user3": "",
|
|
||||||
"user4": "",
|
|
||||||
"user5": "",
|
|
||||||
"user6": "",
|
|
||||||
"user7": "",
|
|
||||||
"user8": "",
|
|
||||||
"user9": "",
|
|
||||||
"type": "event",
|
|
||||||
"revision": 0,
|
|
||||||
"id": "503c4",
|
|
||||||
"cue": "SF1.11"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"title": "Denmark",
|
|
||||||
"subtitle": "The Show",
|
|
||||||
"presenter": "Reddi",
|
|
||||||
"note": "SF1.12",
|
|
||||||
"endAction": "none",
|
|
||||||
"timerType": "count-down",
|
|
||||||
"timeStart": 57600000,
|
|
||||||
"timeEnd": 58800000,
|
|
||||||
"duration": 1200000,
|
|
||||||
"isPublic": true,
|
|
||||||
"skip": false,
|
|
||||||
"colour": "",
|
|
||||||
"user0": "",
|
|
||||||
"user1": "",
|
|
||||||
"user2": "",
|
|
||||||
"user3": "",
|
|
||||||
"user4": "",
|
|
||||||
"user5": "",
|
|
||||||
"user6": "",
|
|
||||||
"user7": "",
|
|
||||||
"user8": "",
|
|
||||||
"user9": "",
|
|
||||||
"type": "event",
|
|
||||||
"revision": 0,
|
|
||||||
"id": "5e965",
|
|
||||||
"cue": "SF1.12"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"title": "Austria",
|
|
||||||
"subtitle": "Halo",
|
|
||||||
"presenter": "LUM!X & Pia Maria",
|
|
||||||
"note": "SF1.13",
|
|
||||||
"endAction": "none",
|
|
||||||
"timerType": "count-down",
|
|
||||||
"timeStart": 59100000,
|
|
||||||
"timeEnd": 60300000,
|
|
||||||
"duration": 1200000,
|
|
||||||
"isPublic": true,
|
|
||||||
"skip": false,
|
|
||||||
"colour": "",
|
|
||||||
"user0": "",
|
|
||||||
"user1": "",
|
|
||||||
"user2": "",
|
|
||||||
"user3": "",
|
|
||||||
"user4": "",
|
|
||||||
"user5": "",
|
|
||||||
"user6": "",
|
|
||||||
"user7": "",
|
|
||||||
"user8": "",
|
|
||||||
"user9": "",
|
|
||||||
"type": "event",
|
|
||||||
"revision": 0,
|
|
||||||
"id": "bab4a",
|
|
||||||
"cue": "SF1.13"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"title": "Greece",
|
|
||||||
"subtitle": "Die Together",
|
|
||||||
"presenter": "Amanda Tenfjord",
|
|
||||||
"note": "SF1.14",
|
|
||||||
"endAction": "none",
|
|
||||||
"timerType": "count-down",
|
|
||||||
"timeStart": 60600000,
|
|
||||||
"timeEnd": 61800000,
|
|
||||||
"duration": 1200000,
|
|
||||||
"isPublic": true,
|
|
||||||
"skip": false,
|
|
||||||
"colour": "",
|
|
||||||
"user0": "",
|
|
||||||
"user1": "",
|
|
||||||
"user2": "",
|
|
||||||
"user3": "",
|
|
||||||
"user4": "",
|
|
||||||
"user5": "",
|
|
||||||
"user6": "",
|
|
||||||
"user7": "",
|
|
||||||
"user8": "",
|
|
||||||
"user9": "",
|
|
||||||
"type": "event",
|
|
||||||
"revision": 0,
|
|
||||||
"id": "d3eb1",
|
|
||||||
"cue": "SF1.14"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"project": {
|
|
||||||
"title": "Eurovision Song Contest",
|
|
||||||
"description": "Turin 2022",
|
|
||||||
"publicUrl": "www.getontime.no",
|
|
||||||
"publicInfo": "Rehearsal Schedule - Turin 2022",
|
|
||||||
"backstageUrl": "www.github.com/cpvalente/ontime",
|
|
||||||
"backstageInfo": "Rehearsal Schedule - Turin 2022\nAll performers to wear full costumes for 1st rehearsal"
|
|
||||||
},
|
|
||||||
"settings": {
|
|
||||||
"app": "ontime",
|
|
||||||
"version": "3.0.0-alpha",
|
|
||||||
"serverPort": 4001,
|
|
||||||
"editorKey": null,
|
|
||||||
"operatorKey": null,
|
|
||||||
"timeFormat": "24",
|
|
||||||
"language": "en"
|
|
||||||
},
|
|
||||||
"viewSettings": {
|
|
||||||
"overrideStyles": false,
|
|
||||||
"normalColor": "#ffffffcc",
|
|
||||||
"warningColor": "#FFAB33",
|
|
||||||
"warningThreshold": 120000,
|
|
||||||
"dangerColor": "#ED3333",
|
|
||||||
"dangerThreshold": 60000,
|
|
||||||
"endMessage": ""
|
|
||||||
},
|
|
||||||
"aliases": [
|
|
||||||
{
|
|
||||||
"enabled": true,
|
|
||||||
"alias": "test",
|
|
||||||
"pathAndParams": "lower?bg=ff2&text=f00&size=0.6&transition=5"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"userFields": {
|
|
||||||
"user0": "user0",
|
|
||||||
"user1": "user1",
|
|
||||||
"user2": "user2",
|
|
||||||
"user3": "user3",
|
|
||||||
"user4": "user4",
|
|
||||||
"user5": "user5",
|
|
||||||
"user6": "user6",
|
|
||||||
"user7": "user7",
|
|
||||||
"user8": "user8",
|
|
||||||
"user9": "user9"
|
|
||||||
},
|
|
||||||
"osc": {
|
|
||||||
"portIn": 8888,
|
|
||||||
"portOut": 9999,
|
|
||||||
"targetIP": "127.0.0.1",
|
|
||||||
"enabledIn": true,
|
|
||||||
"enabledOut": true,
|
|
||||||
"subscriptions": {
|
|
||||||
"onLoad": [],
|
|
||||||
"onStart": [],
|
|
||||||
"onPause": [],
|
|
||||||
"onStop": [],
|
|
||||||
"onUpdate": [
|
|
||||||
{
|
|
||||||
"id": "10eea",
|
|
||||||
"enabled": true,
|
|
||||||
"message": "/ontime/update/{{timer.current}}"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"onFinish": []
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"http": {
|
|
||||||
"enabledOut": true,
|
|
||||||
"subscriptions": {
|
|
||||||
"onLoad": [],
|
|
||||||
"onStart": [],
|
|
||||||
"onPause": [],
|
|
||||||
"onStop": [],
|
|
||||||
"onUpdate": [],
|
|
||||||
"onFinish": []
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,483 +0,0 @@
|
|||||||
{
|
|
||||||
"rundown": [
|
|
||||||
{
|
|
||||||
"title": "Albania",
|
|
||||||
"subtitle": "Sekret",
|
|
||||||
"presenter": "Ronela Hajati",
|
|
||||||
"note": "SF1.01",
|
|
||||||
"endAction": "none",
|
|
||||||
"timerType": "count-down",
|
|
||||||
"timeStart": 36000000,
|
|
||||||
"timeEnd": 37200000,
|
|
||||||
"duration": 1200000,
|
|
||||||
"isPublic": true,
|
|
||||||
"skip": false,
|
|
||||||
"colour": "",
|
|
||||||
"user0": "",
|
|
||||||
"user1": "",
|
|
||||||
"user2": "",
|
|
||||||
"user3": "",
|
|
||||||
"user4": "",
|
|
||||||
"user5": "",
|
|
||||||
"user6": "",
|
|
||||||
"user7": "",
|
|
||||||
"user8": "",
|
|
||||||
"user9": "",
|
|
||||||
"type": "event",
|
|
||||||
"revision": 0,
|
|
||||||
"cue": "SF1.01",
|
|
||||||
"id": "32d31"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"title": "Latvia",
|
|
||||||
"subtitle": "Eat Your Salad",
|
|
||||||
"presenter": "Citi Zeni",
|
|
||||||
"note": "SF1.02",
|
|
||||||
"endAction": "none",
|
|
||||||
"timerType": "count-down",
|
|
||||||
"timeStart": 37500000,
|
|
||||||
"timeEnd": 38700000,
|
|
||||||
"duration": 1200000,
|
|
||||||
"isPublic": true,
|
|
||||||
"skip": false,
|
|
||||||
"colour": "",
|
|
||||||
"user0": "",
|
|
||||||
"user1": "",
|
|
||||||
"user2": "",
|
|
||||||
"user3": "",
|
|
||||||
"user4": "",
|
|
||||||
"user5": "",
|
|
||||||
"user6": "",
|
|
||||||
"user7": "",
|
|
||||||
"user8": "",
|
|
||||||
"user9": "",
|
|
||||||
"type": "event",
|
|
||||||
"revision": 0,
|
|
||||||
"cue": "SF1.02",
|
|
||||||
"id": "21cd2"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"title": "Lithuania",
|
|
||||||
"subtitle": "Sentimentai",
|
|
||||||
"presenter": "Monika Liu",
|
|
||||||
"note": "SF1.03",
|
|
||||||
"endAction": "none",
|
|
||||||
"timerType": "count-down",
|
|
||||||
"timeStart": 39000000,
|
|
||||||
"timeEnd": 40200000,
|
|
||||||
"duration": 1200000,
|
|
||||||
"isPublic": true,
|
|
||||||
"skip": false,
|
|
||||||
"colour": "",
|
|
||||||
"user0": "",
|
|
||||||
"user1": "",
|
|
||||||
"user2": "",
|
|
||||||
"user3": "",
|
|
||||||
"user4": "",
|
|
||||||
"user5": "",
|
|
||||||
"user6": "",
|
|
||||||
"user7": "",
|
|
||||||
"user8": "",
|
|
||||||
"user9": "",
|
|
||||||
"type": "event",
|
|
||||||
"revision": 0,
|
|
||||||
"cue": "SF1.03",
|
|
||||||
"id": "0b371"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"title": "Switzerland",
|
|
||||||
"subtitle": "Boys Do Cry",
|
|
||||||
"presenter": "Marius Bear",
|
|
||||||
"note": "SF1.04",
|
|
||||||
"endAction": "none",
|
|
||||||
"timerType": "count-down",
|
|
||||||
"timeStart": 40500000,
|
|
||||||
"timeEnd": 41700000,
|
|
||||||
"duration": 1200000,
|
|
||||||
"isPublic": true,
|
|
||||||
"skip": false,
|
|
||||||
"colour": "",
|
|
||||||
"user0": "",
|
|
||||||
"user1": "",
|
|
||||||
"user2": "",
|
|
||||||
"user3": "",
|
|
||||||
"user4": "",
|
|
||||||
"user5": "",
|
|
||||||
"user6": "",
|
|
||||||
"user7": "",
|
|
||||||
"user8": "",
|
|
||||||
"user9": "",
|
|
||||||
"type": "event",
|
|
||||||
"revision": 0,
|
|
||||||
"cue": "SF1.04",
|
|
||||||
"id": "3cd28"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"title": "Slovenia",
|
|
||||||
"subtitle": "Disko",
|
|
||||||
"presenter": "LPS",
|
|
||||||
"note": "SF1.05",
|
|
||||||
"endAction": "none",
|
|
||||||
"timerType": "count-down",
|
|
||||||
"timeStart": 42000000,
|
|
||||||
"timeEnd": 43200000,
|
|
||||||
"duration": 1200000,
|
|
||||||
"isPublic": true,
|
|
||||||
"skip": false,
|
|
||||||
"colour": "",
|
|
||||||
"user0": "",
|
|
||||||
"user1": "",
|
|
||||||
"user2": "",
|
|
||||||
"user3": "",
|
|
||||||
"user4": "",
|
|
||||||
"user5": "",
|
|
||||||
"user6": "",
|
|
||||||
"user7": "",
|
|
||||||
"user8": "",
|
|
||||||
"user9": "",
|
|
||||||
"type": "event",
|
|
||||||
"revision": 0,
|
|
||||||
"cue": "SF1.05",
|
|
||||||
"id": "e457f"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"title": "Lunch break",
|
|
||||||
"type": "block",
|
|
||||||
"id": "01e85"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"title": "Ukraine",
|
|
||||||
"subtitle": "Stefania",
|
|
||||||
"presenter": "Kalush Orchestra",
|
|
||||||
"note": "SF1.06",
|
|
||||||
"endAction": "none",
|
|
||||||
"timerType": "count-down",
|
|
||||||
"timeStart": 47100000,
|
|
||||||
"timeEnd": 48300000,
|
|
||||||
"duration": 1200000,
|
|
||||||
"isPublic": true,
|
|
||||||
"skip": false,
|
|
||||||
"colour": "",
|
|
||||||
"user0": "",
|
|
||||||
"user1": "",
|
|
||||||
"user2": "",
|
|
||||||
"user3": "",
|
|
||||||
"user4": "",
|
|
||||||
"user5": "",
|
|
||||||
"user6": "",
|
|
||||||
"user7": "",
|
|
||||||
"user8": "",
|
|
||||||
"user9": "",
|
|
||||||
"type": "event",
|
|
||||||
"revision": 0,
|
|
||||||
"cue": "SF1.06",
|
|
||||||
"id": "1c420"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"title": "Bulgaria",
|
|
||||||
"subtitle": "Intention",
|
|
||||||
"presenter": "Intelligent Music Project",
|
|
||||||
"note": "SF1.07",
|
|
||||||
"endAction": "none",
|
|
||||||
"timerType": "count-down",
|
|
||||||
"timeStart": 48600000,
|
|
||||||
"timeEnd": 49800000,
|
|
||||||
"duration": 1200000,
|
|
||||||
"isPublic": true,
|
|
||||||
"skip": false,
|
|
||||||
"colour": "",
|
|
||||||
"user0": "",
|
|
||||||
"user1": "",
|
|
||||||
"user2": "",
|
|
||||||
"user3": "",
|
|
||||||
"user4": "",
|
|
||||||
"user5": "",
|
|
||||||
"user6": "",
|
|
||||||
"user7": "",
|
|
||||||
"user8": "",
|
|
||||||
"user9": "",
|
|
||||||
"type": "event",
|
|
||||||
"revision": 0,
|
|
||||||
"cue": "SF1.07",
|
|
||||||
"id": "b7737"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"title": "Netherlands",
|
|
||||||
"subtitle": "De Diepte",
|
|
||||||
"presenter": "S10",
|
|
||||||
"note": "SF1.08",
|
|
||||||
"endAction": "none",
|
|
||||||
"timerType": "count-down",
|
|
||||||
"timeStart": 50100000,
|
|
||||||
"timeEnd": 51300000,
|
|
||||||
"duration": 1200000,
|
|
||||||
"isPublic": true,
|
|
||||||
"skip": false,
|
|
||||||
"colour": "",
|
|
||||||
"user0": "",
|
|
||||||
"user1": "",
|
|
||||||
"user2": "",
|
|
||||||
"user3": "",
|
|
||||||
"user4": "",
|
|
||||||
"user5": "",
|
|
||||||
"user6": "",
|
|
||||||
"user7": "",
|
|
||||||
"user8": "",
|
|
||||||
"user9": "",
|
|
||||||
"type": "event",
|
|
||||||
"revision": 0,
|
|
||||||
"cue": "SF1.08",
|
|
||||||
"id": "d3a80"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"title": "Moldova",
|
|
||||||
"subtitle": "Trenuletul",
|
|
||||||
"presenter": "Zdob si Zdub",
|
|
||||||
"note": "SF1.09",
|
|
||||||
"endAction": "none",
|
|
||||||
"timerType": "count-down",
|
|
||||||
"timeStart": 51600000,
|
|
||||||
"timeEnd": 52800000,
|
|
||||||
"duration": 1200000,
|
|
||||||
"isPublic": true,
|
|
||||||
"skip": false,
|
|
||||||
"colour": "",
|
|
||||||
"user0": "",
|
|
||||||
"user1": "",
|
|
||||||
"user2": "",
|
|
||||||
"user3": "",
|
|
||||||
"user4": "",
|
|
||||||
"user5": "",
|
|
||||||
"user6": "",
|
|
||||||
"user7": "",
|
|
||||||
"user8": "",
|
|
||||||
"user9": "",
|
|
||||||
"type": "event",
|
|
||||||
"revision": 0,
|
|
||||||
"cue": "SF1.09",
|
|
||||||
"id": "8276c"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"title": "Portugal",
|
|
||||||
"subtitle": "Saudade Saudade",
|
|
||||||
"presenter": "Maro",
|
|
||||||
"note": "SF1.10",
|
|
||||||
"endAction": "none",
|
|
||||||
"timerType": "count-down",
|
|
||||||
"timeStart": 53100000,
|
|
||||||
"timeEnd": 54300000,
|
|
||||||
"duration": 1200000,
|
|
||||||
"isPublic": true,
|
|
||||||
"skip": false,
|
|
||||||
"colour": "",
|
|
||||||
"user0": "",
|
|
||||||
"user1": "",
|
|
||||||
"user2": "",
|
|
||||||
"user3": "",
|
|
||||||
"user4": "",
|
|
||||||
"user5": "",
|
|
||||||
"user6": "",
|
|
||||||
"user7": "",
|
|
||||||
"user8": "",
|
|
||||||
"user9": "",
|
|
||||||
"type": "event",
|
|
||||||
"revision": 0,
|
|
||||||
"cue": "SF1.10",
|
|
||||||
"id": "2340b"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"title": "Afternoon break",
|
|
||||||
"type": "block",
|
|
||||||
"id": "cb90b"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"title": "Croatia",
|
|
||||||
"subtitle": "Guilty Pleasure",
|
|
||||||
"presenter": "Mia Dimsic",
|
|
||||||
"note": "SF1.11",
|
|
||||||
"endAction": "none",
|
|
||||||
"timerType": "count-down",
|
|
||||||
"timeStart": 56100000,
|
|
||||||
"timeEnd": 57300000,
|
|
||||||
"duration": 1200000,
|
|
||||||
"isPublic": true,
|
|
||||||
"skip": false,
|
|
||||||
"colour": "",
|
|
||||||
"user0": "",
|
|
||||||
"user1": "",
|
|
||||||
"user2": "",
|
|
||||||
"user3": "",
|
|
||||||
"user4": "",
|
|
||||||
"user5": "",
|
|
||||||
"user6": "",
|
|
||||||
"user7": "",
|
|
||||||
"user8": "",
|
|
||||||
"user9": "",
|
|
||||||
"type": "event",
|
|
||||||
"revision": 0,
|
|
||||||
"cue": "SF1.11",
|
|
||||||
"id": "503c4"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"title": "Denmark",
|
|
||||||
"subtitle": "The Show",
|
|
||||||
"presenter": "Reddi",
|
|
||||||
"note": "SF1.12",
|
|
||||||
"endAction": "none",
|
|
||||||
"timerType": "count-down",
|
|
||||||
"timeStart": 57600000,
|
|
||||||
"timeEnd": 58800000,
|
|
||||||
"duration": 1200000,
|
|
||||||
"isPublic": true,
|
|
||||||
"skip": false,
|
|
||||||
"colour": "",
|
|
||||||
"user0": "",
|
|
||||||
"user1": "",
|
|
||||||
"user2": "",
|
|
||||||
"user3": "",
|
|
||||||
"user4": "",
|
|
||||||
"user5": "",
|
|
||||||
"user6": "",
|
|
||||||
"user7": "",
|
|
||||||
"user8": "",
|
|
||||||
"user9": "",
|
|
||||||
"type": "event",
|
|
||||||
"revision": 0,
|
|
||||||
"cue": "SF1.12",
|
|
||||||
"id": "5e965"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"title": "Austria",
|
|
||||||
"subtitle": "Halo",
|
|
||||||
"presenter": "LUM!X & Pia Maria",
|
|
||||||
"note": "SF1.13",
|
|
||||||
"endAction": "none",
|
|
||||||
"timerType": "count-down",
|
|
||||||
"timeStart": 59100000,
|
|
||||||
"timeEnd": 60300000,
|
|
||||||
"duration": 1200000,
|
|
||||||
"isPublic": true,
|
|
||||||
"skip": false,
|
|
||||||
"colour": "",
|
|
||||||
"user0": "",
|
|
||||||
"user1": "",
|
|
||||||
"user2": "",
|
|
||||||
"user3": "",
|
|
||||||
"user4": "",
|
|
||||||
"user5": "",
|
|
||||||
"user6": "",
|
|
||||||
"user7": "",
|
|
||||||
"user8": "",
|
|
||||||
"user9": "",
|
|
||||||
"type": "event",
|
|
||||||
"revision": 0,
|
|
||||||
"cue": "SF1.13",
|
|
||||||
"id": "bab4a"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"title": "Greece",
|
|
||||||
"subtitle": "Die Together",
|
|
||||||
"presenter": "Amanda Tenfjord",
|
|
||||||
"note": "SF1.14",
|
|
||||||
"endAction": "none",
|
|
||||||
"timerType": "count-down",
|
|
||||||
"timeStart": 60600000,
|
|
||||||
"timeEnd": 61800000,
|
|
||||||
"duration": 1200000,
|
|
||||||
"isPublic": true,
|
|
||||||
"skip": false,
|
|
||||||
"colour": "",
|
|
||||||
"user0": "",
|
|
||||||
"user1": "",
|
|
||||||
"user2": "",
|
|
||||||
"user3": "",
|
|
||||||
"user4": "",
|
|
||||||
"user5": "",
|
|
||||||
"user6": "",
|
|
||||||
"user7": "",
|
|
||||||
"user8": "",
|
|
||||||
"user9": "",
|
|
||||||
"type": "event",
|
|
||||||
"revision": 0,
|
|
||||||
"cue": "SF1.14",
|
|
||||||
"id": "d3eb1"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"project": {
|
|
||||||
"title": "Eurovision Song Contest",
|
|
||||||
"description": "Turin 2022",
|
|
||||||
"publicUrl": "www.getontime.no",
|
|
||||||
"publicInfo": "Rehearsal Schedule - Turin 2022",
|
|
||||||
"backstageUrl": "www.github.com/cpvalente/ontime",
|
|
||||||
"backstageInfo": "Rehearsal Schedule - Turin 2022\nAll performers to wear full costumes for 1st rehearsal"
|
|
||||||
},
|
|
||||||
"settings": {
|
|
||||||
"app": "ontime",
|
|
||||||
"version": "3.0.0-alpha",
|
|
||||||
"serverPort": 4001,
|
|
||||||
"editorKey": null,
|
|
||||||
"operatorKey": null,
|
|
||||||
"timeFormat": "24",
|
|
||||||
"language": "en"
|
|
||||||
},
|
|
||||||
"viewSettings": {
|
|
||||||
"overrideStyles": false,
|
|
||||||
"normalColor": "#ffffffcc",
|
|
||||||
"warningColor": "#FFAB33",
|
|
||||||
"warningThreshold": 120000,
|
|
||||||
"dangerColor": "#ED3333",
|
|
||||||
"dangerThreshold": 60000,
|
|
||||||
"endMessage": ""
|
|
||||||
},
|
|
||||||
"aliases": [
|
|
||||||
{
|
|
||||||
"enabled": true,
|
|
||||||
"alias": "test",
|
|
||||||
"pathAndParams": "lower?bg=ff2&text=f00&size=0.6&transition=5"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"userFields": {
|
|
||||||
"user0": "user0",
|
|
||||||
"user1": "user1",
|
|
||||||
"user2": "user2",
|
|
||||||
"user3": "user3",
|
|
||||||
"user4": "user4",
|
|
||||||
"user5": "user5",
|
|
||||||
"user6": "user6",
|
|
||||||
"user7": "user7",
|
|
||||||
"user8": "user8",
|
|
||||||
"user9": "user9"
|
|
||||||
},
|
|
||||||
"osc": {
|
|
||||||
"portIn": 8888,
|
|
||||||
"portOut": 9999,
|
|
||||||
"targetIP": "127.0.0.1",
|
|
||||||
"enabledIn": true,
|
|
||||||
"enabledOut": true,
|
|
||||||
"subscriptions": {
|
|
||||||
"onLoad": [],
|
|
||||||
"onStart": [],
|
|
||||||
"onPause": [],
|
|
||||||
"onStop": [],
|
|
||||||
"onUpdate": [
|
|
||||||
{
|
|
||||||
"id": "10eea",
|
|
||||||
"enabled": true,
|
|
||||||
"message": "/ontime/update/{{timer.current}}"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"onFinish": []
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"http": {
|
|
||||||
"enabledOut": true,
|
|
||||||
"subscriptions": {
|
|
||||||
"onLoad": [],
|
|
||||||
"onStart": [],
|
|
||||||
"onPause": [],
|
|
||||||
"onStop": [],
|
|
||||||
"onUpdate": [],
|
|
||||||
"onFinish": []
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,481 +0,0 @@
|
|||||||
{
|
|
||||||
"rundown": [
|
|
||||||
{
|
|
||||||
"title": "Albania",
|
|
||||||
"subtitle": "Sekret",
|
|
||||||
"presenter": "Ronela Hajati",
|
|
||||||
"note": "SF1.01",
|
|
||||||
"endAction": "none",
|
|
||||||
"timerType": "count-down",
|
|
||||||
"timeStart": 36000000,
|
|
||||||
"timeEnd": 37200000,
|
|
||||||
"duration": 1200000,
|
|
||||||
"isPublic": true,
|
|
||||||
"skip": false,
|
|
||||||
"colour": "",
|
|
||||||
"user0": "",
|
|
||||||
"user1": "",
|
|
||||||
"user2": "",
|
|
||||||
"user3": "",
|
|
||||||
"user4": "",
|
|
||||||
"user5": "",
|
|
||||||
"user6": "",
|
|
||||||
"user7": "",
|
|
||||||
"user8": "",
|
|
||||||
"user9": "",
|
|
||||||
"type": "event",
|
|
||||||
"revision": 0,
|
|
||||||
"cue": "SF1.01",
|
|
||||||
"id": "32d31"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"title": "Latvia",
|
|
||||||
"subtitle": "Eat Your Salad",
|
|
||||||
"presenter": "Citi Zeni",
|
|
||||||
"note": "SF1.02",
|
|
||||||
"endAction": "none",
|
|
||||||
"timerType": "count-down",
|
|
||||||
"timeStart": 37500000,
|
|
||||||
"timeEnd": 38700000,
|
|
||||||
"duration": 1200000,
|
|
||||||
"isPublic": true,
|
|
||||||
"skip": false,
|
|
||||||
"colour": "",
|
|
||||||
"user0": "",
|
|
||||||
"user1": "",
|
|
||||||
"user2": "",
|
|
||||||
"user3": "",
|
|
||||||
"user4": "",
|
|
||||||
"user5": "",
|
|
||||||
"user6": "",
|
|
||||||
"user7": "",
|
|
||||||
"user8": "",
|
|
||||||
"user9": "",
|
|
||||||
"type": "event",
|
|
||||||
"revision": 0,
|
|
||||||
"cue": "SF1.02",
|
|
||||||
"id": "21cd2"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"title": "Lithuania",
|
|
||||||
"subtitle": "Sentimentai",
|
|
||||||
"presenter": "Monika Liu",
|
|
||||||
"note": "SF1.03",
|
|
||||||
"endAction": "none",
|
|
||||||
"timerType": "count-down",
|
|
||||||
"timeStart": 39000000,
|
|
||||||
"timeEnd": 40200000,
|
|
||||||
"duration": 1200000,
|
|
||||||
"isPublic": true,
|
|
||||||
"skip": false,
|
|
||||||
"colour": "",
|
|
||||||
"user0": "",
|
|
||||||
"user1": "",
|
|
||||||
"user2": "",
|
|
||||||
"user3": "",
|
|
||||||
"user4": "",
|
|
||||||
"user5": "",
|
|
||||||
"user6": "",
|
|
||||||
"user7": "",
|
|
||||||
"user8": "",
|
|
||||||
"user9": "",
|
|
||||||
"type": "event",
|
|
||||||
"revision": 0,
|
|
||||||
"cue": "SF1.03",
|
|
||||||
"id": "0b371"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"title": "Switzerland",
|
|
||||||
"subtitle": "Boys Do Cry",
|
|
||||||
"presenter": "Marius Bear",
|
|
||||||
"note": "SF1.04",
|
|
||||||
"endAction": "none",
|
|
||||||
"timerType": "count-down",
|
|
||||||
"timeStart": 40500000,
|
|
||||||
"timeEnd": 41700000,
|
|
||||||
"duration": 1200000,
|
|
||||||
"isPublic": true,
|
|
||||||
"skip": false,
|
|
||||||
"colour": "",
|
|
||||||
"user0": "",
|
|
||||||
"user1": "",
|
|
||||||
"user2": "",
|
|
||||||
"user3": "",
|
|
||||||
"user4": "",
|
|
||||||
"user5": "",
|
|
||||||
"user6": "",
|
|
||||||
"user7": "",
|
|
||||||
"user8": "",
|
|
||||||
"user9": "",
|
|
||||||
"type": "event",
|
|
||||||
"revision": 0,
|
|
||||||
"cue": "SF1.04",
|
|
||||||
"id": "3cd28"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"title": "Slovenia",
|
|
||||||
"subtitle": "Disko",
|
|
||||||
"presenter": "LPS",
|
|
||||||
"note": "SF1.05",
|
|
||||||
"endAction": "none",
|
|
||||||
"timerType": "count-down",
|
|
||||||
"timeStart": 42000000,
|
|
||||||
"timeEnd": 43200000,
|
|
||||||
"duration": 1200000,
|
|
||||||
"isPublic": true,
|
|
||||||
"skip": false,
|
|
||||||
"colour": "",
|
|
||||||
"user0": "",
|
|
||||||
"user1": "",
|
|
||||||
"user2": "",
|
|
||||||
"user3": "",
|
|
||||||
"user4": "",
|
|
||||||
"user5": "",
|
|
||||||
"user6": "",
|
|
||||||
"user7": "",
|
|
||||||
"user8": "",
|
|
||||||
"user9": "",
|
|
||||||
"type": "event",
|
|
||||||
"revision": 0,
|
|
||||||
"cue": "SF1.05",
|
|
||||||
"id": "e457f"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"title": "Lunch break",
|
|
||||||
"type": "block",
|
|
||||||
"id": "01e85"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"title": "Ukraine",
|
|
||||||
"subtitle": "Stefania",
|
|
||||||
"presenter": "Kalush Orchestra",
|
|
||||||
"note": "SF1.06",
|
|
||||||
"endAction": "none",
|
|
||||||
"timerType": "count-down",
|
|
||||||
"timeStart": 47100000,
|
|
||||||
"timeEnd": 48300000,
|
|
||||||
"duration": 1200000,
|
|
||||||
"isPublic": true,
|
|
||||||
"skip": false,
|
|
||||||
"colour": "",
|
|
||||||
"user0": "",
|
|
||||||
"user1": "",
|
|
||||||
"user2": "",
|
|
||||||
"user3": "",
|
|
||||||
"user4": "",
|
|
||||||
"user5": "",
|
|
||||||
"user6": "",
|
|
||||||
"user7": "",
|
|
||||||
"user8": "",
|
|
||||||
"user9": "",
|
|
||||||
"type": "event",
|
|
||||||
"revision": 0,
|
|
||||||
"cue": "SF1.06",
|
|
||||||
"id": "1c420"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"title": "Bulgaria",
|
|
||||||
"subtitle": "Intention",
|
|
||||||
"presenter": "Intelligent Music Project",
|
|
||||||
"note": "SF1.07",
|
|
||||||
"endAction": "none",
|
|
||||||
"timerType": "count-down",
|
|
||||||
"timeStart": 48600000,
|
|
||||||
"timeEnd": 49800000,
|
|
||||||
"duration": 1200000,
|
|
||||||
"isPublic": true,
|
|
||||||
"skip": false,
|
|
||||||
"colour": "",
|
|
||||||
"user0": "",
|
|
||||||
"user1": "",
|
|
||||||
"user2": "",
|
|
||||||
"user3": "",
|
|
||||||
"user4": "",
|
|
||||||
"user5": "",
|
|
||||||
"user6": "",
|
|
||||||
"user7": "",
|
|
||||||
"user8": "",
|
|
||||||
"user9": "",
|
|
||||||
"type": "event",
|
|
||||||
"revision": 0,
|
|
||||||
"cue": "SF1.07",
|
|
||||||
"id": "b7737"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"title": "Netherlands",
|
|
||||||
"subtitle": "De Diepte",
|
|
||||||
"presenter": "S10",
|
|
||||||
"note": "SF1.08",
|
|
||||||
"endAction": "none",
|
|
||||||
"timerType": "count-down",
|
|
||||||
"timeStart": 50100000,
|
|
||||||
"timeEnd": 51300000,
|
|
||||||
"duration": 1200000,
|
|
||||||
"isPublic": true,
|
|
||||||
"skip": false,
|
|
||||||
"colour": "",
|
|
||||||
"user0": "",
|
|
||||||
"user1": "",
|
|
||||||
"user2": "",
|
|
||||||
"user3": "",
|
|
||||||
"user4": "",
|
|
||||||
"user5": "",
|
|
||||||
"user6": "",
|
|
||||||
"user7": "",
|
|
||||||
"user8": "",
|
|
||||||
"user9": "",
|
|
||||||
"type": "event",
|
|
||||||
"revision": 0,
|
|
||||||
"cue": "SF1.08",
|
|
||||||
"id": "d3a80"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"title": "Moldova",
|
|
||||||
"subtitle": "Trenuletul",
|
|
||||||
"presenter": "Zdob si Zdub",
|
|
||||||
"note": "SF1.09",
|
|
||||||
"endAction": "none",
|
|
||||||
"timerType": "count-down",
|
|
||||||
"timeStart": 51600000,
|
|
||||||
"timeEnd": 52800000,
|
|
||||||
"duration": 1200000,
|
|
||||||
"isPublic": true,
|
|
||||||
"skip": false,
|
|
||||||
"colour": "",
|
|
||||||
"user0": "",
|
|
||||||
"user1": "",
|
|
||||||
"user2": "",
|
|
||||||
"user3": "",
|
|
||||||
"user4": "",
|
|
||||||
"user5": "",
|
|
||||||
"user6": "",
|
|
||||||
"user7": "",
|
|
||||||
"user8": "",
|
|
||||||
"user9": "",
|
|
||||||
"type": "event",
|
|
||||||
"revision": 0,
|
|
||||||
"cue": "SF1.09",
|
|
||||||
"id": "8276c"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"title": "Portugal",
|
|
||||||
"subtitle": "Saudade Saudade",
|
|
||||||
"presenter": "Maro",
|
|
||||||
"note": "SF1.10",
|
|
||||||
"endAction": "none",
|
|
||||||
"timerType": "count-down",
|
|
||||||
"timeStart": 53100000,
|
|
||||||
"timeEnd": 54300000,
|
|
||||||
"duration": 1200000,
|
|
||||||
"isPublic": true,
|
|
||||||
"skip": false,
|
|
||||||
"colour": "",
|
|
||||||
"user0": "",
|
|
||||||
"user1": "",
|
|
||||||
"user2": "",
|
|
||||||
"user3": "",
|
|
||||||
"user4": "",
|
|
||||||
"user5": "",
|
|
||||||
"user6": "",
|
|
||||||
"user7": "",
|
|
||||||
"user8": "",
|
|
||||||
"user9": "",
|
|
||||||
"type": "event",
|
|
||||||
"revision": 0,
|
|
||||||
"cue": "SF1.10",
|
|
||||||
"id": "2340b"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"title": "Afternoon break",
|
|
||||||
"type": "block",
|
|
||||||
"id": "cb90b"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"title": "Croatia",
|
|
||||||
"subtitle": "Guilty Pleasure",
|
|
||||||
"presenter": "Mia Dimsic",
|
|
||||||
"note": "SF1.11",
|
|
||||||
"endAction": "none",
|
|
||||||
"timerType": "count-down",
|
|
||||||
"timeStart": 56100000,
|
|
||||||
"timeEnd": 57300000,
|
|
||||||
"duration": 1200000,
|
|
||||||
"isPublic": true,
|
|
||||||
"skip": false,
|
|
||||||
"colour": "",
|
|
||||||
"user0": "",
|
|
||||||
"user1": "",
|
|
||||||
"user2": "",
|
|
||||||
"user3": "",
|
|
||||||
"user4": "",
|
|
||||||
"user5": "",
|
|
||||||
"user6": "",
|
|
||||||
"user7": "",
|
|
||||||
"user8": "",
|
|
||||||
"user9": "",
|
|
||||||
"type": "event",
|
|
||||||
"revision": 0,
|
|
||||||
"cue": "SF1.11",
|
|
||||||
"id": "503c4"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"title": "Denmark",
|
|
||||||
"subtitle": "The Show",
|
|
||||||
"presenter": "Reddi",
|
|
||||||
"note": "SF1.12",
|
|
||||||
"endAction": "none",
|
|
||||||
"timerType": "count-down",
|
|
||||||
"timeStart": 57600000,
|
|
||||||
"timeEnd": 58800000,
|
|
||||||
"duration": 1200000,
|
|
||||||
"isPublic": true,
|
|
||||||
"skip": false,
|
|
||||||
"colour": "",
|
|
||||||
"user0": "",
|
|
||||||
"user1": "",
|
|
||||||
"user2": "",
|
|
||||||
"user3": "",
|
|
||||||
"user4": "",
|
|
||||||
"user5": "",
|
|
||||||
"user6": "",
|
|
||||||
"user7": "",
|
|
||||||
"user8": "",
|
|
||||||
"user9": "",
|
|
||||||
"type": "event",
|
|
||||||
"revision": 0,
|
|
||||||
"cue": "SF1.12",
|
|
||||||
"id": "5e965"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"title": "Austria",
|
|
||||||
"subtitle": "Halo",
|
|
||||||
"presenter": "LUM!X & Pia Maria",
|
|
||||||
"note": "SF1.13",
|
|
||||||
"endAction": "none",
|
|
||||||
"timerType": "count-down",
|
|
||||||
"timeStart": 59100000,
|
|
||||||
"timeEnd": 60300000,
|
|
||||||
"duration": 1200000,
|
|
||||||
"isPublic": true,
|
|
||||||
"skip": false,
|
|
||||||
"colour": "",
|
|
||||||
"user0": "",
|
|
||||||
"user1": "",
|
|
||||||
"user2": "",
|
|
||||||
"user3": "",
|
|
||||||
"user4": "",
|
|
||||||
"user5": "",
|
|
||||||
"user6": "",
|
|
||||||
"user7": "",
|
|
||||||
"user8": "",
|
|
||||||
"user9": "",
|
|
||||||
"type": "event",
|
|
||||||
"revision": 0,
|
|
||||||
"cue": "SF1.13",
|
|
||||||
"id": "bab4a"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"title": "Greece",
|
|
||||||
"subtitle": "Die Together",
|
|
||||||
"presenter": "Amanda Tenfjord",
|
|
||||||
"note": "SF1.14",
|
|
||||||
"endAction": "none",
|
|
||||||
"timerType": "count-down",
|
|
||||||
"timeStart": 60600000,
|
|
||||||
"timeEnd": 61800000,
|
|
||||||
"duration": 1200000,
|
|
||||||
"isPublic": true,
|
|
||||||
"skip": false,
|
|
||||||
"colour": "",
|
|
||||||
"user0": "",
|
|
||||||
"user1": "",
|
|
||||||
"user2": "",
|
|
||||||
"user3": "",
|
|
||||||
"user4": "",
|
|
||||||
"user5": "",
|
|
||||||
"user6": "",
|
|
||||||
"user7": "",
|
|
||||||
"user8": "",
|
|
||||||
"user9": "",
|
|
||||||
"type": "event",
|
|
||||||
"revision": 0,
|
|
||||||
"cue": "SF1.14",
|
|
||||||
"id": "d3eb1"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"project": {
|
|
||||||
"title": "Eurovision Song Contest",
|
|
||||||
"description": "Turin 2022",
|
|
||||||
"publicUrl": "www.getontime.no",
|
|
||||||
"publicInfo": "Rehearsal Schedule - Turin 2022",
|
|
||||||
"backstageUrl": "www.github.com/cpvalente/ontime",
|
|
||||||
"backstageInfo": "Rehearsal Schedule - Turin 2022\nAll performers to wear full costumes for 1st rehearsal"
|
|
||||||
},
|
|
||||||
"settings": {
|
|
||||||
"app": "ontime",
|
|
||||||
"version": "3.0.0-alpha",
|
|
||||||
"serverPort": 4001,
|
|
||||||
"editorKey": null,
|
|
||||||
"operatorKey": null,
|
|
||||||
"timeFormat": "24",
|
|
||||||
"language": "en"
|
|
||||||
},
|
|
||||||
"viewSettings": {
|
|
||||||
"overrideStyles": false,
|
|
||||||
"normalColor": "#ffffffcc",
|
|
||||||
"warningColor": "#FFAB33",
|
|
||||||
"dangerColor": "#ED3333",
|
|
||||||
"endMessage": ""
|
|
||||||
},
|
|
||||||
"aliases": [
|
|
||||||
{
|
|
||||||
"enabled": true,
|
|
||||||
"alias": "test",
|
|
||||||
"pathAndParams": "lower?bg=ff2&text=f00&size=0.6&transition=5"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"userFields": {
|
|
||||||
"user0": "user0",
|
|
||||||
"user1": "user1",
|
|
||||||
"user2": "user2",
|
|
||||||
"user3": "user3",
|
|
||||||
"user4": "user4",
|
|
||||||
"user5": "user5",
|
|
||||||
"user6": "user6",
|
|
||||||
"user7": "user7",
|
|
||||||
"user8": "user8",
|
|
||||||
"user9": "user9"
|
|
||||||
},
|
|
||||||
"osc": {
|
|
||||||
"portIn": 8888,
|
|
||||||
"portOut": 9999,
|
|
||||||
"targetIP": "127.0.0.1",
|
|
||||||
"enabledIn": true,
|
|
||||||
"enabledOut": true,
|
|
||||||
"subscriptions": {
|
|
||||||
"onLoad": [],
|
|
||||||
"onStart": [],
|
|
||||||
"onPause": [],
|
|
||||||
"onStop": [],
|
|
||||||
"onUpdate": [
|
|
||||||
{
|
|
||||||
"id": "10eea",
|
|
||||||
"enabled": true,
|
|
||||||
"message": "/ontime/update/{{timer.current}}"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"onFinish": []
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"http": {
|
|
||||||
"enabledOut": true,
|
|
||||||
"subscriptions": {
|
|
||||||
"onLoad": [],
|
|
||||||
"onStart": [],
|
|
||||||
"onPause": [],
|
|
||||||
"onStop": [],
|
|
||||||
"onUpdate": [],
|
|
||||||
"onFinish": []
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,481 +0,0 @@
|
|||||||
{
|
|
||||||
"rundown": [
|
|
||||||
{
|
|
||||||
"id": "32d31",
|
|
||||||
"type": "event",
|
|
||||||
"title": "Albania",
|
|
||||||
"subtitle": "Sekret",
|
|
||||||
"presenter": "Ronela Hajati",
|
|
||||||
"timeStart": 36000000,
|
|
||||||
"timeEnd": 37200000,
|
|
||||||
"duration": 1200000,
|
|
||||||
"endAction": "none",
|
|
||||||
"timerType": "count-down",
|
|
||||||
"isPublic": true,
|
|
||||||
"skip": false,
|
|
||||||
"note": "SF1.01",
|
|
||||||
"user0": "",
|
|
||||||
"user1": "",
|
|
||||||
"user2": "",
|
|
||||||
"user3": "",
|
|
||||||
"user4": "",
|
|
||||||
"user5": "",
|
|
||||||
"user6": "",
|
|
||||||
"user7": "",
|
|
||||||
"user8": "",
|
|
||||||
"user9": "",
|
|
||||||
"colour": "",
|
|
||||||
"cue": "SF1.01",
|
|
||||||
"revision": 0
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "21cd2",
|
|
||||||
"type": "event",
|
|
||||||
"title": "Latvia",
|
|
||||||
"subtitle": "Eat Your Salad",
|
|
||||||
"presenter": "Citi Zeni",
|
|
||||||
"timeStart": 37500000,
|
|
||||||
"timeEnd": 38700000,
|
|
||||||
"duration": 1200000,
|
|
||||||
"endAction": "none",
|
|
||||||
"timerType": "count-down",
|
|
||||||
"isPublic": true,
|
|
||||||
"skip": false,
|
|
||||||
"note": "SF1.02",
|
|
||||||
"user0": "",
|
|
||||||
"user1": "",
|
|
||||||
"user2": "",
|
|
||||||
"user3": "",
|
|
||||||
"user4": "",
|
|
||||||
"user5": "",
|
|
||||||
"user6": "",
|
|
||||||
"user7": "",
|
|
||||||
"user8": "",
|
|
||||||
"user9": "",
|
|
||||||
"colour": "",
|
|
||||||
"cue": "SF1.02",
|
|
||||||
"revision": 0
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "0b371",
|
|
||||||
"type": "event",
|
|
||||||
"title": "Lithuania",
|
|
||||||
"subtitle": "Sentimentai",
|
|
||||||
"presenter": "Monika Liu",
|
|
||||||
"timeStart": 39000000,
|
|
||||||
"timeEnd": 40200000,
|
|
||||||
"duration": 1200000,
|
|
||||||
"endAction": "none",
|
|
||||||
"timerType": "count-down",
|
|
||||||
"isPublic": true,
|
|
||||||
"skip": false,
|
|
||||||
"note": "SF1.03",
|
|
||||||
"user0": "",
|
|
||||||
"user1": "",
|
|
||||||
"user2": "",
|
|
||||||
"user3": "",
|
|
||||||
"user4": "",
|
|
||||||
"user5": "",
|
|
||||||
"user6": "",
|
|
||||||
"user7": "",
|
|
||||||
"user8": "",
|
|
||||||
"user9": "",
|
|
||||||
"colour": "",
|
|
||||||
"cue": "SF1.03",
|
|
||||||
"revision": 0
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "3cd28",
|
|
||||||
"type": "event",
|
|
||||||
"title": "Switzerland",
|
|
||||||
"subtitle": "Boys Do Cry",
|
|
||||||
"presenter": "Marius Bear",
|
|
||||||
"timeStart": 40500000,
|
|
||||||
"timeEnd": 41700000,
|
|
||||||
"duration": 1200000,
|
|
||||||
"endAction": "none",
|
|
||||||
"timerType": "count-down",
|
|
||||||
"isPublic": true,
|
|
||||||
"skip": false,
|
|
||||||
"note": "SF1.04",
|
|
||||||
"user0": "",
|
|
||||||
"user1": "",
|
|
||||||
"user2": "",
|
|
||||||
"user3": "",
|
|
||||||
"user4": "",
|
|
||||||
"user5": "",
|
|
||||||
"user6": "",
|
|
||||||
"user7": "",
|
|
||||||
"user8": "",
|
|
||||||
"user9": "",
|
|
||||||
"colour": "",
|
|
||||||
"cue": "SF1.04",
|
|
||||||
"revision": 0
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "e457f",
|
|
||||||
"type": "event",
|
|
||||||
"title": "Slovenia",
|
|
||||||
"subtitle": "Disko",
|
|
||||||
"presenter": "LPS",
|
|
||||||
"timeStart": 42000000,
|
|
||||||
"timeEnd": 43200000,
|
|
||||||
"duration": 1200000,
|
|
||||||
"endAction": "none",
|
|
||||||
"timerType": "count-down",
|
|
||||||
"isPublic": true,
|
|
||||||
"skip": false,
|
|
||||||
"note": "SF1.05",
|
|
||||||
"user0": "",
|
|
||||||
"user1": "",
|
|
||||||
"user2": "",
|
|
||||||
"user3": "",
|
|
||||||
"user4": "",
|
|
||||||
"user5": "",
|
|
||||||
"user6": "",
|
|
||||||
"user7": "",
|
|
||||||
"user8": "",
|
|
||||||
"user9": "",
|
|
||||||
"colour": "",
|
|
||||||
"cue": "SF1.05",
|
|
||||||
"revision": 0
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"title": "Lunch break",
|
|
||||||
"type": "block",
|
|
||||||
"id": "01e85"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "1c420",
|
|
||||||
"type": "event",
|
|
||||||
"title": "Ukraine",
|
|
||||||
"subtitle": "Stefania",
|
|
||||||
"presenter": "Kalush Orchestra",
|
|
||||||
"timeStart": 47100000,
|
|
||||||
"timeEnd": 48300000,
|
|
||||||
"duration": 1200000,
|
|
||||||
"endAction": "none",
|
|
||||||
"timerType": "count-down",
|
|
||||||
"isPublic": true,
|
|
||||||
"skip": false,
|
|
||||||
"note": "SF1.06",
|
|
||||||
"user0": "",
|
|
||||||
"user1": "",
|
|
||||||
"user2": "",
|
|
||||||
"user3": "",
|
|
||||||
"user4": "",
|
|
||||||
"user5": "",
|
|
||||||
"user6": "",
|
|
||||||
"user7": "",
|
|
||||||
"user8": "",
|
|
||||||
"user9": "",
|
|
||||||
"colour": "",
|
|
||||||
"cue": "SF1.06",
|
|
||||||
"revision": 0
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "b7737",
|
|
||||||
"type": "event",
|
|
||||||
"title": "Bulgaria",
|
|
||||||
"subtitle": "Intention",
|
|
||||||
"presenter": "Intelligent Music Project",
|
|
||||||
"timeStart": 48600000,
|
|
||||||
"timeEnd": 49800000,
|
|
||||||
"duration": 1200000,
|
|
||||||
"endAction": "none",
|
|
||||||
"timerType": "count-down",
|
|
||||||
"isPublic": true,
|
|
||||||
"skip": false,
|
|
||||||
"note": "SF1.07",
|
|
||||||
"user0": "",
|
|
||||||
"user1": "",
|
|
||||||
"user2": "",
|
|
||||||
"user3": "",
|
|
||||||
"user4": "",
|
|
||||||
"user5": "",
|
|
||||||
"user6": "",
|
|
||||||
"user7": "",
|
|
||||||
"user8": "",
|
|
||||||
"user9": "",
|
|
||||||
"colour": "",
|
|
||||||
"cue": "SF1.07",
|
|
||||||
"revision": 0
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "d3a80",
|
|
||||||
"type": "event",
|
|
||||||
"title": "Netherlands",
|
|
||||||
"subtitle": "De Diepte",
|
|
||||||
"presenter": "S10",
|
|
||||||
"timeStart": 50100000,
|
|
||||||
"timeEnd": 51300000,
|
|
||||||
"duration": 1200000,
|
|
||||||
"endAction": "none",
|
|
||||||
"timerType": "count-down",
|
|
||||||
"isPublic": true,
|
|
||||||
"skip": false,
|
|
||||||
"note": "SF1.08",
|
|
||||||
"user0": "",
|
|
||||||
"user1": "",
|
|
||||||
"user2": "",
|
|
||||||
"user3": "",
|
|
||||||
"user4": "",
|
|
||||||
"user5": "",
|
|
||||||
"user6": "",
|
|
||||||
"user7": "",
|
|
||||||
"user8": "",
|
|
||||||
"user9": "",
|
|
||||||
"colour": "",
|
|
||||||
"cue": "SF1.08",
|
|
||||||
"revision": 0
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "8276c",
|
|
||||||
"type": "event",
|
|
||||||
"title": "Moldova",
|
|
||||||
"subtitle": "Trenuletul",
|
|
||||||
"presenter": "Zdob si Zdub",
|
|
||||||
"timeStart": 51600000,
|
|
||||||
"timeEnd": 52800000,
|
|
||||||
"duration": 1200000,
|
|
||||||
"endAction": "none",
|
|
||||||
"timerType": "count-down",
|
|
||||||
"isPublic": true,
|
|
||||||
"skip": false,
|
|
||||||
"note": "SF1.09",
|
|
||||||
"user0": "",
|
|
||||||
"user1": "",
|
|
||||||
"user2": "",
|
|
||||||
"user3": "",
|
|
||||||
"user4": "",
|
|
||||||
"user5": "",
|
|
||||||
"user6": "",
|
|
||||||
"user7": "",
|
|
||||||
"user8": "",
|
|
||||||
"user9": "",
|
|
||||||
"colour": "",
|
|
||||||
"cue": "SF1.09",
|
|
||||||
"revision": 0
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "2340b",
|
|
||||||
"type": "event",
|
|
||||||
"title": "Portugal",
|
|
||||||
"subtitle": "Saudade Saudade",
|
|
||||||
"presenter": "Maro",
|
|
||||||
"timeStart": 53100000,
|
|
||||||
"timeEnd": 54300000,
|
|
||||||
"duration": 1200000,
|
|
||||||
"endAction": "none",
|
|
||||||
"timerType": "count-down",
|
|
||||||
"isPublic": true,
|
|
||||||
"skip": false,
|
|
||||||
"note": "SF1.10",
|
|
||||||
"user0": "",
|
|
||||||
"user1": "",
|
|
||||||
"user2": "",
|
|
||||||
"user3": "",
|
|
||||||
"user4": "",
|
|
||||||
"user5": "",
|
|
||||||
"user6": "",
|
|
||||||
"user7": "",
|
|
||||||
"user8": "",
|
|
||||||
"user9": "",
|
|
||||||
"colour": "",
|
|
||||||
"cue": "SF1.10",
|
|
||||||
"revision": 0
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"title": "Afternoon break",
|
|
||||||
"type": "block",
|
|
||||||
"id": "cb90b"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "503c4",
|
|
||||||
"type": "event",
|
|
||||||
"title": "Croatia",
|
|
||||||
"subtitle": "Guilty Pleasure",
|
|
||||||
"presenter": "Mia Dimsic",
|
|
||||||
"timeStart": 56100000,
|
|
||||||
"timeEnd": 57300000,
|
|
||||||
"duration": 1200000,
|
|
||||||
"endAction": "none",
|
|
||||||
"timerType": "count-down",
|
|
||||||
"isPublic": true,
|
|
||||||
"skip": false,
|
|
||||||
"note": "SF1.11",
|
|
||||||
"user0": "",
|
|
||||||
"user1": "",
|
|
||||||
"user2": "",
|
|
||||||
"user3": "",
|
|
||||||
"user4": "",
|
|
||||||
"user5": "",
|
|
||||||
"user6": "",
|
|
||||||
"user7": "",
|
|
||||||
"user8": "",
|
|
||||||
"user9": "",
|
|
||||||
"colour": "",
|
|
||||||
"cue": "SF1.11",
|
|
||||||
"revision": 0
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "5e965",
|
|
||||||
"type": "event",
|
|
||||||
"title": "Denmark",
|
|
||||||
"subtitle": "The Show",
|
|
||||||
"presenter": "Reddi",
|
|
||||||
"timeStart": 57600000,
|
|
||||||
"timeEnd": 58800000,
|
|
||||||
"duration": 1200000,
|
|
||||||
"endAction": "none",
|
|
||||||
"timerType": "count-down",
|
|
||||||
"isPublic": true,
|
|
||||||
"skip": false,
|
|
||||||
"note": "SF1.12",
|
|
||||||
"user0": "",
|
|
||||||
"user1": "",
|
|
||||||
"user2": "",
|
|
||||||
"user3": "",
|
|
||||||
"user4": "",
|
|
||||||
"user5": "",
|
|
||||||
"user6": "",
|
|
||||||
"user7": "",
|
|
||||||
"user8": "",
|
|
||||||
"user9": "",
|
|
||||||
"colour": "",
|
|
||||||
"cue": "SF1.12",
|
|
||||||
"revision": 0
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "bab4a",
|
|
||||||
"type": "event",
|
|
||||||
"title": "Austria",
|
|
||||||
"subtitle": "Halo",
|
|
||||||
"presenter": "LUM!X & Pia Maria",
|
|
||||||
"timeStart": 59100000,
|
|
||||||
"timeEnd": 60300000,
|
|
||||||
"duration": 1200000,
|
|
||||||
"endAction": "none",
|
|
||||||
"timerType": "count-down",
|
|
||||||
"isPublic": true,
|
|
||||||
"skip": false,
|
|
||||||
"note": "SF1.13",
|
|
||||||
"user0": "",
|
|
||||||
"user1": "",
|
|
||||||
"user2": "",
|
|
||||||
"user3": "",
|
|
||||||
"user4": "",
|
|
||||||
"user5": "",
|
|
||||||
"user6": "",
|
|
||||||
"user7": "",
|
|
||||||
"user8": "",
|
|
||||||
"user9": "",
|
|
||||||
"colour": "",
|
|
||||||
"cue": "SF1.13",
|
|
||||||
"revision": 0
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "d3eb1",
|
|
||||||
"type": "event",
|
|
||||||
"title": "Greece",
|
|
||||||
"subtitle": "Die Together",
|
|
||||||
"presenter": "Amanda Tenfjord",
|
|
||||||
"timeStart": 60600000,
|
|
||||||
"timeEnd": 61800000,
|
|
||||||
"duration": 1200000,
|
|
||||||
"endAction": "none",
|
|
||||||
"timerType": "count-down",
|
|
||||||
"isPublic": true,
|
|
||||||
"skip": false,
|
|
||||||
"note": "SF1.14",
|
|
||||||
"user0": "",
|
|
||||||
"user1": "",
|
|
||||||
"user2": "",
|
|
||||||
"user3": "",
|
|
||||||
"user4": "",
|
|
||||||
"user5": "",
|
|
||||||
"user6": "",
|
|
||||||
"user7": "",
|
|
||||||
"user8": "",
|
|
||||||
"user9": "",
|
|
||||||
"colour": "",
|
|
||||||
"cue": "SF1.14",
|
|
||||||
"revision": 0
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"project": {
|
|
||||||
"title": "Eurovision Song Contest",
|
|
||||||
"description": "Turin 2022",
|
|
||||||
"publicUrl": "www.getontime.no",
|
|
||||||
"publicInfo": "Rehearsal Schedule - Turin 2022",
|
|
||||||
"backstageUrl": "www.github.com/cpvalente/ontime",
|
|
||||||
"backstageInfo": "Rehearsal Schedule - Turin 2022\nAll performers to wear full costumes for 1st rehearsal"
|
|
||||||
},
|
|
||||||
"settings": {
|
|
||||||
"app": "ontime",
|
|
||||||
"version": "3.0.0-alpha",
|
|
||||||
"serverPort": 4001,
|
|
||||||
"editorKey": null,
|
|
||||||
"operatorKey": null,
|
|
||||||
"timeFormat": "24",
|
|
||||||
"language": "en"
|
|
||||||
},
|
|
||||||
"viewSettings": {
|
|
||||||
"overrideStyles": false,
|
|
||||||
"normalColor": "#ffffffcc",
|
|
||||||
"warningColor": "#FFAB33",
|
|
||||||
"dangerColor": "#ED3333",
|
|
||||||
"endMessage": ""
|
|
||||||
},
|
|
||||||
"aliases": [
|
|
||||||
{
|
|
||||||
"enabled": true,
|
|
||||||
"alias": "test",
|
|
||||||
"pathAndParams": "lower?bg=ff2&text=f00&size=0.6&transition=5"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"userFields": {
|
|
||||||
"user0": "user0",
|
|
||||||
"user1": "user1",
|
|
||||||
"user2": "user2",
|
|
||||||
"user3": "user3",
|
|
||||||
"user4": "user4",
|
|
||||||
"user5": "user5",
|
|
||||||
"user6": "user6",
|
|
||||||
"user7": "user7",
|
|
||||||
"user8": "user8",
|
|
||||||
"user9": "user9"
|
|
||||||
},
|
|
||||||
"osc": {
|
|
||||||
"portIn": 8888,
|
|
||||||
"portOut": 9999,
|
|
||||||
"targetIP": "127.0.0.1",
|
|
||||||
"enabledIn": true,
|
|
||||||
"enabledOut": true,
|
|
||||||
"subscriptions": {
|
|
||||||
"onLoad": [],
|
|
||||||
"onStart": [],
|
|
||||||
"onPause": [],
|
|
||||||
"onStop": [],
|
|
||||||
"onUpdate": [
|
|
||||||
{
|
|
||||||
"id": "10eea",
|
|
||||||
"enabled": true,
|
|
||||||
"message": "/ontime/update/{{timer.current}}"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"onFinish": []
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"http": {
|
|
||||||
"enabledOut": true,
|
|
||||||
"subscriptions": {
|
|
||||||
"onLoad": [],
|
|
||||||
"onStart": [],
|
|
||||||
"onPause": [],
|
|
||||||
"onStop": [],
|
|
||||||
"onUpdate": [],
|
|
||||||
"onFinish": []
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -22,11 +22,7 @@ test('test project file upload', async ({ page }) => {
|
|||||||
await page.getByRole('button', { name: 'Import' }).click();
|
await page.getByRole('button', { name: 'Import' }).click();
|
||||||
|
|
||||||
// asset test events
|
// asset test events
|
||||||
await page.getByPlaceholder('Start').first().click();
|
await page.getByText('Albania').click();
|
||||||
await page.getByText('First test event').click();
|
await page.getByText('Latvia').click();
|
||||||
await page.getByTestId('delay-input').click();
|
await page.getByText('Lithuania').click();
|
||||||
await page.getByText('New start: 10:10').click();
|
|
||||||
await page.getByText('Second test event').click();
|
|
||||||
await page.getByText('Lunch').click();
|
|
||||||
await page.getByText('Third test event').click();
|
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -2,31 +2,21 @@ import { expect, test } from '@playwright/test';
|
|||||||
import fs from 'fs';
|
import fs from 'fs';
|
||||||
|
|
||||||
test('cuesheet displays events and exports csv', async ({ page }) => {
|
test('cuesheet displays events and exports csv', async ({ page }) => {
|
||||||
// ensure elements exist in editor
|
|
||||||
await page.goto('http://localhost:4001/editor');
|
|
||||||
await page.getByText('First test event').click();
|
|
||||||
await page.getByText('Second test event').click();
|
|
||||||
await page.getByText('Third test event').click();
|
|
||||||
await page.getByText('Add timeSubtract timeApplyCancel').click();
|
|
||||||
await page.getByText('Lunch').click();
|
|
||||||
|
|
||||||
// same elements in cuesheet
|
// same elements in cuesheet
|
||||||
await page.goto('http://localhost:4001/cuesheet');
|
await page.goto('http://localhost:4001/cuesheet');
|
||||||
await page.getByText('All about Carlos demo event').click();
|
await page.getByText('Eurovision Song Contest').click();
|
||||||
await page.getByRole('cell', { name: 'First test event' }).click();
|
await page.getByRole('cell', { name: 'Lunch break' }).click();
|
||||||
await page.getByRole('cell', { name: 'Second test event' }).click();
|
await page.getByRole('cell', { name: 'Albania' }).click();
|
||||||
await page.getByRole('cell', { name: 'Third test event' }).click();
|
await page.getByRole('cell', { name: 'Latvia' }).click();
|
||||||
await page.getByRole('cell', { name: '+10 min' }).click();
|
await page.getByRole('cell', { name: 'Lithuania' }).click();
|
||||||
await page.getByRole('cell', { name: 'Lunch' }).click();
|
|
||||||
const downloadPromise = page.waitForEvent('download');
|
|
||||||
await page.getByTestId('cuesheet').getByText('Export').click();
|
|
||||||
await page.getByText('CSV').click();
|
|
||||||
|
|
||||||
// From here we test the CSV download feature
|
// From here we test the CSV download feature
|
||||||
|
const downloadPromise = page.waitForEvent('download');
|
||||||
|
await page.getByTestId('cuesheet').getByText('Export CSV').click();
|
||||||
|
|
||||||
function validateCSV(contents) {
|
function validateCSV(contents: string) {
|
||||||
// We should try to keep this in sync with the implementation over at cuesheetUtils.ts
|
// We should try to keep this in sync with the implementation over at cuesheetUtils.ts
|
||||||
const expectedHeader = ['All about Carlos demo event', 'www.getontime.no'];
|
const expectedHeader = ['Project title: Eurovision Song Contest', 'Project description: Turin 2022'];
|
||||||
const expectedColumns = [
|
const expectedColumns = [
|
||||||
'Time Start',
|
'Time Start',
|
||||||
'Time End',
|
'Time End',
|
||||||
@@ -50,14 +40,15 @@ test('cuesheet displays events and exports csv', async ({ page }) => {
|
|||||||
'user8',
|
'user8',
|
||||||
'user9',
|
'user9',
|
||||||
];
|
];
|
||||||
const expectedValues = ['First test event', 'Second test event', 'Third test event', 'Lunch'];
|
const expectedValues = ['Albania', 'Latvia', 'Lithuania', 'Lunch break'];
|
||||||
|
|
||||||
const allExpected = [...expectedHeader, ...expectedColumns, ...expectedValues];
|
const allExpected = [...expectedHeader, ...expectedColumns, ...expectedValues];
|
||||||
return allExpected.every((value) => contents.includes(value));
|
return allExpected.every((value) => {
|
||||||
|
return contents.toLowerCase().includes(value.toLowerCase());
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const download = await downloadPromise;
|
const download = await downloadPromise;
|
||||||
const contents = await fs.promises.readFile(await download.path(), 'utf-8');
|
const contents = await fs.promises.readFile(await download.path(), 'utf-8');
|
||||||
expect(contents).toContain('All about Carlos demo event');
|
|
||||||
expect(validateCSV(contents)).toBe(true);
|
expect(validateCSV(contents)).toBe(true);
|
||||||
});
|
});
|
||||||
|
|||||||
Vendored
+394
-62
@@ -1,48 +1,15 @@
|
|||||||
{
|
{
|
||||||
"rundown": [
|
"rundown": [
|
||||||
{
|
{
|
||||||
"title": "First test event",
|
"title": "Albania",
|
||||||
"subtitle": "",
|
"subtitle": "Sekret",
|
||||||
"presenter": "",
|
"presenter": "Ronela Hajati",
|
||||||
"note": "",
|
"note": "SF1.01",
|
||||||
"endAction": "none",
|
|
||||||
"timerType": "count-down",
|
|
||||||
"timeStart": 32400000,
|
|
||||||
"timeEnd": 36000000,
|
|
||||||
"duration": 3600000,
|
|
||||||
"isPublic": true,
|
|
||||||
"skip": false,
|
|
||||||
"colour": "",
|
|
||||||
"user0": "",
|
|
||||||
"user1": "",
|
|
||||||
"user2": "",
|
|
||||||
"user3": "",
|
|
||||||
"user4": "",
|
|
||||||
"user5": "",
|
|
||||||
"user6": "",
|
|
||||||
"user7": "",
|
|
||||||
"user8": "",
|
|
||||||
"user9": "",
|
|
||||||
"type": "event",
|
|
||||||
"revision": 4,
|
|
||||||
"id": "aa42f"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"duration": 600000,
|
|
||||||
"type": "delay",
|
|
||||||
"revision": 0,
|
|
||||||
"id": "b1d5a"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"title": "Second test event",
|
|
||||||
"subtitle": "",
|
|
||||||
"presenter": "",
|
|
||||||
"note": "",
|
|
||||||
"endAction": "none",
|
"endAction": "none",
|
||||||
"timerType": "count-down",
|
"timerType": "count-down",
|
||||||
"timeStart": 36000000,
|
"timeStart": 36000000,
|
||||||
"timeEnd": 39600000,
|
"timeEnd": 37200000,
|
||||||
"duration": 3600000,
|
"duration": 1200000,
|
||||||
"isPublic": true,
|
"isPublic": true,
|
||||||
"skip": false,
|
"skip": false,
|
||||||
"colour": "",
|
"colour": "",
|
||||||
@@ -57,24 +24,20 @@
|
|||||||
"user8": "",
|
"user8": "",
|
||||||
"user9": "",
|
"user9": "",
|
||||||
"type": "event",
|
"type": "event",
|
||||||
"revision": 2,
|
"revision": 0,
|
||||||
"id": "d71bc"
|
"id": "32d31",
|
||||||
|
"cue": "SF1.01"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"title": "Lunch",
|
"title": "Latvia",
|
||||||
"type": "block",
|
"subtitle": "Eat Your Salad",
|
||||||
"id": "91682"
|
"presenter": "Citi Zeni",
|
||||||
},
|
"note": "SF1.02",
|
||||||
{
|
|
||||||
"title": "Third test event",
|
|
||||||
"subtitle": "",
|
|
||||||
"presenter": "",
|
|
||||||
"note": "",
|
|
||||||
"endAction": "none",
|
"endAction": "none",
|
||||||
"timerType": "count-down",
|
"timerType": "count-down",
|
||||||
"timeStart": 39600000,
|
"timeStart": 37500000,
|
||||||
"timeEnd": 720000,
|
"timeEnd": 38700000,
|
||||||
"duration": 47520000,
|
"duration": 1200000,
|
||||||
"isPublic": true,
|
"isPublic": true,
|
||||||
"skip": false,
|
"skip": false,
|
||||||
"colour": "",
|
"colour": "",
|
||||||
@@ -89,17 +52,364 @@
|
|||||||
"user8": "",
|
"user8": "",
|
||||||
"user9": "",
|
"user9": "",
|
||||||
"type": "event",
|
"type": "event",
|
||||||
"revision": 2,
|
"revision": 0,
|
||||||
"id": "da5b4"
|
"id": "21cd2",
|
||||||
|
"cue": "SF1.02"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"title": "Lithuania",
|
||||||
|
"subtitle": "Sentimentai",
|
||||||
|
"presenter": "Monika Liu",
|
||||||
|
"note": "SF1.03",
|
||||||
|
"endAction": "none",
|
||||||
|
"timerType": "count-down",
|
||||||
|
"timeStart": 39000000,
|
||||||
|
"timeEnd": 40200000,
|
||||||
|
"duration": 1200000,
|
||||||
|
"isPublic": true,
|
||||||
|
"skip": false,
|
||||||
|
"colour": "",
|
||||||
|
"user0": "",
|
||||||
|
"user1": "",
|
||||||
|
"user2": "",
|
||||||
|
"user3": "",
|
||||||
|
"user4": "",
|
||||||
|
"user5": "",
|
||||||
|
"user6": "",
|
||||||
|
"user7": "",
|
||||||
|
"user8": "",
|
||||||
|
"user9": "",
|
||||||
|
"type": "event",
|
||||||
|
"revision": 0,
|
||||||
|
"id": "0b371",
|
||||||
|
"cue": "SF1.03"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"title": "Switzerland",
|
||||||
|
"subtitle": "Boys Do Cry",
|
||||||
|
"presenter": "Marius Bear",
|
||||||
|
"note": "SF1.04",
|
||||||
|
"endAction": "none",
|
||||||
|
"timerType": "count-down",
|
||||||
|
"timeStart": 40500000,
|
||||||
|
"timeEnd": 41700000,
|
||||||
|
"duration": 1200000,
|
||||||
|
"isPublic": true,
|
||||||
|
"skip": false,
|
||||||
|
"colour": "",
|
||||||
|
"user0": "",
|
||||||
|
"user1": "",
|
||||||
|
"user2": "",
|
||||||
|
"user3": "",
|
||||||
|
"user4": "",
|
||||||
|
"user5": "",
|
||||||
|
"user6": "",
|
||||||
|
"user7": "",
|
||||||
|
"user8": "",
|
||||||
|
"user9": "",
|
||||||
|
"type": "event",
|
||||||
|
"revision": 0,
|
||||||
|
"id": "3cd28",
|
||||||
|
"cue": "SF1.04"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"title": "Slovenia",
|
||||||
|
"subtitle": "Disko",
|
||||||
|
"presenter": "LPS",
|
||||||
|
"note": "SF1.05",
|
||||||
|
"endAction": "none",
|
||||||
|
"timerType": "count-down",
|
||||||
|
"timeStart": 42000000,
|
||||||
|
"timeEnd": 43200000,
|
||||||
|
"duration": 1200000,
|
||||||
|
"isPublic": true,
|
||||||
|
"skip": false,
|
||||||
|
"colour": "",
|
||||||
|
"user0": "",
|
||||||
|
"user1": "",
|
||||||
|
"user2": "",
|
||||||
|
"user3": "",
|
||||||
|
"user4": "",
|
||||||
|
"user5": "",
|
||||||
|
"user6": "",
|
||||||
|
"user7": "",
|
||||||
|
"user8": "",
|
||||||
|
"user9": "",
|
||||||
|
"type": "event",
|
||||||
|
"revision": 0,
|
||||||
|
"id": "e457f",
|
||||||
|
"cue": "SF1.05"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"title": "Lunch break",
|
||||||
|
"type": "block",
|
||||||
|
"id": "01e85"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"title": "Ukraine",
|
||||||
|
"subtitle": "Stefania",
|
||||||
|
"presenter": "Kalush Orchestra",
|
||||||
|
"note": "SF1.06",
|
||||||
|
"endAction": "none",
|
||||||
|
"timerType": "count-down",
|
||||||
|
"timeStart": 47100000,
|
||||||
|
"timeEnd": 48300000,
|
||||||
|
"duration": 1200000,
|
||||||
|
"isPublic": true,
|
||||||
|
"skip": false,
|
||||||
|
"colour": "",
|
||||||
|
"user0": "",
|
||||||
|
"user1": "",
|
||||||
|
"user2": "",
|
||||||
|
"user3": "",
|
||||||
|
"user4": "",
|
||||||
|
"user5": "",
|
||||||
|
"user6": "",
|
||||||
|
"user7": "",
|
||||||
|
"user8": "",
|
||||||
|
"user9": "",
|
||||||
|
"type": "event",
|
||||||
|
"revision": 0,
|
||||||
|
"id": "1c420",
|
||||||
|
"cue": "SF1.06"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"title": "Bulgaria",
|
||||||
|
"subtitle": "Intention",
|
||||||
|
"presenter": "Intelligent Music Project",
|
||||||
|
"note": "SF1.07",
|
||||||
|
"endAction": "none",
|
||||||
|
"timerType": "count-down",
|
||||||
|
"timeStart": 48600000,
|
||||||
|
"timeEnd": 49800000,
|
||||||
|
"duration": 1200000,
|
||||||
|
"isPublic": true,
|
||||||
|
"skip": false,
|
||||||
|
"colour": "",
|
||||||
|
"user0": "",
|
||||||
|
"user1": "",
|
||||||
|
"user2": "",
|
||||||
|
"user3": "",
|
||||||
|
"user4": "",
|
||||||
|
"user5": "",
|
||||||
|
"user6": "",
|
||||||
|
"user7": "",
|
||||||
|
"user8": "",
|
||||||
|
"user9": "",
|
||||||
|
"type": "event",
|
||||||
|
"revision": 0,
|
||||||
|
"id": "b7737",
|
||||||
|
"cue": "SF1.07"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"title": "Netherlands",
|
||||||
|
"subtitle": "De Diepte",
|
||||||
|
"presenter": "S10",
|
||||||
|
"note": "SF1.08",
|
||||||
|
"endAction": "none",
|
||||||
|
"timerType": "count-down",
|
||||||
|
"timeStart": 50100000,
|
||||||
|
"timeEnd": 51300000,
|
||||||
|
"duration": 1200000,
|
||||||
|
"isPublic": true,
|
||||||
|
"skip": false,
|
||||||
|
"colour": "",
|
||||||
|
"user0": "",
|
||||||
|
"user1": "",
|
||||||
|
"user2": "",
|
||||||
|
"user3": "",
|
||||||
|
"user4": "",
|
||||||
|
"user5": "",
|
||||||
|
"user6": "",
|
||||||
|
"user7": "",
|
||||||
|
"user8": "",
|
||||||
|
"user9": "",
|
||||||
|
"type": "event",
|
||||||
|
"revision": 0,
|
||||||
|
"id": "d3a80",
|
||||||
|
"cue": "SF1.08"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"title": "Moldova",
|
||||||
|
"subtitle": "Trenuletul",
|
||||||
|
"presenter": "Zdob si Zdub",
|
||||||
|
"note": "SF1.09",
|
||||||
|
"endAction": "none",
|
||||||
|
"timerType": "count-down",
|
||||||
|
"timeStart": 51600000,
|
||||||
|
"timeEnd": 52800000,
|
||||||
|
"duration": 1200000,
|
||||||
|
"isPublic": true,
|
||||||
|
"skip": false,
|
||||||
|
"colour": "",
|
||||||
|
"user0": "",
|
||||||
|
"user1": "",
|
||||||
|
"user2": "",
|
||||||
|
"user3": "",
|
||||||
|
"user4": "",
|
||||||
|
"user5": "",
|
||||||
|
"user6": "",
|
||||||
|
"user7": "",
|
||||||
|
"user8": "",
|
||||||
|
"user9": "",
|
||||||
|
"type": "event",
|
||||||
|
"revision": 0,
|
||||||
|
"id": "8276c",
|
||||||
|
"cue": "SF1.09"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"title": "Portugal",
|
||||||
|
"subtitle": "Saudade Saudade",
|
||||||
|
"presenter": "Maro",
|
||||||
|
"note": "SF1.10",
|
||||||
|
"endAction": "none",
|
||||||
|
"timerType": "count-down",
|
||||||
|
"timeStart": 53100000,
|
||||||
|
"timeEnd": 54300000,
|
||||||
|
"duration": 1200000,
|
||||||
|
"isPublic": true,
|
||||||
|
"skip": false,
|
||||||
|
"colour": "",
|
||||||
|
"user0": "",
|
||||||
|
"user1": "",
|
||||||
|
"user2": "",
|
||||||
|
"user3": "",
|
||||||
|
"user4": "",
|
||||||
|
"user5": "",
|
||||||
|
"user6": "",
|
||||||
|
"user7": "",
|
||||||
|
"user8": "",
|
||||||
|
"user9": "",
|
||||||
|
"type": "event",
|
||||||
|
"revision": 0,
|
||||||
|
"id": "2340b",
|
||||||
|
"cue": "SF1.10"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"title": "Afternoon break",
|
||||||
|
"type": "block",
|
||||||
|
"id": "cb90b"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"title": "Croatia",
|
||||||
|
"subtitle": "Guilty Pleasure",
|
||||||
|
"presenter": "Mia Dimsic",
|
||||||
|
"note": "SF1.11",
|
||||||
|
"endAction": "none",
|
||||||
|
"timerType": "count-down",
|
||||||
|
"timeStart": 56100000,
|
||||||
|
"timeEnd": 57300000,
|
||||||
|
"duration": 1200000,
|
||||||
|
"isPublic": true,
|
||||||
|
"skip": false,
|
||||||
|
"colour": "",
|
||||||
|
"user0": "",
|
||||||
|
"user1": "",
|
||||||
|
"user2": "",
|
||||||
|
"user3": "",
|
||||||
|
"user4": "",
|
||||||
|
"user5": "",
|
||||||
|
"user6": "",
|
||||||
|
"user7": "",
|
||||||
|
"user8": "",
|
||||||
|
"user9": "",
|
||||||
|
"type": "event",
|
||||||
|
"revision": 0,
|
||||||
|
"id": "503c4",
|
||||||
|
"cue": "SF1.11"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"title": "Denmark",
|
||||||
|
"subtitle": "The Show",
|
||||||
|
"presenter": "Reddi",
|
||||||
|
"note": "SF1.12",
|
||||||
|
"endAction": "none",
|
||||||
|
"timerType": "count-down",
|
||||||
|
"timeStart": 57600000,
|
||||||
|
"timeEnd": 58800000,
|
||||||
|
"duration": 1200000,
|
||||||
|
"isPublic": true,
|
||||||
|
"skip": false,
|
||||||
|
"colour": "",
|
||||||
|
"user0": "",
|
||||||
|
"user1": "",
|
||||||
|
"user2": "",
|
||||||
|
"user3": "",
|
||||||
|
"user4": "",
|
||||||
|
"user5": "",
|
||||||
|
"user6": "",
|
||||||
|
"user7": "",
|
||||||
|
"user8": "",
|
||||||
|
"user9": "",
|
||||||
|
"type": "event",
|
||||||
|
"revision": 0,
|
||||||
|
"id": "5e965",
|
||||||
|
"cue": "SF1.12"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"title": "Austria",
|
||||||
|
"subtitle": "Halo",
|
||||||
|
"presenter": "LUM!X & Pia Maria",
|
||||||
|
"note": "SF1.13",
|
||||||
|
"endAction": "none",
|
||||||
|
"timerType": "count-down",
|
||||||
|
"timeStart": 59100000,
|
||||||
|
"timeEnd": 60300000,
|
||||||
|
"duration": 1200000,
|
||||||
|
"isPublic": true,
|
||||||
|
"skip": false,
|
||||||
|
"colour": "",
|
||||||
|
"user0": "",
|
||||||
|
"user1": "",
|
||||||
|
"user2": "",
|
||||||
|
"user3": "",
|
||||||
|
"user4": "",
|
||||||
|
"user5": "",
|
||||||
|
"user6": "",
|
||||||
|
"user7": "",
|
||||||
|
"user8": "",
|
||||||
|
"user9": "",
|
||||||
|
"type": "event",
|
||||||
|
"revision": 0,
|
||||||
|
"id": "bab4a",
|
||||||
|
"cue": "SF1.13"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"title": "Greece",
|
||||||
|
"subtitle": "Die Together",
|
||||||
|
"presenter": "Amanda Tenfjord",
|
||||||
|
"note": "SF1.14",
|
||||||
|
"endAction": "none",
|
||||||
|
"timerType": "count-down",
|
||||||
|
"timeStart": 60600000,
|
||||||
|
"timeEnd": 61800000,
|
||||||
|
"duration": 1200000,
|
||||||
|
"isPublic": true,
|
||||||
|
"skip": false,
|
||||||
|
"colour": "",
|
||||||
|
"user0": "",
|
||||||
|
"user1": "",
|
||||||
|
"user2": "",
|
||||||
|
"user3": "",
|
||||||
|
"user4": "",
|
||||||
|
"user5": "",
|
||||||
|
"user6": "",
|
||||||
|
"user7": "",
|
||||||
|
"user8": "",
|
||||||
|
"user9": "",
|
||||||
|
"type": "event",
|
||||||
|
"revision": 0,
|
||||||
|
"id": "d3eb1",
|
||||||
|
"cue": "SF1.14"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"project": {
|
"project": {
|
||||||
"title": "All about Carlos demo event",
|
"title": "Eurovision Song Contest",
|
||||||
"description": "Demo event for Ontime",
|
"description": "Turin 2022",
|
||||||
"publicUrl": "www.getontime.no",
|
"publicUrl": "www.getontime.no",
|
||||||
"publicInfo": "WiFi: demoproject \nPassword: ontimeproject",
|
"publicInfo": "Rehearsal Schedule - Turin 2022",
|
||||||
"backstageUrl": "www.getontime.no",
|
"backstageUrl": "www.github.com/cpvalente/ontime",
|
||||||
"backstageInfo": "WiFi: demobackstage\nPassword: ontimeproject"
|
"backstageInfo": "Rehearsal Schedule - Turin 2022\nAll performers to wear full costumes for 1st rehearsal"
|
||||||
},
|
},
|
||||||
"settings": {
|
"settings": {
|
||||||
"app": "ontime",
|
"app": "ontime",
|
||||||
@@ -112,6 +422,11 @@
|
|||||||
},
|
},
|
||||||
"viewSettings": {
|
"viewSettings": {
|
||||||
"overrideStyles": false,
|
"overrideStyles": false,
|
||||||
|
"normalColor": "#ffffffcc",
|
||||||
|
"warningColor": "#FFAB33",
|
||||||
|
"warningThreshold": 120000,
|
||||||
|
"dangerColor": "#ED3333",
|
||||||
|
"dangerThreshold": 60000,
|
||||||
"endMessage": ""
|
"endMessage": ""
|
||||||
},
|
},
|
||||||
"aliases": [
|
"aliases": [
|
||||||
@@ -137,8 +452,25 @@
|
|||||||
"portIn": 8888,
|
"portIn": 8888,
|
||||||
"portOut": 9999,
|
"portOut": 9999,
|
||||||
"targetIP": "127.0.0.1",
|
"targetIP": "127.0.0.1",
|
||||||
"enabledIn": false,
|
"enabledIn": true,
|
||||||
"enabledOut": false,
|
"enabledOut": true,
|
||||||
|
"subscriptions": {
|
||||||
|
"onLoad": [],
|
||||||
|
"onStart": [],
|
||||||
|
"onPause": [],
|
||||||
|
"onStop": [],
|
||||||
|
"onUpdate": [
|
||||||
|
{
|
||||||
|
"id": "10eea",
|
||||||
|
"enabled": true,
|
||||||
|
"message": "/ontime/update/{{timer.current}}"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"onFinish": []
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"http": {
|
||||||
|
"enabledOut": true,
|
||||||
"subscriptions": {
|
"subscriptions": {
|
||||||
"onLoad": [],
|
"onLoad": [],
|
||||||
"onStart": [],
|
"onStart": [],
|
||||||
|
|||||||
@@ -1,25 +0,0 @@
|
|||||||
import fs from 'fs';
|
|
||||||
import path from 'path';
|
|
||||||
|
|
||||||
// haven't been able to get this to work
|
|
||||||
// it is well discussed in the issues, so should be able to find something
|
|
||||||
// https://playwrightsolutions.com/making-a-post/
|
|
||||||
// https://playwright.dev/docs/api/class-apirequestcontext#api-request-context-fetch
|
|
||||||
// the upload is accepted by backend we receive 400 after upload and parse
|
|
||||||
|
|
||||||
export async function uploadTestDb(request) {
|
|
||||||
const filePath = path.resolve('e2e/tests/fixtures/test-db.json');
|
|
||||||
const file = fs.readFileSync(filePath);
|
|
||||||
|
|
||||||
const response = await request.post('http://localhost:4001/ontime/db?onlyRundown=false', {
|
|
||||||
multipart: {
|
|
||||||
file: {
|
|
||||||
fileName: filePath,
|
|
||||||
mimeType: 'application/json',
|
|
||||||
buffer: file,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
return response;
|
|
||||||
}
|
|
||||||
@@ -1,6 +1,10 @@
|
|||||||
import { OntimeRundown } from '../../definitions/core/Rundown.type.js';
|
import { OntimeRundownEntry } from '../../definitions/core/Rundown.type.js';
|
||||||
|
|
||||||
export interface GetRundownCached {
|
type EventId = string;
|
||||||
rundown: OntimeRundown;
|
export type NormalisedRundown = Record<EventId, OntimeRundownEntry>;
|
||||||
|
|
||||||
|
export interface RundownCached {
|
||||||
|
rundown: NormalisedRundown;
|
||||||
|
order: EventId[];
|
||||||
revision: number;
|
revision: number;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,7 +16,6 @@ export type OntimeBaseEvent = {
|
|||||||
export type OntimeDelay = OntimeBaseEvent & {
|
export type OntimeDelay = OntimeBaseEvent & {
|
||||||
type: SupportedEvent.Delay;
|
type: SupportedEvent.Delay;
|
||||||
duration: number;
|
duration: number;
|
||||||
revision: number;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export type OntimeBlock = OntimeBaseEvent & {
|
export type OntimeBlock = OntimeBaseEvent & {
|
||||||
|
|||||||
@@ -48,7 +48,7 @@ export type {
|
|||||||
ProjectFileListResponse,
|
ProjectFileListResponse,
|
||||||
MessageResponse,
|
MessageResponse,
|
||||||
} from './api/ontime-controller/BackendResponse.type.js';
|
} from './api/ontime-controller/BackendResponse.type.js';
|
||||||
export type { GetRundownCached } from './api/rundown-controller/BackendResponse.type.js';
|
export type { RundownCached, NormalisedRundown } from './api/rundown-controller/BackendResponse.type.js';
|
||||||
|
|
||||||
// SERVER RUNTIME
|
// SERVER RUNTIME
|
||||||
export { type Log, LogLevel, type LogMessage, LogOrigin } from './definitions/runtime/Logger.type.js';
|
export { type Log, LogLevel, type LogMessage, LogOrigin } from './definitions/runtime/Logger.type.js';
|
||||||
|
|||||||
+20
-2
@@ -1,5 +1,4 @@
|
|||||||
// runtime utils
|
// runtime utils
|
||||||
export { getFirst, getFirstEvent, getLastEvent, getNext, getPrevious } from './src/rundown-utils/rundownUtils.js';
|
|
||||||
export { validatePlayback } from './src/validate-action/validatePlayback.js';
|
export { validatePlayback } from './src/validate-action/validatePlayback.js';
|
||||||
export { validateTimes } from './src/validate-events/validateEvent.js';
|
export { validateTimes } from './src/validate-events/validateEvent.js';
|
||||||
export { calculateDuration } from './src/validate-events/validateEvent.js';
|
export { calculateDuration } from './src/validate-events/validateEvent.js';
|
||||||
@@ -8,7 +7,23 @@ export { calculateDuration } from './src/validate-events/validateEvent.js';
|
|||||||
export { sanitiseCue } from './src/cue-utils/cueUtils.js';
|
export { sanitiseCue } from './src/cue-utils/cueUtils.js';
|
||||||
export { getCueCandidate } from './src/cue-utils/cueUtils.js';
|
export { getCueCandidate } from './src/cue-utils/cueUtils.js';
|
||||||
export { generateId } from './src/generate-id/generateId.js';
|
export { generateId } from './src/generate-id/generateId.js';
|
||||||
export { getPreviousEvent, swapOntimeEvents } from './src/rundown-utils/rundownUtils.js';
|
export {
|
||||||
|
getFirst,
|
||||||
|
getFirstEvent,
|
||||||
|
getFirstEventNormal,
|
||||||
|
getFirstNormal,
|
||||||
|
getLastEvent,
|
||||||
|
getLastEventNormal,
|
||||||
|
getNext,
|
||||||
|
getNextEvent,
|
||||||
|
getNextEventNormal,
|
||||||
|
getNextNormal,
|
||||||
|
getPrevious,
|
||||||
|
getPreviousEvent,
|
||||||
|
getPreviousEventNormal,
|
||||||
|
getPreviousNormal,
|
||||||
|
swapEventData,
|
||||||
|
} from './src/rundown-utils/rundownUtils.js';
|
||||||
|
|
||||||
// format utils
|
// format utils
|
||||||
export {
|
export {
|
||||||
@@ -35,6 +50,9 @@ export { dayInMs, mts } from './src/timeConstants.js';
|
|||||||
// helpers from externals
|
// helpers from externals
|
||||||
export { deepmerge } from './src/externals/deepmerge.js';
|
export { deepmerge } from './src/externals/deepmerge.js';
|
||||||
|
|
||||||
|
// array utils
|
||||||
|
export { deleteAtIndex, insertAtIndex, reorderArray, sortArrayByProperty } from './src/array-utils/arrayUtils.js';
|
||||||
|
|
||||||
// generic utilities
|
// generic utilities
|
||||||
export { isNumeric } from './src/types/types.js';
|
export { isNumeric } from './src/types/types.js';
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -1,4 +1,4 @@
|
|||||||
import { insertAtIndex, reorderArray, sortArrayByProperty } from '../arrayUtils.js';
|
import { insertAtIndex, reorderArray, sortArrayByProperty } from './arrayUtils.js';
|
||||||
|
|
||||||
describe('insertAtIndex', () => {
|
describe('insertAtIndex', () => {
|
||||||
it('should insert an item at the beginning of the array', () => {
|
it('should insert an item at the beginning of the array', () => {
|
||||||
@@ -58,6 +58,8 @@ export function reorderArray<T>(array: T[], fromIndex: number, toIndex: number)
|
|||||||
|
|
||||||
export const sortArrayByProperty = <T>(arr: T[], property: string): T[] => {
|
export const sortArrayByProperty = <T>(arr: T[], property: string): T[] => {
|
||||||
return [...arr].sort((a, b) => {
|
return [...arr].sort((a, b) => {
|
||||||
|
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||||
|
// @ts-ignore -- its ok
|
||||||
return a[property] - b[property];
|
return a[property] - b[property];
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
@@ -1,28 +1,6 @@
|
|||||||
import { generateId } from './generateId.js';
|
import { generateId } from './generateId.js';
|
||||||
|
|
||||||
test('generate a valid 5 digit id', () => {
|
test('generate a valid 6 digit id', () => {
|
||||||
const id = generateId();
|
const id = generateId();
|
||||||
expect(id.length).toBe(5);
|
expect(id.length).toBe(6);
|
||||||
});
|
|
||||||
|
|
||||||
test('generate 100 with less than 110 attempts', () => {
|
|
||||||
const ids = new Set<string>();
|
|
||||||
let attempts = 1;
|
|
||||||
while (ids.size < 100) {
|
|
||||||
ids.add(generateId());
|
|
||||||
attempts++;
|
|
||||||
}
|
|
||||||
|
|
||||||
expect(attempts).toBeLessThan(105);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('generate 1000 with less than 1020 attempts', () => {
|
|
||||||
const ids = new Set<string>();
|
|
||||||
let attempts = 1;
|
|
||||||
while (ids.size < 1000) {
|
|
||||||
ids.add(generateId());
|
|
||||||
attempts++;
|
|
||||||
}
|
|
||||||
|
|
||||||
expect(attempts).toBeLessThan(1020);
|
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { customAlphabet } from 'nanoid';
|
import { customAlphabet } from 'nanoid';
|
||||||
|
|
||||||
const nanoid = customAlphabet('1234567890abcdef', 5);
|
const nanoid = customAlphabet('1234567890abcdef', 6);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Generates a random id from the defined alphabet
|
* Generates a random id from the defined alphabet
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { OntimeRundown, SupportedEvent } from 'ontime-types';
|
import { OntimeEvent, OntimeRundown, SupportedEvent } from 'ontime-types';
|
||||||
|
|
||||||
import { getNext, getNextEvent, getPrevious, getPreviousEvent } from './rundownUtils';
|
import { getNext, getNextEvent, getPrevious, getPreviousEvent, swapEventData } from './rundownUtils';
|
||||||
|
|
||||||
describe('getNext()', () => {
|
describe('getNext()', () => {
|
||||||
it('returns the next event of type event', () => {
|
it('returns the next event of type event', () => {
|
||||||
@@ -149,3 +149,43 @@ describe('getPreviousEvent()', () => {
|
|||||||
expect(previousIndex).toBe(null);
|
expect(previousIndex).toBe(null);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('swapEventData', () => {
|
||||||
|
it('swaps some data between two events', () => {
|
||||||
|
const eventA = {
|
||||||
|
id: '1',
|
||||||
|
cue: 'A',
|
||||||
|
timeStart: 1,
|
||||||
|
timeEnd: 1,
|
||||||
|
duration: 1,
|
||||||
|
delay: 1,
|
||||||
|
} as OntimeEvent;
|
||||||
|
const eventB = {
|
||||||
|
id: '2',
|
||||||
|
cue: 'B',
|
||||||
|
timeStart: 2,
|
||||||
|
timeEnd: 2,
|
||||||
|
duration: 2,
|
||||||
|
delay: 2,
|
||||||
|
} as OntimeEvent;
|
||||||
|
|
||||||
|
const { newA, newB } = swapEventData(eventA, eventB);
|
||||||
|
|
||||||
|
expect(newA).toMatchObject({
|
||||||
|
id: '1',
|
||||||
|
cue: 'B',
|
||||||
|
timeStart: 1,
|
||||||
|
timeEnd: 1,
|
||||||
|
duration: 1,
|
||||||
|
delay: 1,
|
||||||
|
});
|
||||||
|
expect(newB).toMatchObject({
|
||||||
|
id: '2',
|
||||||
|
cue: 'A',
|
||||||
|
timeStart: 2,
|
||||||
|
timeEnd: 2,
|
||||||
|
duration: 2,
|
||||||
|
delay: 2,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { isOntimeEvent, OntimeEvent, OntimeRundown, OntimeRundownEntry } from 'ontime-types';
|
import { isOntimeEvent, NormalisedRundown, OntimeEvent, OntimeRundown, OntimeRundownEntry } from 'ontime-types';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Gets first event in rundown, if it exists
|
* Gets first event in rundown, if it exists
|
||||||
@@ -9,6 +9,17 @@ export function getFirst(rundown: OntimeRundownEntry[]) {
|
|||||||
return rundown.length ? rundown[0] : null;
|
return rundown.length ? rundown[0] : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets first event in a normalised rundown, if it exists
|
||||||
|
* @param rundown
|
||||||
|
* @param order
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
export function getFirstNormal(rundown: NormalisedRundown, order: string[]) {
|
||||||
|
const firstId = order[0];
|
||||||
|
return rundown[firstId] ?? null;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Gets first scheduled event in rundown, if it exists
|
* Gets first scheduled event in rundown, if it exists
|
||||||
* @param {OntimeRundownEntry[]} rundown
|
* @param {OntimeRundownEntry[]} rundown
|
||||||
@@ -27,6 +38,34 @@ export function getFirstEvent(rundown: OntimeRundownEntry[]): {
|
|||||||
return { firstEvent: null, firstIndex: null };
|
return { firstEvent: null, firstIndex: null };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets first scheduled event in a normalised rundown, if it exists
|
||||||
|
* @param rundown
|
||||||
|
* @param order
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
export function getFirstEventNormal(
|
||||||
|
rundown: NormalisedRundown,
|
||||||
|
order: string[],
|
||||||
|
): {
|
||||||
|
firstEvent: OntimeEvent | null;
|
||||||
|
firstIndex: number | null;
|
||||||
|
} {
|
||||||
|
for (let i = 0; i < order.length; i++) {
|
||||||
|
const firstId = order[i];
|
||||||
|
const firstEvent = rundown[firstId];
|
||||||
|
if (isOntimeEvent(firstEvent)) {
|
||||||
|
return { firstEvent, firstIndex: i };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return { firstEvent: null, firstIndex: null };
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets last scheduled event in rundown, if it exists
|
||||||
|
* @param {OntimeRundownEntry[]} rundown
|
||||||
|
* @return {{ firstEvent: OntimeEvent | null; firstIndex: number | null } }
|
||||||
|
*/
|
||||||
export function getLastEvent(rundown: OntimeRundown): {
|
export function getLastEvent(rundown: OntimeRundown): {
|
||||||
lastEvent: OntimeEvent | null;
|
lastEvent: OntimeEvent | null;
|
||||||
lastIndex: number | null;
|
lastIndex: number | null;
|
||||||
@@ -45,7 +84,34 @@ export function getLastEvent(rundown: OntimeRundown): {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Gets next event in rundown, if it exists
|
* Gets last scheduled event in a normalised rundown, if it exists
|
||||||
|
* @param rundown
|
||||||
|
* @param order
|
||||||
|
* @return {{ firstEvent: OntimeEvent | null; firstIndex: number | null } }
|
||||||
|
*/
|
||||||
|
export function getLastEventNormal(
|
||||||
|
rundown: NormalisedRundown,
|
||||||
|
order: string[],
|
||||||
|
): {
|
||||||
|
lastEvent: OntimeEvent | null;
|
||||||
|
lastIndex: number | null;
|
||||||
|
} {
|
||||||
|
if (order.length < 1) {
|
||||||
|
return { lastEvent: null, lastIndex: null };
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let i = order.length - 1; i > 0; i--) {
|
||||||
|
const lastId = order[i];
|
||||||
|
const lastEvent = rundown[lastId];
|
||||||
|
if (isOntimeEvent(lastEvent)) {
|
||||||
|
return { lastEvent, lastIndex: i };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return { lastEvent: null, lastIndex: null };
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets next entry in rundown, if it exists
|
||||||
* @param {OntimeRundownEntry[]} rundown
|
* @param {OntimeRundownEntry[]} rundown
|
||||||
* @param {string} currentId
|
* @param {string} currentId
|
||||||
* @return {{ nextEvent: OntimeRundownEntry | null; nextIndex: number | null } }
|
* @return {{ nextEvent: OntimeRundownEntry | null; nextIndex: number | null } }
|
||||||
@@ -64,6 +130,29 @@ export function getNext(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets next entry in rundown, if it exists
|
||||||
|
* @param rundown
|
||||||
|
* @param order
|
||||||
|
* @param currentId
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
export function getNextNormal(
|
||||||
|
rundown: NormalisedRundown,
|
||||||
|
order: string[],
|
||||||
|
currentId: string,
|
||||||
|
): { nextEvent: OntimeRundownEntry | null; nextIndex: number | null } {
|
||||||
|
const index = order.findIndex((id) => id === currentId);
|
||||||
|
if (index !== -1 && index + 1 < order.length) {
|
||||||
|
const nextIndex = index + 1;
|
||||||
|
const nextId = order[nextIndex];
|
||||||
|
const nextEvent = rundown[nextId];
|
||||||
|
return { nextEvent, nextIndex };
|
||||||
|
} else {
|
||||||
|
return { nextEvent: null, nextIndex: null };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Gets next scheduled event in rundown, if it exists
|
* Gets next scheduled event in rundown, if it exists
|
||||||
* @param {OntimeRundownEntry[]} rundown
|
* @param {OntimeRundownEntry[]} rundown
|
||||||
@@ -89,7 +178,34 @@ export function getNextEvent(
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Gets previous event in rundown, if it exists
|
* Gets next scheduled event in a normalised rundown, if it exists
|
||||||
|
* @param rundown
|
||||||
|
* @param order
|
||||||
|
* @param {string} currentId
|
||||||
|
* @return {{ nextEvent: OntimeEvent | null; nextIndex: number | null } }
|
||||||
|
*/
|
||||||
|
export function getNextEventNormal(
|
||||||
|
rundown: NormalisedRundown,
|
||||||
|
order: string[],
|
||||||
|
currentId: string,
|
||||||
|
): { nextEvent: OntimeEvent | null; nextIndex: number | null } {
|
||||||
|
const index = order.findIndex((id) => id === currentId);
|
||||||
|
if (index < 0) {
|
||||||
|
return { nextEvent: null, nextIndex: null };
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let i = index + 1; i < order.length; i++) {
|
||||||
|
const nextId = order[i];
|
||||||
|
const nextEvent = rundown[nextId];
|
||||||
|
if (isOntimeEvent(nextEvent)) {
|
||||||
|
return { nextEvent, nextIndex: i };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return { nextEvent: null, nextIndex: null };
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets previous entry in rundown, if it exists
|
||||||
* @param {OntimeRundownEntry[]} rundown
|
* @param {OntimeRundownEntry[]} rundown
|
||||||
* @param {string} currentId
|
* @param {string} currentId
|
||||||
* @return {{ previousEvent: OntimeRundownEntry | null; previousIndex: number | null } }
|
* @return {{ previousEvent: OntimeRundownEntry | null; previousIndex: number | null } }
|
||||||
@@ -108,6 +224,29 @@ export function getPrevious(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets previous entry in a nornalised rundown, if it exists
|
||||||
|
* @param rundown
|
||||||
|
* @param order
|
||||||
|
* @param {string} currentId
|
||||||
|
* @return {{ previousEvent: OntimeRundownEntry | null; previousIndex: number | null } }
|
||||||
|
*/
|
||||||
|
export function getPreviousNormal(
|
||||||
|
rundown: NormalisedRundown,
|
||||||
|
order: string[],
|
||||||
|
currentId: string,
|
||||||
|
): { previousEvent: OntimeRundownEntry | null; previousIndex: number | null } {
|
||||||
|
const index = order.findIndex((id) => id === currentId);
|
||||||
|
if (index !== -1 && index - 1 >= 0) {
|
||||||
|
const previousIndex = index - 1;
|
||||||
|
const previousId = order[previousIndex];
|
||||||
|
const previousEvent = rundown[previousId];
|
||||||
|
return { previousEvent, previousIndex };
|
||||||
|
} else {
|
||||||
|
return { previousEvent: null, previousIndex: null };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Gets previous scheduled event in rundown, if it exists
|
* Gets previous scheduled event in rundown, if it exists
|
||||||
* @param {OntimeRundownEntry[]} rundown
|
* @param {OntimeRundownEntry[]} rundown
|
||||||
@@ -132,41 +271,54 @@ export function getPreviousEvent(
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @description swaps two OntimeEvents in the rundown
|
* Gets previous scheduled event in a normalised rundown, if it exists
|
||||||
* @param {OntimeRundown} rundown
|
* @param rundown
|
||||||
* @param {number} fromEventIndex
|
* @param order
|
||||||
* @param {number} toEventIndex
|
* @param {string} currentId
|
||||||
* @returns {OntimeRundown}
|
* @return {{ previousEvent: OntimeRundownEntry | null; previousIndex: number | null } }
|
||||||
*/
|
*/
|
||||||
export const swapOntimeEvents = (
|
export function getPreviousEventNormal(
|
||||||
rundown: OntimeRundown,
|
rundown: NormalisedRundown,
|
||||||
fromEventIndex: number,
|
order: string[],
|
||||||
toEventIndex: number,
|
currentId: string,
|
||||||
): OntimeRundown => {
|
): { previousEvent: OntimeEvent | null; previousIndex: number | null } {
|
||||||
const updatedRundown = [...rundown];
|
const index = order.findIndex((id) => id === currentId);
|
||||||
|
if (index < 0) {
|
||||||
if (fromEventIndex < 0 || toEventIndex < 0) {
|
return { previousEvent: null, previousIndex: null };
|
||||||
throw new Error('ID not found at index');
|
|
||||||
}
|
}
|
||||||
|
for (let i = index - 1; i >= 0; i--) {
|
||||||
|
const previousId = order[i];
|
||||||
|
const previousEvent = rundown[previousId];
|
||||||
|
if (isOntimeEvent(previousEvent)) {
|
||||||
|
return { previousEvent, previousIndex: i };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return { previousEvent: null, previousIndex: null };
|
||||||
|
}
|
||||||
|
|
||||||
const fromEvent = updatedRundown.at(fromEventIndex) as OntimeEvent;
|
/**
|
||||||
const toEvent = updatedRundown.at(toEventIndex) as OntimeEvent;
|
* @description swaps two OntimeEvents in the rundown
|
||||||
|
* @param {OntimeEvent} eventA
|
||||||
updatedRundown[fromEventIndex] = {
|
* @param {OntimeEvent} eventB
|
||||||
...toEvent,
|
*/
|
||||||
timeStart: fromEvent.timeStart,
|
export const swapEventData = (eventA: OntimeEvent, eventB: OntimeEvent): { newA: OntimeEvent; newB: OntimeEvent } => {
|
||||||
timeEnd: fromEvent.timeEnd,
|
const newA = {
|
||||||
duration: fromEvent.duration,
|
...eventB,
|
||||||
delay: fromEvent.delay,
|
id: eventA.id,
|
||||||
|
timeStart: eventA.timeStart,
|
||||||
|
timeEnd: eventA.timeEnd,
|
||||||
|
duration: eventA.duration,
|
||||||
|
delay: eventA.delay,
|
||||||
};
|
};
|
||||||
|
|
||||||
updatedRundown[toEventIndex] = {
|
const newB = {
|
||||||
...fromEvent,
|
...eventA,
|
||||||
timeStart: toEvent.timeStart,
|
id: eventB.id,
|
||||||
timeEnd: toEvent.timeEnd,
|
timeStart: eventB.timeStart,
|
||||||
duration: toEvent.duration,
|
timeEnd: eventB.timeEnd,
|
||||||
delay: toEvent.delay,
|
duration: eventB.duration,
|
||||||
|
delay: eventB.delay,
|
||||||
};
|
};
|
||||||
|
|
||||||
return updatedRundown;
|
return { newA, newB };
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user