Compare commits

...

17 Commits

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

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

* fix: update test
2025-09-08 09:49:01 +02:00
arc-alex cc34db775a chore: bump version 2025-09-08 06:51:07 +02:00
37 changed files with 304 additions and 226 deletions
+2 -1
View File
@@ -16,7 +16,8 @@
# Ignore build folders # Ignore build folders
node_modules node_modules
dist **/node_modules
**/dist
# Ignore default volumes created by running docker compose up # Ignore default volumes created by running docker compose up
ontime-db ontime-db
+13
View File
@@ -99,3 +99,16 @@ Other useful commands
- __List running processes__ by running `docker ps` - __List running processes__ by running `docker ps`
- __Kill running process__ by running `docker kill <process-id>` - __Kill running process__ by running `docker kill <process-id>`
## CONTRIBUTION GUIDELINES
If you want to propose changes to the codebase, please reach out before opening a Pull Request.
For new PRs, please follow the following checklist:
* [ ] You have updated and ran unit locally and they are passing. Unit tests are generally created for all utility functions and business logic
* [ ] You have ran code formatting and linting in all your changes
* [ ] The branch is clean and the commits are meaningfully separated and contain descriptive messages
* [ ] The PR body contains description and motivation for the changes
After this checklist is complete, you can request a review from one of the maintainers to get feedback and approval on the changes. \
We will review as soon as possible
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@getontime/cli", "name": "@getontime/cli",
"version": "4.0.0-beta.2", "version": "4.0.0-beta.3",
"author": "Carlos Valente", "author": "Carlos Valente",
"description": "Time keeping for live events", "description": "Time keeping for live events",
"repository": "https://github.com/cpvalente/ontime", "repository": "https://github.com/cpvalente/ontime",
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "ontime-ui", "name": "ontime-ui",
"version": "4.0.0-beta.2", "version": "4.0.0-beta.3",
"private": true, "private": true,
"type": "module", "type": "module",
"dependencies": { "dependencies": {
@@ -11,9 +11,11 @@
} }
.pin { .pin {
margin-top: 0.5rem;
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
gap: 4px;
input { input {
font-size: 4rem; font-size: 4rem;
@@ -1,6 +1,5 @@
import { RefObject, useCallback, useEffect, useRef } from 'react'; import { RefObject, useCallback, useEffect } from 'react';
import { MaybeString } from 'ontime-types';
import { useSelectedEventId } from './useSocket';
function scrollToComponent<ComponentRef extends HTMLElement, ScrollRef extends HTMLElement>( function scrollToComponent<ComponentRef extends HTMLElement, ScrollRef extends HTMLElement>(
componentRef: RefObject<ComponentRef>, componentRef: RefObject<ComponentRef>,
@@ -18,37 +17,26 @@ function scrollToComponent<ComponentRef extends HTMLElement, ScrollRef extends H
scrollRef.current.scrollTo({ top, behavior: 'smooth' }); scrollRef.current.scrollTo({ top, behavior: 'smooth' });
} }
function snapToComponent<ComponentRef extends HTMLElement, ScrollRef extends HTMLElement>(
componentRef: RefObject<ComponentRef>,
scrollRef: RefObject<ScrollRef>,
topOffset: number,
) {
if (!componentRef.current || !scrollRef.current) {
return;
}
const componentRect = componentRef.current.getBoundingClientRect();
const scrollRect = scrollRef.current.getBoundingClientRect();
const top = componentRect.top - scrollRect.top + scrollRef.current.scrollTop - topOffset;
// maintain current x scroll position
scrollRef.current.scrollTo(scrollRef.current.scrollLeft, top);
}
interface UseFollowComponentProps { interface UseFollowComponentProps {
followRef: RefObject<HTMLElement | null>; followRef: RefObject<HTMLElement | null>;
scrollRef: RefObject<HTMLElement | null>; scrollRef: RefObject<HTMLElement | null>;
doFollow: boolean; doFollow: boolean;
topOffset?: number; topOffset?: number;
setScrollFlag?: (newValue: boolean) => void; setScrollFlag?: (newValue: boolean) => void;
followTrigger?: MaybeString; // this would be an entry id or null
} }
export default function useFollowComponent(props: UseFollowComponentProps) { export default function useFollowComponent({
const { followRef, scrollRef, doFollow, topOffset = 100, setScrollFlag } = props; followRef,
scrollRef,
// when cursor moves, view should follow doFollow,
topOffset = 100,
setScrollFlag,
followTrigger,
}: UseFollowComponentProps) {
// when trigger moves, view should follow
useEffect(() => { useEffect(() => {
if (!doFollow) { if (!doFollow || !followTrigger) {
return; return;
} }
@@ -60,16 +48,14 @@ export default function useFollowComponent(props: UseFollowComponentProps) {
setScrollFlag?.(false); setScrollFlag?.(false);
}); });
} }
}, [followTrigger, doFollow, followRef, scrollRef, setScrollFlag, topOffset]);
// eslint-disable-next-line -- the prompt seems incorrect
}, [followRef?.current, scrollRef?.current]);
const scrollToRefComponent = useCallback( const scrollToRefComponent = useCallback(
(componentRef = followRef, containerRef = scrollRef, offset = topOffset) => { (componentRef = followRef, containerRef = scrollRef, offset = topOffset) => {
if (componentRef.current && containerRef.current) { if (componentRef && containerRef) {
// @ts-expect-error -- we know this are not null // @ts-expect-error -- we know this are not null
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
scrollToComponent(componentRef!, scrollRef!, offset); scrollToComponent(componentRef!, containerRef!, offset);
} }
}, },
[followRef, scrollRef, topOffset], [followRef, scrollRef, topOffset],
@@ -77,32 +63,3 @@ export default function useFollowComponent(props: UseFollowComponentProps) {
return scrollToRefComponent; return scrollToRefComponent;
} }
export function useFollowSelected(doFollow: boolean, topOffset = 100) {
const selectedEvenId = useSelectedEventId();
const selectedRef = useRef<HTMLTableRowElement>(null);
const scrollRef = useRef<HTMLDivElement>(null);
useEffect(() => {
if (!doFollow) {
return;
}
if (selectedEvenId && selectedRef.current && scrollRef.current) {
// Use requestAnimationFrame to ensure the component is fully loaded
window.requestAnimationFrame(() => {
snapToComponent(
{ current: selectedRef.current } as RefObject<HTMLElement>,
{ current: scrollRef.current } as RefObject<HTMLElement>,
topOffset,
);
});
}
}, [doFollow, selectedEvenId, topOffset]);
return {
selectedRef,
scrollRef,
};
}
@@ -11,7 +11,6 @@ const createSelector =
export const setClientRemote = { export const setClientRemote = {
setIdentify: (payload: { target: string; identify: boolean }) => sendSocket('client', payload), setIdentify: (payload: { target: string; identify: boolean }) => sendSocket('client', payload),
setRedirect: (payload: { target: string; redirect: string }) => { setRedirect: (payload: { target: string; redirect: string }) => {
console.log('--- got', payload);
sendSocket('client', payload); sendSocket('client', payload);
}, },
setClientName: (payload: { target: string; rename: string }) => sendSocket('client', payload), setClientName: (payload: { target: string; rename: string }) => sendSocket('client', payload),
@@ -1,19 +0,0 @@
export type OverridableOptions = {
keyColour?: string;
textColour?: string;
textBackground?: string;
font?: string;
size?: number;
justifyContent?: 'start' | 'center' | 'end';
alignItems?: 'start' | 'center' | 'end';
left?: string;
top?: string;
hideNav?: boolean;
hideOvertime?: boolean;
hideMessagesOverlay?: boolean;
hideEndMessage?: boolean;
language?: string;
showProgressBar?: boolean;
hideTimerSeconds?: boolean;
removeLeadingZeros?: boolean;
};
@@ -49,6 +49,7 @@ export default function Operator() {
scrollRef, scrollRef,
doFollow: !lockAutoScroll, doFollow: !lockAutoScroll,
topOffset: selectedOffset, topOffset: selectedOffset,
followTrigger: selectedEventId,
}); });
useWindowTitle('Operator'); useWindowTitle('Operator');
@@ -181,7 +182,7 @@ export default function Operator() {
const { isPast, isSelected, isLinkedToLoaded, totalGap } = process(nestedEntry); const { isPast, isSelected, isLinkedToLoaded, totalGap } = process(nestedEntry);
// hide past events (if setting) and skipped events // hide past events (if setting) and skipped events
if (hidePast && isPast) { if ((hidePast && isPast) || nestedEntry.skip) {
return null; return null;
} }
+7 -2
View File
@@ -82,7 +82,12 @@ export default function Rundown({ data, rundownMetadata }: RundownProps) {
const cursorRef = useRef<HTMLDivElement | null>(null); const cursorRef = useRef<HTMLDivElement | null>(null);
const scrollRef = useRef<HTMLDivElement | null>(null); const scrollRef = useRef<HTMLDivElement | null>(null);
useFollowComponent({ followRef: cursorRef, scrollRef, doFollow: editorMode === AppMode.Run }); useFollowComponent({
followRef: cursorRef,
scrollRef,
doFollow: true,
followTrigger: editorMode === AppMode.Edit ? cursor : featureData?.selectedEventId,
});
// DND KIT // DND KIT
const sensors = useSensors(useSensor(PointerSensor, { activationConstraint: { distance: 10 } })); const sensors = useSensors(useSensor(PointerSensor, { activationConstraint: { distance: 10 } }));
@@ -312,7 +317,7 @@ export default function Rundown({ data, rundownMetadata }: RundownProps) {
setMetadata(rundownMetadata); setMetadata(rundownMetadata);
}, [order, entries, rundownMetadata]); }, [order, entries, rundownMetadata]);
// in run mode, we follow selection // in run mode, we follow the playback selection and open groups as needed
useEffect(() => { useEffect(() => {
if (editorMode !== AppMode.Run || !featureData?.selectedEventId) { if (editorMode !== AppMode.Run || !featureData?.selectedEventId) {
return; return;
@@ -28,20 +28,22 @@
padding-inline: 0; padding-inline: 0;
box-shadow: $box-shadow-right; box-shadow: $box-shadow-right;
flex: 1 2 auto; /* flex-grow: 1, flex-shrink: 2, flex-basis: auto */ flex: 1 1 0; /* flex-grow: 1, flex-shrink: 1, flex-basis: 0 */
min-width: 38rem; min-width: 38rem;
max-width: 60rem; max-width: none;
width: 0;
} }
.side { .side {
max-height: 100%; max-height: 100%;
max-width: 45rem;
margin: 0.5rem 0; margin: 0.5rem 0;
padding: 1rem; padding: 1rem;
padding-right: 0; padding-right: 0;
background-color: $gray-1325; background-color: $gray-1325;
border-radius: 0 8px 8px 0; border-radius: 0 8px 8px 0;
flex: 1 1 auto; /* flex-grow: 1, flex-shrink: 1, flex-basis: auto */ flex: 1 1 0; /* flex-grow: 1, flex-shrink: 1, flex-basis: 0 */
max-width: 45rem; // width is locked to swatch picker elements
min-width: calc(15 * 2rem + 13 * 0.5rem);
width: 0;
} }
@@ -41,9 +41,9 @@ export function canDrop(
order?: 'after' | 'before', order?: 'after' | 'before',
isTargetCollapsed?: boolean, isTargetCollapsed?: boolean,
): boolean { ): boolean {
// this would mean inserting a group inside another // inserting before would mean adding a group inside another
if (targetType === 'end-group') { if (targetType === 'end-group') {
return false; return order === 'after';
} }
// this means swapping places with another group // this means swapping places with another group
@@ -1,6 +1,6 @@
import { useMemo, useState } from 'react'; import { useMemo, useState } from 'react';
import { IoAdd } from 'react-icons/io5'; import { IoAdd } from 'react-icons/io5';
import { EntryId, isOntimeEvent, isPlayableEvent, OntimeEvent, OntimeView } from 'ontime-types'; import { isOntimeEvent, isPlayableEvent, OntimeEvent, OntimeView } from 'ontime-types';
import Button from '../../common/components/buttons/Button'; import Button from '../../common/components/buttons/Button';
import Empty from '../../common/components/state/Empty'; import Empty from '../../common/components/state/Empty';
@@ -16,7 +16,7 @@ import { useTranslation } from '../../translation/TranslationProvider';
import Loader from '../common/loader/Loader'; import Loader from '../common/loader/Loader';
import { getCountdownOptions, useCountdownOptions } from './countdown.options'; import { getCountdownOptions, useCountdownOptions } from './countdown.options';
import { getOrderedSubscriptions } from './countdown.utils'; import { CountdownSubscription, getOrderedSubscriptions } from './countdown.utils';
import CountdownSelect from './CountdownSelect'; import CountdownSelect from './CountdownSelect';
import CountdownSubscriptions from './CountdownSubscriptions'; import CountdownSubscriptions from './CountdownSubscriptions';
import SingleEventCountdown from './SingleEventCountdown'; import SingleEventCountdown from './SingleEventCountdown';
@@ -59,6 +59,8 @@ function Countdown({ customFields, rundownData, projectData, isMirrored, setting
[defaultFormat, customFields, subscriptions], [defaultFormat, customFields, subscriptions],
); );
console.log(subscriptions)
return ( return (
<div className={`countdown ${isMirrored ? 'mirror' : ''}`} data-testid='countdown-view'> <div className={`countdown ${isMirrored ? 'mirror' : ''}`} data-testid='countdown-view'>
<ViewParamsEditor target={OntimeView.Countdown} viewOptions={countdownOptions} /> <ViewParamsEditor target={OntimeView.Countdown} viewOptions={countdownOptions} />
@@ -87,7 +89,7 @@ function Countdown({ customFields, rundownData, projectData, isMirrored, setting
interface CountdownContentsProps { interface CountdownContentsProps {
playableEvents: ExtendedEntry<OntimeEvent>[]; playableEvents: ExtendedEntry<OntimeEvent>[];
subscriptions: EntryId[]; subscriptions: CountdownSubscription;
goToEditMode: () => void; goToEditMode: () => void;
} }
@@ -1,5 +1,5 @@
import { useState } from 'react'; import { useState } from 'react';
import { IoArrowBack, IoClose, IoSaveOutline } from 'react-icons/io5'; import { IoArrowBack, IoClose, IoSaveOutline, IoAlbumsOutline } from 'react-icons/io5';
import { useNavigate } from 'react-router'; import { useNavigate } from 'react-router';
import { EntryId, OntimeEvent } from 'ontime-types'; import { EntryId, OntimeEvent } from 'ontime-types';
@@ -7,18 +7,19 @@ import Button from '../../common/components/buttons/Button';
import { cx } from '../../common/utils/styleUtils'; import { cx } from '../../common/utils/styleUtils';
import ClockTime from '../../features/viewers/common/clock-time/ClockTime'; import ClockTime from '../../features/viewers/common/clock-time/ClockTime';
import { makeSubscriptionsUrl } from './countdown.utils'; import { CountdownSubscription, makeSubscriptionsUrl } from './countdown.utils';
import './Countdown.scss'; import './Countdown.scss';
interface CountdownSelectProps { interface CountdownSelectProps {
events: OntimeEvent[]; events: OntimeEvent[];
subscriptions: EntryId[]; subscriptions: CountdownSubscription;
disableEdit: () => void; disableEdit: () => void;
} }
export default function CountdownSelect({ events, subscriptions, disableEdit }: CountdownSelectProps) { export default function CountdownSelect({ events, subscriptions, disableEdit }: CountdownSelectProps) {
const [selected, setSelected] = useState<EntryId[]>(subscriptions); const maybeAllSubscriptions: EntryId[] = subscriptions === 'all' ? events.map((event) => event.id) : subscriptions;
const [selected, setSelected] = useState<EntryId[]>(maybeAllSubscriptions);
const navigate = useNavigate(); const navigate = useNavigate();
/** /**
@@ -47,6 +48,18 @@ export default function CountdownSelect({ events, subscriptions, disableEdit }:
navigate(url.search.toString()); navigate(url.search.toString());
}; };
/**
* Creates a URL with all
* and navigates to it
*/
const applyAll = () => {
// we remove events that no longer exist to avoid stale subscriptions
const url = makeSubscriptionsUrl(window.location.href, 'all');
disableEdit();
setSelected([]);
navigate(url.search.toString());
};
// make a copy of the selected array for quick lookup // make a copy of the selected array for quick lookup
const selectedIds = new Set(selected); const selectedIds = new Set(selected);
@@ -86,6 +99,10 @@ export default function CountdownSelect({ events, subscriptions, disableEdit }:
<Button variant='subtle' size='xlarge' onClick={disableEdit}> <Button variant='subtle' size='xlarge' onClick={disableEdit}>
<IoArrowBack /> Go back <IoArrowBack /> Go back
</Button> </Button>
<Button variant='subtle' size='xlarge' onClick={applyAll}>
{/* TODO: icon ??? */}
<IoAlbumsOutline /> Use All
</Button>
<Button variant='subtle' size='xlarge' onClick={() => setSelected([])} disabled={selected.length === 0}> <Button variant='subtle' size='xlarge' onClick={() => setSelected([])} disabled={selected.length === 0}>
<IoClose /> Clear <IoClose /> Clear
</Button> </Button>
@@ -53,6 +53,7 @@ export default function CountdownSubscriptions({ subscribedEvents, goToEditMode
scrollRef, scrollRef,
doFollow: !lockAutoScroll, doFollow: !lockAutoScroll,
topOffset: 0, topOffset: 0,
followTrigger: selectedEventId,
}); });
// reset scroll if nothing is selected // reset scroll if nothing is selected
@@ -8,11 +8,12 @@ import { ViewOption } from '../../common/components/view-params-editor/viewParam
import { makeOptionsFromCustomFields } from '../../common/components/view-params-editor/viewParams.utils'; import { makeOptionsFromCustomFields } from '../../common/components/view-params-editor/viewParams.utils';
import { PresetContext } from '../../common/context/PresetContext'; import { PresetContext } from '../../common/context/PresetContext';
import { isStringBoolean } from '../../features/viewers/common/viewUtils'; import { isStringBoolean } from '../../features/viewers/common/viewUtils';
import { CountdownSubscription } from './countdown.utils';
export const getCountdownOptions = ( export const getCountdownOptions = (
timeFormat: string, timeFormat: string,
customFields: CustomFields, customFields: CustomFields,
persistedSubscriptions: EntryId[], persistedSubscriptions: CountdownSubscription,
): ViewOption[] => { ): ViewOption[] => {
const secondaryOptions = makeOptionsFromCustomFields(customFields, [ const secondaryOptions = makeOptionsFromCustomFields(customFields, [
{ value: 'none', label: 'None' }, { value: 'none', label: 'None' },
@@ -55,7 +56,7 @@ export const getCountdownOptions = (
id: 'sub', id: 'sub',
title: 'Event subscription', title: 'Event subscription',
description: 'The events to follow', description: 'The events to follow',
values: persistedSubscriptions, values: persistedSubscriptions === 'all' ? ['all'] : persistedSubscriptions,
type: 'persist', type: 'persist',
}, },
], ],
@@ -64,7 +65,7 @@ export const getCountdownOptions = (
}; };
type CountdownOptions = { type CountdownOptions = {
subscriptions: EntryId[]; subscriptions: CountdownSubscription;
secondarySource: keyof OntimeEvent | null; secondarySource: keyof OntimeEvent | null;
showExpected: boolean; showExpected: boolean;
}; };
@@ -85,8 +86,10 @@ function getOptionsFromParams(searchParams: URLSearchParams, defaultValues?: URL
return searchParams.getAll(key) as EntryId[]; return searchParams.getAll(key) as EntryId[];
}; };
const subscriptions = getArrayValues('sub');
return { return {
subscriptions: getArrayValues('sub'), subscriptions: subscriptions.at(0) === 'all' ? 'all' : subscriptions,
secondarySource: getValue('secondary-src') as keyof OntimeEvent | null, secondarySource: getValue('secondary-src') as keyof OntimeEvent | null,
showExpected: isStringBoolean(getValue('showExpected')), showExpected: isStringBoolean(getValue('showExpected')),
}; };
@@ -14,6 +14,8 @@ export function sanitiseTitle(title: string | null) {
return title ?? '{no title}'; return title ?? '{no title}';
} }
export type CountdownSubscription = EntryId[] | 'all';
export const preferredFormat12 = 'h:mm a'; export const preferredFormat12 = 'h:mm a';
export const preferredFormat24 = 'HH:mm'; export const preferredFormat24 = 'HH:mm';
@@ -120,7 +122,7 @@ export function useSubscriptionDisplayData(
/** /**
* Adds a set of subscriptions to the URL parameters * Adds a set of subscriptions to the URL parameters
*/ */
export function makeSubscriptionsUrl(urlRef: string, subscriptions: EntryId[]) { export function makeSubscriptionsUrl(urlRef: string, subscriptions: CountdownSubscription) {
const url = new URL(urlRef); const url = new URL(urlRef);
const newParams = new URLSearchParams(); const newParams = new URLSearchParams();
@@ -131,10 +133,14 @@ export function makeSubscriptionsUrl(urlRef: string, subscriptions: EntryId[]) {
} }
} }
// add new subscriptions if (subscriptions === 'all') {
subscriptions.forEach((id) => { newParams.append('sub', 'all');
newParams.append('sub', id); } else {
}); // add new subscriptions
subscriptions.forEach((id) => {
newParams.append('sub', id);
});
}
url.search = newParams.toString(); url.search = newParams.toString();
@@ -146,38 +152,14 @@ export function makeSubscriptionsUrl(urlRef: string, subscriptions: EntryId[]) {
* Since the original array is already ordered, we simply filter out the events * Since the original array is already ordered, we simply filter out the events
* which are not in the subscriptions list. * which are not in the subscriptions list.
*/ */
export function getOrderedSubscriptions<T extends OntimeEntry>(subscriptions: EntryId[], playableEvents: T[]): T[] { export function getOrderedSubscriptions<T extends OntimeEntry>(
subscriptions: CountdownSubscription,
playableEvents: T[],
): T[] {
if (subscriptions === 'all') return playableEvents;
return playableEvents.filter((event) => subscriptions.includes(event.id)); return playableEvents.filter((event) => subscriptions.includes(event.id));
} }
/**
* Checks through the rundown whether the current event is linked to the loaded event
*/
export function isLinkedToLoadedEvent(events: OntimeEvent[], loadedId: EntryId | null, currentId: EntryId): boolean {
// if nothing is loaded, we return true to simplify the logic
if (!loadedId) {
return true;
}
const loadedIndex = events.findIndex((event) => event.id === loadedId);
if (loadedIndex === -1) {
return true;
}
for (let i = loadedIndex; i < events.length; i++) {
const event = events[i];
if (event.id === currentId) {
return true;
}
if (event.linkStart === null) {
return false;
}
}
return true;
}
export function isOutsideRange(a: number, b: number): boolean { export function isOutsideRange(a: number, b: number): boolean {
return Math.abs(a - b) > MILLIS_PER_MINUTE; return Math.abs(a - b) > MILLIS_PER_MINUTE;
} }
@@ -11,14 +11,14 @@ import {
import { ColumnDef } from '@tanstack/react-table'; import { ColumnDef } from '@tanstack/react-table';
import type { ExtendedEntry } from '../../../common/utils/rundownMetadata'; import type { ExtendedEntry } from '../../../common/utils/rundownMetadata';
import useColumnManager from '../cuesheet-table/useColumnManager'; import { useColumnOrder } from '../cuesheet-table/useColumnManager';
interface CuesheetDndProps { interface CuesheetDndProps {
columns: ColumnDef<ExtendedEntry>[]; columns: ColumnDef<ExtendedEntry>[];
} }
export default function CuesheetDnd({ columns, children }: PropsWithChildren<CuesheetDndProps>) { export default function CuesheetDnd({ columns, children }: PropsWithChildren<CuesheetDndProps>) {
const { columnOrder, saveColumnOrder } = useColumnManager(columns); const { columnOrder, saveColumnOrder } = useColumnOrder(columns);
const sensors = useSensors( const sensors = useSensors(
useSensor(PointerSensor, { useSensor(PointerSensor, {
@@ -13,14 +13,14 @@ import type { ExtendedEntry } from '../../../common/utils/rundownMetadata';
import { AppMode } from '../../../ontimeConfig'; import { AppMode } from '../../../ontimeConfig';
import { usePersistedCuesheetOptions } from '../cuesheet.options'; import { usePersistedCuesheetOptions } from '../cuesheet.options';
import CuesheetHeader from './cuesheet-table-elements/CuesheetHeader'; import { CuesheetHeader, SortableCuesheetHeader } from './cuesheet-table-elements/CuesheetHeader';
import DelayRow from './cuesheet-table-elements/DelayRow'; import DelayRow from './cuesheet-table-elements/DelayRow';
import EventRow from './cuesheet-table-elements/EventRow'; import EventRow from './cuesheet-table-elements/EventRow';
import GroupRow from './cuesheet-table-elements/GroupRow'; import GroupRow from './cuesheet-table-elements/GroupRow';
import MilestoneRow from './cuesheet-table-elements/MilestoneRow'; import MilestoneRow from './cuesheet-table-elements/MilestoneRow';
import CuesheetTableMenu from './cuesheet-table-menu/CuesheetTableMenu'; import CuesheetTableMenu from './cuesheet-table-menu/CuesheetTableMenu';
import CuesheetTableSettings from './cuesheet-table-settings/CuesheetTableSettings'; import CuesheetTableSettings from './cuesheet-table-settings/CuesheetTableSettings';
import useColumnManager from './useColumnManager'; import { useColumnOrder, useColumnSizes, useColumnVisibility } from './useColumnManager';
import style from './CuesheetTable.module.scss'; import style from './CuesheetTable.module.scss';
@@ -79,8 +79,9 @@ export default function CuesheetTable({ columns, cuesheetMode }: CuesheetTablePr
[cuesheetMode, data, hideIndexColumn, hideTableSeconds, showDelayedTimes, updateEntry, updateTimer], [cuesheetMode, data, hideIndexColumn, hideTableSeconds, showDelayedTimes, updateEntry, updateTimer],
); );
const { columnVisibility, columnOrder, columnSizing, resetColumnOrder, setColumnVisibility, setColumnSizing } = const { columnOrder, resetColumnOrder } = useColumnOrder(columns);
useColumnManager(columns); const { columnSizing, setColumnSizing } = useColumnSizes();
const { columnVisibility, setColumnVisibility } = useColumnVisibility();
const table = useReactTable({ const table = useReactTable({
data, data,
@@ -237,11 +238,14 @@ export default function CuesheetTable({ columns, cuesheetMode }: CuesheetTablePr
TableHead: (virtuosoProps) => <thead className={style.tableHeader} {...virtuosoProps} />, TableHead: (virtuosoProps) => <thead className={style.tableHeader} {...virtuosoProps} />,
}} }}
fixedHeaderContent={() => { fixedHeaderContent={() => {
return table return table.getHeaderGroups().map((headerGroup) => {
.getHeaderGroups() const HeaderComponent = table.getState().columnSizingInfo.isResizingColumn
.map((headerGroup) => ( ? CuesheetHeader
<CuesheetHeader key={headerGroup.id} cuesheetMode={cuesheetMode} headerGroup={headerGroup} /> : SortableCuesheetHeader;
));
// if the table is being resized, we render non-sortable headers to avoid performance issues
return <HeaderComponent key={headerGroup.id} cuesheetMode={cuesheetMode} headerGroup={headerGroup} />;
});
}} }}
/> />
@@ -7,7 +7,7 @@ import { getAccessibleColour } from '../../../../common/utils/styleUtils';
import { AppMode } from '../../../../ontimeConfig'; import { AppMode } from '../../../../ontimeConfig';
import { usePersistedCuesheetOptions } from '../../cuesheet.options'; import { usePersistedCuesheetOptions } from '../../cuesheet.options';
import { SortableCell } from './SortableCell'; import { Draggable, SortableCell, TableCell } from './SortableCell';
import style from '../CuesheetTable.module.scss'; import style from '../CuesheetTable.module.scss';
@@ -16,8 +16,9 @@ interface CuesheetHeaderProps {
cuesheetMode: AppMode; cuesheetMode: AppMode;
} }
export default function CuesheetHeader({ headerGroup, cuesheetMode }: CuesheetHeaderProps) { export function SortableCuesheetHeader({ headerGroup, cuesheetMode }: CuesheetHeaderProps) {
const hideIndexColumn = usePersistedCuesheetOptions((state) => state.hideIndexColumn); const hideIndexColumn = usePersistedCuesheetOptions((state) => state.hideIndexColumn);
return ( return (
<tr key={headerGroup.id}> <tr key={headerGroup.id}>
{cuesheetMode === AppMode.Edit && <th className={style.actionColumn} tabIndex={-1} />} {cuesheetMode === AppMode.Edit && <th className={style.actionColumn} tabIndex={-1} />}
@@ -43,8 +44,10 @@ export default function CuesheetHeader({ headerGroup, cuesheetMode }: CuesheetHe
return ( return (
<SortableCell <SortableCell
key={header.column.columnDef.id} key={header.column.columnDef.id}
header={header} columnId={header.column.id}
colSpan={header.colSpan}
injectedStyles={{ width: `calc(var(--header-${header?.id}-size) * 1px)`, ...customStyles }} injectedStyles={{ width: `calc(var(--header-${header?.id}-size) * 1px)`, ...customStyles }}
draggable={<Draggable header={header} />}
> >
{header.isPlaceholder ? null : flexRender(header.column.columnDef.header, header.getContext())} {header.isPlaceholder ? null : flexRender(header.column.columnDef.header, header.getContext())}
</SortableCell> </SortableCell>
@@ -54,3 +57,43 @@ export default function CuesheetHeader({ headerGroup, cuesheetMode }: CuesheetHe
</tr> </tr>
); );
} }
export function CuesheetHeader({ headerGroup, cuesheetMode }: CuesheetHeaderProps) {
const hideIndexColumn = usePersistedCuesheetOptions((state) => state.hideIndexColumn);
return (
<tr key={headerGroup.id}>
{cuesheetMode === AppMode.Edit && <th className={style.actionColumn} tabIndex={-1} />}
{!hideIndexColumn && (
<th className={style.indexColumn} tabIndex={-1}>
#
</th>
)}
{headerGroup.headers.map((header) => {
const customBackground = header.column.columnDef.meta?.colour;
const canWrite = header.column.columnDef.meta?.canWrite;
const customStyles: CSSProperties = {
opacity: canWrite ? 1 : 0.6,
};
if (customBackground) {
const customColour = getAccessibleColour(customBackground);
customStyles.backgroundColor = customColour.backgroundColor;
customStyles.color = customColour.color;
}
return (
<TableCell
key={header.column.columnDef.id}
columnId={header.column.id}
colSpan={header.colSpan}
injectedStyles={{ width: `calc(var(--header-${header?.id}-size) * 1px)`, ...customStyles }}
draggable={<Draggable header={header} />}
>
{header.isPlaceholder ? null : flexRender(header.column.columnDef.header, header.getContext())}
</TableCell>
);
})}
</tr>
);
}
@@ -4,3 +4,7 @@
padding-top: 0.25em; padding-top: 0.25em;
width: 100%; width: 100%;
} }
.multiline {
white-space: break-spaces;
}
@@ -2,6 +2,10 @@ import { PropsWithChildren } from 'react';
import style from './GhostedText.module.scss'; import style from './GhostedText.module.scss';
export default function GhostedText({ children }: PropsWithChildren) { interface GhostedTextProps {
return <div className={style.ghostedText}>{children}</div>; multiline?: boolean;
}
export default function GhostedText({ children, multiline }: PropsWithChildren<GhostedTextProps>) {
return <div className={`${style.ghostedText} ${multiline ? style.multiline : ''}`}>{children}</div>;
} }
@@ -8,16 +8,16 @@ import type { ExtendedEntry } from '../../../../common/utils/rundownMetadata';
import style from '../CuesheetTable.module.scss'; import style from '../CuesheetTable.module.scss';
interface SortableCellProps { interface SortableCellProps {
header: Header<ExtendedEntry, unknown>; columnId: string;
colSpan: number;
injectedStyles: CSSProperties; injectedStyles: CSSProperties;
children: ReactNode; children: ReactNode;
draggable: ReactNode;
} }
export function SortableCell({ header, injectedStyles, children }: SortableCellProps) { export function SortableCell({ columnId, colSpan, injectedStyles, children, draggable }: SortableCellProps) {
const { column, colSpan } = header;
const { attributes, listeners, setNodeRef, transform, transition, isDragging } = useSortable({ const { attributes, listeners, setNodeRef, transform, transition, isDragging } = useSortable({
id: column.id, id: columnId,
}); });
// build drag styles // build drag styles
@@ -34,12 +34,31 @@ export function SortableCell({ header, injectedStyles, children }: SortableCellP
<div {...attributes} {...listeners}> <div {...attributes} {...listeners}>
{children} {children}
</div> </div>
<div {draggable}
onDoubleClick={() => header.column.resetSize()}
onMouseDown={header.getResizeHandler()}
onTouchStart={header.getResizeHandler()}
className={style.resizer}
/>
</th> </th>
); );
} }
export function TableCell({ colSpan, injectedStyles, children, draggable }: SortableCellProps) {
return (
<th style={injectedStyles} colSpan={colSpan} tabIndex={-1}>
<div>{children}</div>
{draggable}
</th>
);
}
interface DraggableProps {
header: Header<ExtendedEntry, unknown>;
}
export function Draggable({ header }: DraggableProps) {
return (
<div
onDoubleClick={() => header.column.resetSize()}
onMouseDown={header.getResizeHandler()}
onTouchStart={header.getResizeHandler()}
className={style.resizer}
/>
);
}
@@ -143,7 +143,7 @@ function MakeMultiLineField({ row, column, table }: CellContext<ExtendedEntry, u
const canWrite = column.columnDef.meta?.canWrite; const canWrite = column.columnDef.meta?.canWrite;
if (!canWrite) { if (!canWrite) {
return <GhostedText>{initialValue}</GhostedText>; return <GhostedText multiline>{initialValue}</GhostedText>;
} }
return <MultiLineCell initialValue={initialValue as string} handleUpdate={update} />; return <MultiLineCell initialValue={initialValue as string} handleUpdate={update} />;
@@ -1,6 +1,6 @@
import { useCallback, useEffect, useState } from 'react'; import { useCallback, useEffect, useState } from 'react';
import { useLocalStorage } from '@mantine/hooks'; import { useLocalStorage } from '@mantine/hooks';
import { ColumnDef } from '@tanstack/react-table'; import { ColumnDef, ColumnSizingState, Updater } from '@tanstack/react-table';
import { debounce } from '../../../common/utils/debounce'; import { debounce } from '../../../common/utils/debounce';
import { makeStageKey } from '../../../common/utils/localStorage'; import { makeStageKey } from '../../../common/utils/localStorage';
@@ -14,17 +14,8 @@ const saveSizesToStorage = debounce((sizes: Record<string, number>) => {
localStorage.setItem(tableSizesKey, JSON.stringify(sizes)); localStorage.setItem(tableSizesKey, JSON.stringify(sizes));
}, 500); }, 500);
export default function useColumnManager(columns: ColumnDef<ExtendedEntry>[]) { export function useColumnSizes() {
const [columnVisibility, setColumnVisibility] = useLocalStorage({ const [columnSizing, setColumnSizingState] = useState<Record<string, number>>(() => {
key: tableHiddenKey,
defaultValue: {},
});
const [columnOrder, saveColumnOrder] = useLocalStorage<string[]>({
key: tableOrderKey,
defaultValue: columns.map((col) => col.id as string),
});
const [columnSizing, setColumnSizingState] = useState(() => {
try { try {
const stored = localStorage.getItem(tableSizesKey); const stored = localStorage.getItem(tableSizesKey);
return stored ? JSON.parse(stored) : {}; return stored ? JSON.parse(stored) : {};
@@ -38,10 +29,22 @@ export default function useColumnManager(columns: ColumnDef<ExtendedEntry>[]) {
saveSizesToStorage(columnSizing); saveSizesToStorage(columnSizing);
}, [columnSizing]); }, [columnSizing]);
const setColumnSizing = useCallback((sizes: typeof columnSizing) => { const setColumnSizing = useCallback((sizesOrUpdater: Updater<ColumnSizingState>) => {
setColumnSizingState(sizes); setColumnSizingState(sizesOrUpdater);
}, []); }, []);
return {
columnSizing,
setColumnSizing,
};
}
export function useColumnOrder(columns: ColumnDef<ExtendedEntry>[]) {
const [columnOrder, saveColumnOrder] = useLocalStorage<string[]>({
key: tableOrderKey,
defaultValue: columns.map((col) => col.id as string),
});
// update column order if columns change // update column order if columns change
useEffect(() => { useEffect(() => {
const newColumns = columns.map((col) => col.id as string); const newColumns = columns.map((col) => col.id as string);
@@ -55,12 +58,20 @@ export default function useColumnManager(columns: ColumnDef<ExtendedEntry>[]) {
}, [columns, saveColumnOrder]); }, [columns, saveColumnOrder]);
return { return {
columnVisibility,
columnOrder, columnOrder,
columnSizing,
resetColumnOrder,
setColumnVisibility,
saveColumnOrder, saveColumnOrder,
setColumnSizing, resetColumnOrder,
};
}
export function useColumnVisibility() {
const [columnVisibility, setColumnVisibility] = useLocalStorage({
key: tableHiddenKey,
defaultValue: {},
});
return {
columnVisibility,
setColumnVisibility,
}; };
} }
@@ -66,4 +66,5 @@
.scrollContainer { .scrollContainer {
max-height: 70vh; max-height: 70vh;
overflow: auto; overflow: auto;
padding-top: 1rem;
} }
@@ -71,8 +71,7 @@ export default function Finder({ isOpen, onClose }: FinderProps) {
results.map((entry, index) => { results.map((entry, index) => {
const isSelected = selected === index; const isSelected = selected === index;
const displayIndex = entry.type === SupportedEntry.Event ? entry.eventIndex : '-'; const displayIndex = entry.type === SupportedEntry.Event ? entry.eventIndex : '-';
const displayCue = entry.type === SupportedEntry.Event ? entry.cue : ''; const displayCue = 'cue' in entry ? entry.cue : '';
const colour = entry.type === SupportedEntry.Event ? entry.colour : '';
return ( return (
<li <li
@@ -83,7 +82,7 @@ export default function Finder({ isOpen, onClose }: FinderProps) {
onClick={submit} onClick={submit}
> >
<div className={style.data}> <div className={style.data}>
<div className={style.index} style={{ '--color': colour }}> <div className={style.index} style={{ '--color': entry.colour }}>
{displayIndex} {displayIndex}
</div> </div>
<div className={style.cue}>{displayCue}</div> <div className={style.cue}>{displayCue}</div>
@@ -99,7 +98,7 @@ export default function Finder({ isOpen, onClose }: FinderProps) {
footerElements={ footerElements={
<div className={style.footer}> <div className={style.footer}>
Use the keywords <span className={style.em}>cue</span>, <span className={style.em}>index</span> or Use the keywords <span className={style.em}>cue</span>, <span className={style.em}>index</span> or
<span className={style.em}>title</span> to filter search <span className={style.em}>title</span> to filter search.
</div> </div>
} }
/> />
@@ -1,6 +1,6 @@
import { ChangeEvent, useCallback, useEffect, useRef, useState } from 'react'; import { ChangeEvent, useCallback, useEffect, useRef, useState } from 'react';
import { useSessionStorage } from '@mantine/hooks'; import { useSessionStorage } from '@mantine/hooks';
import { EntryId, isOntimeEvent, isOntimeGroup, MaybeString, SupportedEntry } from 'ontime-types'; import { EntryId, isOntimeEvent, isOntimeGroup, isOntimeMilestone, MaybeString, SupportedEntry } from 'ontime-types';
import { useFlatRundown } from '../../../common/hooks-query/useRundown'; import { useFlatRundown } from '../../../common/hooks-query/useRundown';
import { useEventSelection } from '../../../features/rundown/useEventSelection'; import { useEventSelection } from '../../../features/rundown/useEventSelection';
@@ -9,14 +9,15 @@ const maxResults = 12;
type FilterableGroup = { type FilterableGroup = {
type: SupportedEntry.Group; type: SupportedEntry.Group;
id: string; id: EntryId;
index: number; index: number;
title: string; title: string;
colour: string;
}; };
type FilterableEvent = { type FilterableEvent = {
type: SupportedEntry.Event; type: SupportedEntry.Event;
id: string; id: EntryId;
index: number; index: number;
eventIndex: number; eventIndex: number;
title: string; title: string;
@@ -25,7 +26,17 @@ type FilterableEvent = {
parent: MaybeString; parent: MaybeString;
}; };
type FilterableEntry = FilterableGroup | FilterableEvent; type FilterableMilestone = {
type: SupportedEntry.Milestone;
id: EntryId;
index: number;
title: string;
cue: string;
colour: string;
parent: MaybeString;
};
type FilterableEntry = FilterableGroup | FilterableEvent | FilterableMilestone;
export default function useFinder() { export default function useFinder() {
const { data, rundownId } = useFlatRundown(); const { data, rundownId } = useFlatRundown();
@@ -59,7 +70,7 @@ export default function useFinder() {
lastSearchString.current = searchValue; lastSearchString.current = searchValue;
if (searchValue.startsWith('index ')) { if (searchValue.startsWith('index ')) {
const searchString = searchValue.replace('index ', '').trim(); const searchString = searchValue.slice('index '.length).trim();
const { results, error } = searchByIndex(searchString); const { results, error } = searchByIndex(searchString);
setResults(results); setResults(results);
setError(error); setError(error);
@@ -67,14 +78,14 @@ export default function useFinder() {
} }
if (searchValue.startsWith('cue ')) { if (searchValue.startsWith('cue ')) {
const searchString = searchValue.replace('cue ', '').trim(); const searchString = searchValue.slice('cue '.length).trim();
const { results, error } = searchByCue(searchString); const { results, error } = searchByCue(searchString);
setResults(results); setResults(results);
setError(error); setError(error);
return; return;
} }
const searchString = searchValue.replace('title ', '').trim(); const searchString = searchValue.startsWith('title ') ? searchValue.slice('title '.length).trim() : searchValue;
const { results, error } = searchByTitle(searchString); const { results, error } = searchByTitle(searchString);
setResults(results); setResults(results);
setError(error); setError(error);
@@ -162,33 +173,46 @@ export default function useFinder() {
break; break;
} }
const event = data[i]; const entry = data[i];
if (isOntimeEvent(event)) { if (isOntimeEvent(entry)) {
if (event.title.toLowerCase().includes(searchString)) { if (entry.title.toLowerCase().includes(searchString)) {
remaining--; remaining--;
results.push({ results.push({
type: SupportedEntry.Event, type: SupportedEntry.Event,
id: event.id, id: entry.id,
index: i, index: i,
eventIndex, eventIndex,
title: event.title, title: entry.title,
cue: event.cue, cue: entry.cue,
colour: event.colour, colour: entry.colour,
parent: event.parent, parent: entry.parent,
} satisfies FilterableEvent); } satisfies FilterableEvent);
} }
eventIndex++; eventIndex++;
} } else if (isOntimeGroup(entry)) {
if (isOntimeGroup(event)) { if (entry.title.toLowerCase().includes(searchString)) {
if (event.title.toLowerCase().includes(searchString)) {
remaining--; remaining--;
results.push({ results.push({
type: SupportedEntry.Group, type: SupportedEntry.Group,
id: event.id, id: entry.id,
index: i, index: i,
title: event.title, title: entry.title,
colour: entry.colour,
} satisfies FilterableGroup); } satisfies FilterableGroup);
} }
} else if (isOntimeMilestone(entry)) {
if (entry.title.toLowerCase().includes(searchString)) {
remaining--;
results.push({
type: SupportedEntry.Milestone,
id: entry.id,
index: i,
title: entry.title,
cue: entry.cue,
colour: entry.colour,
parent: entry.parent,
} satisfies FilterableMilestone);
}
} }
} }
return { results, error: null }; return { results, error: null };
@@ -200,7 +224,7 @@ export default function useFinder() {
const select = useCallback( const select = useCallback(
(selectedEvent: FilterableEntry) => { (selectedEvent: FilterableEntry) => {
// First expand the parent group if this is an event inside a group // First expand the parent group if this is an event inside a group
if (selectedEvent.type === SupportedEntry.Event && selectedEvent.parent !== null) { if ('parent' in selectedEvent && selectedEvent.parent !== null) {
// Try direct state update instead of using callback // Try direct state update instead of using callback
const currentGroups = [...new Set(collapsedGroups)]; const currentGroups = [...new Set(collapsedGroups)];
const newGroups = currentGroups.filter((id) => id !== selectedEvent.parent); const newGroups = currentGroups.filter((id) => id !== selectedEvent.parent);
+3 -3
View File
@@ -68,7 +68,7 @@ function Timer({ customFields, projectData, isMirrored, settings, viewSettings }
hidePhase, hidePhase,
font, font,
keyColour, keyColour,
textColour, timerColour,
} = useTimerOptions(); } = useTimerOptions();
const { getLocalizedString } = useTranslation(); const { getLocalizedString } = useTranslation();
@@ -131,11 +131,11 @@ function Timer({ customFields, projectData, isMirrored, settings, viewSettings }
); );
// gather presentation styles // gather presentation styles
const timerColour = getTimerColour(viewSettings, textColour, showWarning, showDanger); const resolvedTimerColour = getTimerColour(viewSettings, timerColour, showWarning, showDanger);
const { timerFontSize, externalFontSize } = getEstimatedFontSize(display, secondaryContent); const { timerFontSize, externalFontSize } = getEstimatedFontSize(display, secondaryContent);
const userStyles = { const userStyles = {
...(keyColour && { '--timer-bg': keyColour }), ...(keyColour && { '--timer-bg': keyColour }),
...(textColour && { '--timer-colour': timerColour }), ...(resolvedTimerColour && { '--timer-colour': resolvedTimerColour }),
...(font && { '--timer-font': font }), ...(font && { '--timer-font': font }),
}; };
+9 -7
View File
@@ -25,10 +25,12 @@ const timerDisplayOptions: SelectOption[] = [
export const getTimerOptions = (timeFormat: string, customFields: CustomFields): ViewOption[] => { export const getTimerOptions = (timeFormat: string, customFields: CustomFields): ViewOption[] => {
const mainOptions = makeOptionsFromCustomFields(customFields, [ const mainOptions = makeOptionsFromCustomFields(customFields, [
{ value: 'none', label: 'None' },
{ value: 'title', label: 'Title' }, { value: 'title', label: 'Title' },
{ value: 'note', label: 'Note' }, { value: 'note', label: 'Note' },
]); ]);
const secondaryOptions = makeOptionsFromCustomFields(customFields, [ const secondaryOptions = makeOptionsFromCustomFields(customFields, [
{ value: 'none', label: 'None' },
{ value: 'title', label: 'Title' }, { value: 'title', label: 'Title' },
{ value: 'note', label: 'Note' }, { value: 'note', label: 'Note' },
]); ]);
@@ -84,7 +86,7 @@ export const getTimerOptions = (timeFormat: string, customFields: CustomFields):
description: 'Select the data source for the main text', description: 'Select the data source for the main text',
type: 'option', type: 'option',
values: mainOptions, values: mainOptions,
defaultValue: 'Title', defaultValue: 'title',
}, },
{ {
id: 'secondary-src', id: 'secondary-src',
@@ -92,7 +94,7 @@ export const getTimerOptions = (timeFormat: string, customFields: CustomFields):
description: 'Select the data source for the secondary text', description: 'Select the data source for the secondary text',
type: 'option', type: 'option',
values: secondaryOptions, values: secondaryOptions,
defaultValue: '', defaultValue: 'none',
}, },
], ],
}, },
@@ -163,9 +165,9 @@ export const getTimerOptions = (timeFormat: string, customFields: CustomFields):
defaultValue: '101010', defaultValue: '101010',
}, },
{ {
id: 'textColour', id: 'timerColour',
title: 'Text Colour', title: 'Timer Colour',
description: 'Text colour. Default: #f6f6f6', description: 'Timer colour. Default: #f6f6f6',
type: 'colour', type: 'colour',
defaultValue: 'f6f6f6', defaultValue: 'f6f6f6',
}, },
@@ -191,7 +193,7 @@ type TimerOptions = {
hidePhase: boolean; hidePhase: boolean;
font?: string; font?: string;
keyColour?: string; keyColour?: string;
textColour?: string; timerColour?: string;
}; };
/** /**
@@ -226,7 +228,7 @@ function getOptionsFromParams(searchParams: URLSearchParams, defaultValues?: URL
font: getValue('font') ?? undefined, font: getValue('font') ?? undefined,
keyColour: makeColourString(getValue('keyColour')), keyColour: makeColourString(getValue('keyColour')),
textColour: makeColourString(getValue('textColour')), timerColour: makeColourString(getValue('timerColour')),
}; };
} }
+2 -1
View File
@@ -32,7 +32,8 @@
"preserveConstEnums": true, "preserveConstEnums": true,
"allowJs": true, "allowJs": true,
"baseUrl": "src", "baseUrl": "src",
"jsx": "react-jsx" "jsx": "react-jsx",
"outDir": "build"
}, },
"include": [ "include": [
"src" "src"
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "ontime-electron", "name": "ontime-electron",
"version": "4.0.0-beta.2", "version": "4.0.0-beta.3",
"author": "Carlos Valente", "author": "Carlos Valente",
"description": "Time keeping for live events", "description": "Time keeping for live events",
"repository": "https://github.com/cpvalente/ontime", "repository": "https://github.com/cpvalente/ontime",
+1 -1
View File
@@ -2,7 +2,7 @@
"name": "ontime-server", "name": "ontime-server",
"type": "module", "type": "module",
"main": "src/index.ts", "main": "src/index.ts",
"version": "4.0.0-beta.2", "version": "4.0.0-beta.3",
"exports": "./src/index.js", "exports": "./src/index.js",
"dependencies": { "dependencies": {
"@googleapis/sheets": "^5.0.5", "@googleapis/sheets": "^5.0.5",
@@ -10,7 +10,7 @@ export const dataFromExcelTemplate = [
'Timer type', 'Timer type',
'Count to end', 'Count to end',
'Skip', 'Skip',
'Notes', 'Note',
't0', 't0',
'Test1', 'Test1',
'test2', 'test2',
+1 -1
View File
@@ -337,7 +337,7 @@ export const demoDb: DatabaseModel = {
alias: 'minimal', alias: 'minimal',
target: OntimeView.Timer, target: OntimeView.Timer,
search: search:
'hideclock=true&hidecards=true&hideprogress=true&hidemessage=true&hidesecondary=true&hidelogo=true&font=arial+black&keycolour=00ff00&textcolour=ffffff', 'hideclock=true&hidecards=true&hideprogress=true&hidemessage=true&hidesecondary=true&hidelogo=true&font=arial+black&keycolour=00ff00&timerColour=ffffff',
}, },
], ],
customFields: { customFields: {
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "ontime", "name": "ontime",
"version": "4.0.0-beta.2", "version": "4.0.0-beta.3",
"description": "Time keeping for live events", "description": "Time keeping for live events",
"keywords": [ "keywords": [
"ontime", "ontime",
@@ -14,7 +14,7 @@ export const defaultImportMap = {
title: 'title', title: 'title',
countToEnd: 'count to end', countToEnd: 'count to end',
skip: 'skip', skip: 'skip',
note: 'notes', note: 'note',
colour: 'colour', colour: 'colour',
endAction: 'end action', endAction: 'end action',
timerType: 'timer type', timerType: 'timer type',