mirror of
https://github.com/cpvalente/ontime.git
synced 2026-08-11 10:23:54 +00:00
Compare commits
11 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| ddd629d7db | |||
| 9d2a3d5a12 | |||
| 4bc271208e | |||
| dbd535dd6c | |||
| 57b7fe214a | |||
| b136e2856a | |||
| 03e68c03a9 | |||
| 44048a889d | |||
| f72db62e08 | |||
| 333f2623a5 | |||
| 3ebb3451e4 |
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@getontime/cli",
|
"name": "@getontime/cli",
|
||||||
"version": "3.14.3",
|
"version": "3.14.5",
|
||||||
"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,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "ontime-ui",
|
"name": "ontime-ui",
|
||||||
"version": "3.14.3",
|
"version": "3.14.5",
|
||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
|||||||
@@ -4,10 +4,10 @@ import type { MultiselectOptions, ParamField } from './types';
|
|||||||
|
|
||||||
export const makeOptionsFromCustomFields = (
|
export const makeOptionsFromCustomFields = (
|
||||||
customFields: CustomFields,
|
customFields: CustomFields,
|
||||||
additionalOptions: Record<string, string> = {},
|
additionalOptions: Readonly<Record<string, string>> = {},
|
||||||
filterImageType = true,
|
filterImageType = true,
|
||||||
) => {
|
) => {
|
||||||
const options = structuredClone(additionalOptions);
|
const options = { ...additionalOptions };
|
||||||
for (const [key, value] of Object.entries(customFields)) {
|
for (const [key, value] of Object.entries(customFields)) {
|
||||||
if (filterImageType && value.type === 'image') {
|
if (filterImageType && value.type === 'image') {
|
||||||
continue;
|
continue;
|
||||||
|
|||||||
@@ -48,10 +48,12 @@ export const connectSocket = () => {
|
|||||||
|
|
||||||
websocket.onclose = () => {
|
websocket.onclose = () => {
|
||||||
console.warn('WebSocket disconnected');
|
console.warn('WebSocket disconnected');
|
||||||
setOnlineStatus(false);
|
|
||||||
|
|
||||||
if (shouldReconnect) {
|
if (shouldReconnect) {
|
||||||
reconnectTimeout = setTimeout(() => {
|
reconnectTimeout = setTimeout(() => {
|
||||||
|
if (reconnectAttempts > 2) {
|
||||||
|
setOnlineStatus(false);
|
||||||
|
}
|
||||||
console.warn('WebSocket: attempting reconnect');
|
console.warn('WebSocket: attempting reconnect');
|
||||||
if (websocket && websocket.readyState === WebSocket.CLOSED) {
|
if (websocket && websocket.readyState === WebSocket.CLOSED) {
|
||||||
reconnectAttempts += 1;
|
reconnectAttempts += 1;
|
||||||
|
|||||||
+1
-1
@@ -94,7 +94,7 @@ export default function ReportSettings() {
|
|||||||
})();
|
})();
|
||||||
return (
|
return (
|
||||||
<tr key={entry.index}>
|
<tr key={entry.index}>
|
||||||
<th>{entry.index + 1}</th>
|
<th>{entry.index}</th>
|
||||||
<th>{entry.cue}</th>
|
<th>{entry.cue}</th>
|
||||||
<th>{entry.title}</th>
|
<th>{entry.title}</th>
|
||||||
<th className={cx([start && style[start]])}>{formatTime(entry.scheduledStart)}</th>
|
<th className={cx([start && style[start]])}>{formatTime(entry.scheduledStart)}</th>
|
||||||
|
|||||||
@@ -1,9 +1,7 @@
|
|||||||
import { useEffect, useRef } from 'react';
|
import { PropsWithChildren, useEffect, useRef, useState } from 'react';
|
||||||
import { IoEye, IoEyeOffOutline } from 'react-icons/io5';
|
|
||||||
import { Input } from '@chakra-ui/react';
|
import { Input } from '@chakra-ui/react';
|
||||||
|
|
||||||
import TooltipActionBtn from '../../../common/components/buttons/TooltipActionBtn';
|
import { cx } from '../../../common/utils/styleUtils';
|
||||||
import { tooltipDelayMid } from '../../../ontimeConfig';
|
|
||||||
|
|
||||||
import style from './InputRow.module.scss';
|
import style from './InputRow.module.scss';
|
||||||
|
|
||||||
@@ -12,50 +10,53 @@ interface InputRowProps {
|
|||||||
placeholder: string;
|
placeholder: string;
|
||||||
text: string;
|
text: string;
|
||||||
visible: boolean;
|
visible: boolean;
|
||||||
actionHandler: () => void;
|
|
||||||
changeHandler: (newValue: string) => void;
|
changeHandler: (newValue: string) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function InputRow(props: InputRowProps) {
|
export default function InputRow(props: PropsWithChildren<InputRowProps>) {
|
||||||
const { label, placeholder, text, visible, actionHandler, changeHandler } = props;
|
const { label, placeholder, text, visible, changeHandler, children } = props;
|
||||||
|
|
||||||
|
const [value, setValue] = useState(text);
|
||||||
const inputRef = useRef<HTMLInputElement>(null);
|
const inputRef = useRef<HTMLInputElement>(null);
|
||||||
const cursorPositionRef = useRef(0);
|
const cursorPositionRef = useRef(0);
|
||||||
|
|
||||||
// sync cursor position with text
|
// sync cursor position with text
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (inputRef.current) {
|
if (inputRef.current && inputRef.current !== document.activeElement) {
|
||||||
inputRef.current.selectionStart = cursorPositionRef.current;
|
inputRef.current.selectionStart = cursorPositionRef.current;
|
||||||
inputRef.current.selectionEnd = cursorPositionRef.current;
|
inputRef.current.selectionEnd = cursorPositionRef.current;
|
||||||
}
|
}
|
||||||
}, [text]);
|
}, [text]);
|
||||||
|
|
||||||
|
// synchronise external text
|
||||||
|
useEffect(() => {
|
||||||
|
if (inputRef.current !== document.activeElement) {
|
||||||
|
setValue(text);
|
||||||
|
}
|
||||||
|
}, [text]);
|
||||||
|
|
||||||
const handleInputChange = (event: React.ChangeEvent<HTMLInputElement>) => {
|
const handleInputChange = (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
cursorPositionRef.current = event.target.selectionStart ?? 0;
|
cursorPositionRef.current = event.target.selectionStart ?? 0;
|
||||||
|
setValue(event.target.value);
|
||||||
changeHandler(event.target.value);
|
changeHandler(event.target.value);
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={style.inputRow}>
|
<div className={style.inputRow}>
|
||||||
<label className={`${style.label} ${visible ? style.active : ''}`}>{label}</label>
|
<label className={cx([style.label, visible ?? style.active])} htmlFor={label}>
|
||||||
|
{label}
|
||||||
|
</label>
|
||||||
<div className={style.inputItems}>
|
<div className={style.inputItems}>
|
||||||
<Input
|
<Input
|
||||||
|
id={label}
|
||||||
ref={inputRef}
|
ref={inputRef}
|
||||||
size='sm'
|
size='sm'
|
||||||
variant='ontime-filled'
|
variant='ontime-filled'
|
||||||
value={text}
|
value={value}
|
||||||
onChange={handleInputChange}
|
onChange={handleInputChange}
|
||||||
placeholder={placeholder}
|
placeholder={placeholder}
|
||||||
/>
|
/>
|
||||||
<TooltipActionBtn
|
{children}
|
||||||
clickHandler={actionHandler}
|
|
||||||
tooltip={visible ? 'Make invisible' : 'Make visible'}
|
|
||||||
aria-label={`Toggle ${label}`}
|
|
||||||
openDelay={tooltipDelayMid}
|
|
||||||
icon={visible ? <IoEye size='18px' /> : <IoEyeOffOutline size='18px' />}
|
|
||||||
variant={visible ? 'ontime-filled' : 'ontime-subtle'}
|
|
||||||
size='sm'
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,4 +1,8 @@
|
|||||||
|
import { IoEye, IoEyeOffOutline } from 'react-icons/io5';
|
||||||
|
|
||||||
|
import TooltipActionBtn from '../../../common/components/buttons/TooltipActionBtn';
|
||||||
import { setMessage, useExternalMessageInput, useTimerMessageInput } from '../../../common/hooks/useSocket';
|
import { setMessage, useExternalMessageInput, useTimerMessageInput } from '../../../common/hooks/useSocket';
|
||||||
|
import { tooltipDelayMid } from '../../../ontimeConfig';
|
||||||
|
|
||||||
import InputRow from './InputRow';
|
import InputRow from './InputRow';
|
||||||
import TimerControlsPreview from './TimerViewControl';
|
import TimerControlsPreview from './TimerViewControl';
|
||||||
@@ -23,8 +27,17 @@ function TimerMessageInput() {
|
|||||||
text={text}
|
text={text}
|
||||||
visible={visible}
|
visible={visible}
|
||||||
changeHandler={(newValue) => setMessage.timerText(newValue)}
|
changeHandler={(newValue) => setMessage.timerText(newValue)}
|
||||||
actionHandler={() => setMessage.timerVisible(!visible)}
|
>
|
||||||
/>
|
<TooltipActionBtn
|
||||||
|
clickHandler={() => setMessage.timerVisible(!visible)}
|
||||||
|
tooltip={visible ? 'Make invisible' : 'Make visible'}
|
||||||
|
aria-label='Toggle timer message visibility'
|
||||||
|
openDelay={tooltipDelayMid}
|
||||||
|
icon={visible ? <IoEye size='18px' /> : <IoEyeOffOutline size='18px' />}
|
||||||
|
variant={visible ? 'ontime-filled' : 'ontime-subtle'}
|
||||||
|
size='sm'
|
||||||
|
/>
|
||||||
|
</InputRow>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -46,7 +59,16 @@ function ExternalInput() {
|
|||||||
text={text}
|
text={text}
|
||||||
visible={visible}
|
visible={visible}
|
||||||
changeHandler={(newValue) => setMessage.externalText(newValue)}
|
changeHandler={(newValue) => setMessage.externalText(newValue)}
|
||||||
actionHandler={toggleExternal}
|
>
|
||||||
/>
|
<TooltipActionBtn
|
||||||
|
clickHandler={toggleExternal}
|
||||||
|
tooltip={visible ? 'Make invisible' : 'Make visible'}
|
||||||
|
aria-label='Toggle external message visibility'
|
||||||
|
openDelay={tooltipDelayMid}
|
||||||
|
icon={visible ? <IoEye size='18px' /> : <IoEyeOffOutline size='18px' />}
|
||||||
|
variant={visible ? 'ontime-filled' : 'ontime-subtle'}
|
||||||
|
size='sm'
|
||||||
|
/>
|
||||||
|
</InputRow>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -300,7 +300,8 @@ export default function Rundown({ data }: RundownProps) {
|
|||||||
isNextDay = checkIsNextDay(entry, lastEvent);
|
isNextDay = checkIsNextDay(entry, lastEvent);
|
||||||
if (!isPast) {
|
if (!isPast) {
|
||||||
totalGap += entry.gap;
|
totalGap += entry.gap;
|
||||||
isLinkedToLoaded = isLinkedToLoaded && entry.linkStart !== null;
|
// We also include countToEnd in this test as the behavior of a linked event coming after a countToEnd is simelar to an unlinked event
|
||||||
|
isLinkedToLoaded = isLinkedToLoaded && entry.linkStart !== null && !lastEvent?.countToEnd;
|
||||||
}
|
}
|
||||||
if (isNewLatest(entry, lastEvent)) {
|
if (isNewLatest(entry, lastEvent)) {
|
||||||
// populate previous entry
|
// populate previous entry
|
||||||
|
|||||||
@@ -140,7 +140,7 @@ export const ScheduleProvider = ({
|
|||||||
let selectedEventIndex = events.findIndex((event) => event.id === selectedEventId);
|
let selectedEventIndex = events.findIndex((event) => event.id === selectedEventId);
|
||||||
|
|
||||||
// we want to show the event after the current
|
// we want to show the event after the current
|
||||||
const viewEvents = events.toSpliced(0, selectedEventIndex + 1);
|
const viewEvents = events.slice(selectedEventIndex + 1);
|
||||||
selectedEventIndex = 0;
|
selectedEventIndex = 0;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "ontime-electron",
|
"name": "ontime-electron",
|
||||||
"version": "3.14.3",
|
"version": "3.14.5",
|
||||||
"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",
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
"name": "ontime-server",
|
"name": "ontime-server",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"main": "src/index.ts",
|
"main": "src/index.ts",
|
||||||
"version": "3.14.3",
|
"version": "3.14.5",
|
||||||
"exports": "./src/index.js",
|
"exports": "./src/index.js",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@googleapis/sheets": "^5.0.5",
|
"@googleapis/sheets": "^5.0.5",
|
||||||
|
|||||||
@@ -124,6 +124,6 @@ function setSessionCookie(res: Response, token: string) {
|
|||||||
httpOnly: false, // allow websocket to access cookie
|
httpOnly: false, // allow websocket to access cookie
|
||||||
secure: true,
|
secure: true,
|
||||||
path: '/', // allow cookie to be accessed from any path
|
path: '/', // allow cookie to be accessed from any path
|
||||||
sameSite: 'strict',
|
sameSite: 'none', // allow cookies to be sent in cross-origin requests (e.g., iframes)
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -108,7 +108,7 @@ export async function deleteEvent(eventIds: string[]) {
|
|||||||
const scopedMutation = cache.mutateCache(cache.remove);
|
const scopedMutation = cache.mutateCache(cache.remove);
|
||||||
const { didMutate } = await scopedMutation({ eventIds });
|
const { didMutate } = await scopedMutation({ eventIds });
|
||||||
|
|
||||||
if (didMutate === false) {
|
if (!didMutate) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -146,7 +146,7 @@ export async function editEvent(patch: PatchWithId) {
|
|||||||
const { newEvent, didMutate } = await scopedMutation({ patch, eventId: patch.id });
|
const { newEvent, didMutate } = await scopedMutation({ patch, eventId: patch.id });
|
||||||
|
|
||||||
// short circuit if nothing changed
|
// short circuit if nothing changed
|
||||||
if (didMutate === false) {
|
if (!didMutate) {
|
||||||
return newEvent;
|
return newEvent;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import type { Server } from 'http';
|
|||||||
import { networkInterfaces } from 'os';
|
import { networkInterfaces } from 'os';
|
||||||
import type { AddressInfo } from 'net';
|
import type { AddressInfo } from 'net';
|
||||||
|
|
||||||
import { isDocker, isProduction } from '../externals.js';
|
import { isDocker, isOntimeCloud, isProduction } from '../externals.js';
|
||||||
import { logger } from '../classes/Logger.js';
|
import { logger } from '../classes/Logger.js';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -42,6 +42,10 @@ export function getNetworkInterfaces(): { name: string; address: string }[] {
|
|||||||
* @throws any other server errors will result in a throw
|
* @throws any other server errors will result in a throw
|
||||||
*/
|
*/
|
||||||
export function serverTryDesiredPort(server: Server, desiredPort: number): Promise<number> {
|
export function serverTryDesiredPort(server: Server, desiredPort: number): Promise<number> {
|
||||||
|
if (isOntimeCloud) {
|
||||||
|
return forceCloudPort(server);
|
||||||
|
}
|
||||||
|
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
server.once('error', (error) => {
|
server.once('error', (error) => {
|
||||||
// we should only move ports if we are in a desktop environment
|
// we should only move ports if we are in a desktop environment
|
||||||
@@ -84,6 +88,19 @@ export function serverTryDesiredPort(server: Server, desiredPort: number): Promi
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function forceCloudPort(server: Server): Promise<number> {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
server.listen(4001, '0.0.0.0', () => {
|
||||||
|
const address = server.address();
|
||||||
|
if (!isAddressInfo(address)) {
|
||||||
|
reject(new Error('Unknown port type, unable to proceed'));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
resolve(address.port);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Guard verifies that the given address is a usable AddressInfo object
|
* Guard verifies that the given address is a usable AddressInfo object
|
||||||
*/
|
*/
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "ontime",
|
"name": "ontime",
|
||||||
"version": "3.14.3",
|
"version": "3.14.5",
|
||||||
"description": "Time keeping for live events",
|
"description": "Time keeping for live events",
|
||||||
"keywords": [
|
"keywords": [
|
||||||
"ontime",
|
"ontime",
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ export function insertAtIndex<T>(index: number, item: T, array: T[]): T[] {
|
|||||||
* @param array
|
* @param array
|
||||||
*/
|
*/
|
||||||
export function deleteAtIndex<T>(index: number, array: T[]) {
|
export function deleteAtIndex<T>(index: number, array: T[]) {
|
||||||
return array.toSpliced(index, 1);
|
return array.filter((_, i) => i !== index);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
Reference in New Issue
Block a user