* refactor: align header columns

* refactor: improve scrollbar visibility

* refactor: center align table elements

* refactor: make param elements stateful

* fix: issue with collapsed elements not loosing value

* fix: prevent search params containing multiple alias references

* fix: the issue where a file disappears if it is both migrated and recovered in the same load operation (#1744)

* refactor: disable group action for elements in groups

* refactor: move context menu items into the event element (#1747)

* feat: sheet import new features for v4 (#1730)

* import milestone

* fixup! import milestone

* test: milestone import

* add entries to group

stop on new group or on group-end type

* fixup! add entries to group

* cleanup

* add event target duration

* link start if undefined

* add skip import type

* extract some to the excel paresing functions

* tweaks to presentation

* move file

---------

Co-authored-by: Carlos Valente <carlosvalente@pm.me>

* fix: notify runtimeStore of events bieng groupd

* fix: improve authentication and stage detection in demo

* chore: ship logo with project

* fix: client is referenced by name

* fix: prevent reflow in event editor

* fix: stale render on selected event due to ref mismatch

* Create/Load/Delete multiple rundowns (#1696)

* refactor: restore last loaded rundown

* refactor: initialise rundown in ProjectService

* feat: allow switching rundowns

ensure on coordination between the db object and the working object

server provide list of rundowns

implement switch in the UI

implement delete

implement new rundown button

* fix: render order for floating button

* refactor: appropriate names to service

* refactor: rundown endpoints

* refactor: save last loaded rundown ID

* refactor: rundown management UI

* refactor: emit refetch all on project load

---------

Co-authored-by: Carlos Valente <carlosvalente@pm.me>

* feat: recover single event subscription

* fixup! feat: sheet import new features for v4 (#1730)

* fix: prevent dropping a group inside another

* fixup! refactor: move context menu items into the event element (#1747)

* fix: prevent stale references to custom fields

* fix: propagate updates to all rundowns

* refactor: client rundown metadata (#1728)

* generate metadata in the hook

* move test

* ensure there is always a last element

* use for-loop

* update metadata in useEfect

* fully extract metadata generation

* use direct assignment

* cleanup

---------

Co-authored-by: Carlos Valente <carlosvalente@pm.me>

* refactor: small imporvement and tests for coerce functions (#1752)

* refactor: small imporvement and tests for coerce functions

add test `coerceString`

add test `coerceBoolean`

add test `coerceColour`

* remove old todo

* fix: consistent quick add behaviour

* refactor: create flat rundown with metadata

* fix: show add buttons on top

* feat: allow editing milestones

* refactor: style tweaks to rundown elements

refactor: milestones are full width

refactor: cuesheet header alignment

fix: editor styling in cuesheet

* refactor: virtualise table

* refactor: improve overscan (#1758)

* bump version to 4.0.0-alpha.5

---------

Co-authored-by: Alex Christoffer Rasmussen <ac@omnivox.dk>
This commit is contained in:
Carlos Valente
2025-09-03 15:50:20 +02:00
committed by GitHub
parent 3c41b40c5f
commit ae15f3cdc5
102 changed files with 2950 additions and 2104 deletions
@@ -0,0 +1,161 @@
import { millisToSeconds } from 'ontime-utils';
import {
EntryId,
isOntimeEvent,
isPlayableEvent,
MaybeNumber,
OntimeEvent,
Rundown,
Offset,
TimerState,
TimerType,
} from 'ontime-types';
import { deepEqual } from 'fast-equals';
export function isNewSecond(
previousValue: MaybeNumber | undefined,
currentValue: MaybeNumber | undefined,
direction: TimerType.CountDown | TimerType.CountUp = TimerType.CountDown,
) {
return millisToSeconds(currentValue ?? null, direction) !== millisToSeconds(previousValue ?? null, direction);
}
/**
* Checks whether we should update the clock value
* - we have rolled into a new seconds unit
* this is different from the timer update as it looks at the clock as counting up
*/
export function getShouldClockUpdate(previousUpdate: number, now: number): boolean {
const newSeconds = millisToSeconds(now, TimerType.CountUp) !== millisToSeconds(previousUpdate, TimerType.CountUp);
return newSeconds;
}
/**
* Checks whether we should update the timer value
* - we have rolled into a new seconds unit
*/
export function getShouldTimerUpdate(previousValue: TimerState | undefined, currentValue: TimerState): boolean {
if (previousValue === undefined) return true;
return (
// current timer value
isNewSecond(previousValue.current, currentValue.current) ||
//secondary timer value, when in pre-roll
isNewSecond(previousValue.secondaryTimer, currentValue.secondaryTimer) ||
// other timer values that could have changed
previousValue.addedTime !== currentValue.addedTime ||
previousValue.duration !== currentValue.duration ||
previousValue.phase !== currentValue.phase ||
previousValue.playback !== currentValue.playback ||
previousValue.startedAt !== currentValue.startedAt
// elapsed - this would be the direct invert of current value so no need to check
// expectedFinish - this will be moved out by the current value going into over time, no need to check
);
}
export function getShouldOffsetUpdate(
previousValue: Offset | undefined,
currentValue: Offset,
didDependencyUpdate: boolean,
): boolean {
if (previousValue === undefined) return true;
if (previousValue.mode !== currentValue.mode) return true;
// absolute, relative, expected*End are ticked with `didDependencyUpdate`
return didDependencyUpdate && !deepEqual(previousValue, currentValue);
}
/**
* finds the previous playable event, if it exists
*/
export function findPreviousPlayableId(playableEventsOrder: EntryId[], currentEventId?: string): EntryId | undefined {
if (!playableEventsOrder.length) {
return;
}
// if there is no event running, go to first
if (!currentEventId) {
return getFirstPlayableId(playableEventsOrder);
}
const currentIndex = playableEventsOrder.findIndex((eventId) => eventId === currentEventId);
if (currentIndex < 1) {
return getFirstPlayableId(playableEventsOrder);
}
return playableEventsOrder.at(currentIndex - 1);
}
/**
* finds the next event playable event, if it exists
*/
export function findNextPlayableId(playableEventsOrder: EntryId[], currentEventId?: string): EntryId | undefined {
if (!playableEventsOrder.length) {
return;
}
// if there is no event running, go to first
if (!currentEventId) {
return getFirstPlayableId(playableEventsOrder);
}
const currentIndex = playableEventsOrder.findIndex((eventId) => eventId === currentEventId);
if (currentIndex === -1 || currentIndex >= playableEventsOrder.length - 1) {
return getFirstPlayableId(playableEventsOrder);
}
return playableEventsOrder.at(currentIndex + 1);
}
/**
* returns next event that matches a given cue
* then loops from the start
*/
export function findNextPlayableWithCue(
rundown: Rundown,
playableEventsOrder: EntryId[],
targetCue: string,
currentEventIndex = 0,
): OntimeEvent | undefined {
const lowerCaseCue = targetCue.toLowerCase();
for (let i = currentEventIndex; i < playableEventsOrder.length; i++) {
const eventId = playableEventsOrder[i];
const event = rundown.entries[eventId];
if (isOntimeEvent(event) && isPlayableEvent(event) && event.cue.toLowerCase() === lowerCaseCue) {
return event;
}
}
for (let i = 0; i < currentEventIndex; i++) {
const eventId = playableEventsOrder[i];
const event = rundown.entries[eventId];
if (isOntimeEvent(event) && isPlayableEvent(event) && event.cue.toLowerCase() === lowerCaseCue) {
return event;
}
}
}
/**
* Utility returns the first playable event in rundown, if it exists
*/
export function getFirstPlayableId(playableOrder: EntryId[]): EntryId | undefined {
return playableOrder.at(0);
}
/**
* This is a utility function to return an event at a given index
* It uses the timedEventOrder so that the index is the same as the one in the UI
*/
export function getEventAtIndex(
rundown: Rundown,
timedEventOrder: EntryId[],
eventIndex: number,
): OntimeEvent | undefined {
const eventId = timedEventOrder[eventIndex];
if (!eventId) {
return undefined;
}
return rundown.entries[eventId] as OntimeEvent | undefined;
}