mirror of
https://github.com/cpvalente/ontime.git
synced 2026-07-26 10:38:55 +00:00
Compare commits
45 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 75cbcd8e85 | |||
| 701f24cece | |||
| 166160dda9 | |||
| 9c5116f197 | |||
| deb1f043c7 | |||
| cbba19a9b1 | |||
| a5daa76616 | |||
| 08b8e73393 | |||
| 2f3cf9825a | |||
| b0f02e726d | |||
| ac40292813 | |||
| 2ac00ee8cc | |||
| 972f9e35fd | |||
| 1d7ee163a0 | |||
| 1054ec0a5e | |||
| fc2e76db0a | |||
| ef3d541eb3 | |||
| 8daf3313f2 | |||
| 93c913ec5a | |||
| 71b2fa8e21 | |||
| f2b78e0f32 | |||
| b70ef80320 | |||
| 426d42ab63 | |||
| 242cefd35c | |||
| 6beda1575a | |||
| ba75d52d29 | |||
| 35a4361ae9 | |||
| 77225383a4 | |||
| f0e81c0cf0 | |||
| 9355d7bf27 | |||
| cd39227af7 | |||
| 14e21214af | |||
| 03184b0321 | |||
| cd5af769ab | |||
| d116670318 | |||
| 9ba5ebd4bd | |||
| 3f1f06f7c5 | |||
| a78586d2fa | |||
| 8e93b0af25 | |||
| 9a5d50d933 | |||
| 1189a94bff | |||
| f128f7acd2 | |||
| 80ec2d1186 | |||
| 18e6f0d7c4 | |||
| 48f2511764 |
@@ -103,6 +103,9 @@ jobs:
|
||||
- name: Release
|
||||
uses: softprops/action-gh-release@v1
|
||||
with:
|
||||
files: './apps/electron/dist/ontime-linux.AppImage'
|
||||
files: |
|
||||
./apps/electron/dist/ontime-linux-x86_64.AppImage
|
||||
./apps/electron/dist/ontime-linux-arm64.AppImage
|
||||
./apps/electron/dist/ontime-linux-armv7l.AppImage
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
@@ -40,6 +40,7 @@ override.css
|
||||
|
||||
# working stuff
|
||||
**/TODO.md
|
||||
**.local.**
|
||||
|
||||
# docker utils
|
||||
ontime-db
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@getontime/cli",
|
||||
"version": "4.2.0",
|
||||
"version": "4.3.0",
|
||||
"author": "Carlos Valente",
|
||||
"description": "Time keeping for live events",
|
||||
"repository": "https://github.com/cpvalente/ontime",
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
{
|
||||
"name": "ontime-ui",
|
||||
"version": "4.2.0",
|
||||
"version": "4.3.0",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"dependencies": {
|
||||
"@base-ui-components/react": "1.0.0-beta.4",
|
||||
"@base-ui/react": "1.0.0",
|
||||
"@dnd-kit/core": "^6.3.1",
|
||||
"@dnd-kit/sortable": "^10.0.0",
|
||||
"@dnd-kit/utilities": "^3.2.2",
|
||||
@@ -17,21 +17,20 @@
|
||||
"@tanstack/react-table": "^8.21.3",
|
||||
"autosize": "^6.0.1",
|
||||
"axios": "^1.12.2",
|
||||
"babel-plugin-react-compiler": "19.1.0-rc.3",
|
||||
"csv-stringify": "^6.6.0",
|
||||
"prismjs": "^1.30.0",
|
||||
"react": "^19.1.1",
|
||||
"react": "^19.2.3",
|
||||
"react-colorful": "^5.6.1",
|
||||
"react-dom": "^19.1.1",
|
||||
"react-dom": "^19.2.3",
|
||||
"react-fast-compare": "^3.2.2",
|
||||
"react-hook-form": "^7.62.0",
|
||||
"react-icons": "5.5.0",
|
||||
"react-qr-code": "^2.0.18",
|
||||
"react-router": "^7.8.2",
|
||||
"react-router": "^7.11.0",
|
||||
"react-simple-code-editor": "^0.14.1",
|
||||
"react-virtuoso": "^4.14.0",
|
||||
"react-virtuoso": "^4.17.0",
|
||||
"web-vitals": "^5.1.0",
|
||||
"zustand": "^5.0.8"
|
||||
"zustand": "^5.0.9"
|
||||
},
|
||||
"scripts": {
|
||||
"addversion": "node -p \"'export const ONTIME_VERSION = ' + JSON.stringify(require('../../package.json').version) + ';'\" > src/ONTIME_VERSION.js",
|
||||
@@ -66,6 +65,7 @@
|
||||
"@typescript-eslint/eslint-plugin": "catalog:",
|
||||
"@typescript-eslint/parser": "catalog:",
|
||||
"@vitejs/plugin-react": "4.5.1",
|
||||
"babel-plugin-react-compiler": "1.0.0",
|
||||
"eslint": "catalog:",
|
||||
"eslint-config-prettier": "catalog:",
|
||||
"eslint-plugin-jest": "^28.6.0",
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { BrowserRouter } from 'react-router';
|
||||
import { Tooltip } from '@base-ui-components/react/tooltip';
|
||||
import { Tooltip } from '@base-ui/react/tooltip';
|
||||
import { QueryClientProvider } from '@tanstack/react-query';
|
||||
import { ReactQueryDevtools } from '@tanstack/react-query-devtools';
|
||||
|
||||
|
||||
@@ -21,7 +21,23 @@ export async function fetchProjectRundownList(): Promise<ProjectRundownsList> {
|
||||
*/
|
||||
export async function fetchCurrentRundown(): Promise<Rundown> {
|
||||
const res = await axios.get(`${rundownPath}/current`);
|
||||
if (!isValidRundown(res.data)) {
|
||||
throw new Error('Invalid rundown payload');
|
||||
}
|
||||
return res.data;
|
||||
|
||||
function isValidRundown(x: any): x is Rundown {
|
||||
return (
|
||||
x &&
|
||||
typeof x === 'object' &&
|
||||
typeof x.id === 'string' &&
|
||||
Array.isArray(x.order) &&
|
||||
Array.isArray(x.flatOrder) &&
|
||||
x.entries &&
|
||||
typeof x.entries === 'object' &&
|
||||
typeof x.revision === 'number'
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -75,7 +91,10 @@ export async function postAddEntry(
|
||||
/**
|
||||
* HTTP request to edit an entry
|
||||
*/
|
||||
export async function putEditEntry(rundownId: RundownId, data: Partial<OntimeEntry>): Promise<AxiosResponse<OntimeEntry>> {
|
||||
export async function putEditEntry(
|
||||
rundownId: RundownId,
|
||||
data: Partial<OntimeEntry>,
|
||||
): Promise<AxiosResponse<OntimeEntry>> {
|
||||
return axios.put(`${rundownPath}/${rundownId}/entry`, data);
|
||||
}
|
||||
|
||||
@@ -107,7 +126,11 @@ export async function patchReorderEntry(rundownId: RundownId, data: ReorderEntry
|
||||
/**
|
||||
* HTTP request to swap two events
|
||||
*/
|
||||
export async function requestEventSwap(rundownId: RundownId, from: EntryId, to: EntryId): Promise<AxiosResponse<Rundown>> {
|
||||
export async function requestEventSwap(
|
||||
rundownId: RundownId,
|
||||
from: EntryId,
|
||||
to: EntryId,
|
||||
): Promise<AxiosResponse<Rundown>> {
|
||||
return axios.patch(`${rundownPath}/${rundownId}/swap`, { from, to });
|
||||
}
|
||||
|
||||
@@ -121,8 +144,12 @@ export async function requestApplyDelay(rundownId: RundownId, delayId: EntryId):
|
||||
/**
|
||||
* HTTP request for cloning an entry
|
||||
*/
|
||||
export async function postCloneEntry(rundownId: RundownId, entryId: EntryId): Promise<AxiosResponse<Rundown>> {
|
||||
return axios.post(`${rundownPath}/${rundownId}/clone/${entryId}`);
|
||||
export async function postCloneEntry(
|
||||
rundownId: RundownId,
|
||||
entryId: EntryId,
|
||||
options?: { before?: EntryId; after?: EntryId },
|
||||
): Promise<AxiosResponse<Rundown>> {
|
||||
return axios.post(`${rundownPath}/${rundownId}/clone/${entryId}`, options);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,140 +0,0 @@
|
||||
.subtle {
|
||||
background: $gray-1050;
|
||||
color: $blue-400;
|
||||
line-height: 1em;
|
||||
|
||||
&:hover:not(:disabled):not(:active) {
|
||||
background: $gray-1000;
|
||||
color: $blue-500;
|
||||
}
|
||||
|
||||
&:active:not(:disabled) {
|
||||
background: $gray-1100;
|
||||
border-color: $gray-1250;
|
||||
}
|
||||
}
|
||||
|
||||
.subtle-white {
|
||||
background: $gray-1050;
|
||||
color: $ui-white;
|
||||
|
||||
&:hover:not(:disabled):not(:active) {
|
||||
background: $gray-1000;
|
||||
color: $ui-white;
|
||||
}
|
||||
|
||||
&:active:not(:disabled) {
|
||||
background: $gray-1100;
|
||||
border-color: $gray-1250;
|
||||
}
|
||||
|
||||
&:disabled {
|
||||
background: $gray-1050;
|
||||
}
|
||||
}
|
||||
|
||||
.primary {
|
||||
background: $blue-700;
|
||||
color: $ui-white;
|
||||
|
||||
&:hover:not(:disabled):not(:active) {
|
||||
background: $blue-600;
|
||||
}
|
||||
|
||||
&:active:not(:disabled) {
|
||||
background: $blue-800;
|
||||
border-color: $blue-900;
|
||||
}
|
||||
|
||||
&:disabled {
|
||||
opacity: $viewer-opacity-disabled;
|
||||
}
|
||||
}
|
||||
|
||||
.destructive {
|
||||
background: $red-700;
|
||||
color: $ui-white;
|
||||
|
||||
&:hover:not(:disabled):not(:active) {
|
||||
background: $red-600;
|
||||
color: $ui-white;
|
||||
}
|
||||
|
||||
&:active:not(:disabled) {
|
||||
background: $red-800;
|
||||
border-color: $red-900;
|
||||
}
|
||||
}
|
||||
|
||||
.subtle-destructive {
|
||||
background: $gray-1050;
|
||||
color: $red-400;
|
||||
|
||||
&:hover:not(:disabled):not(:active) {
|
||||
background: $gray-1000;
|
||||
color: $red-500;
|
||||
}
|
||||
|
||||
&:active:not(:disabled) {
|
||||
background: $gray-1100;
|
||||
border-color: $gray-1250;
|
||||
}
|
||||
}
|
||||
|
||||
.ghosted {
|
||||
background: transparent;
|
||||
color: $blue-500;
|
||||
|
||||
&:hover:not(:disabled):not(:active) {
|
||||
background: $gray-1000;
|
||||
color: $blue-500;
|
||||
}
|
||||
|
||||
&:active:not(:disabled) {
|
||||
background: $gray-1100;
|
||||
border-color: $gray-1250;
|
||||
}
|
||||
|
||||
&:disabled {
|
||||
opacity: $opacity-disabled;
|
||||
}
|
||||
}
|
||||
|
||||
.ghosted-white {
|
||||
background: transparent;
|
||||
color: $ui-white;
|
||||
|
||||
&:hover:not(:disabled):not(:active) {
|
||||
background: $gray-1000;
|
||||
color: $ui-white;
|
||||
}
|
||||
|
||||
&:active:not(:disabled) {
|
||||
background: $gray-1100;
|
||||
border-color: $gray-1250;
|
||||
}
|
||||
|
||||
&:disabled {
|
||||
opacity: $opacity-disabled;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
.ghosted-destructive {
|
||||
background: transparent;
|
||||
color: $red-500;
|
||||
|
||||
&:hover:not(:disabled):not(:active) {
|
||||
background: $gray-1000;
|
||||
color: $red-500;
|
||||
}
|
||||
|
||||
&:active:not(:disabled) {
|
||||
background: $gray-1100;
|
||||
border-color: $gray-1250;
|
||||
}
|
||||
|
||||
&:disabled {
|
||||
opacity: $opacity-disabled;
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
@use '@/theme/viewerDefs' as *;
|
||||
@import './BaseButtonStyles.module.scss';
|
||||
@use './buttonVariants' as base;
|
||||
|
||||
@include base.button-variants;
|
||||
|
||||
.baseButton {
|
||||
position: relative;
|
||||
@@ -81,4 +82,3 @@
|
||||
.fluid {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
@use '@/theme/viewerDefs' as *;
|
||||
@import './BaseButtonStyles.module.scss';
|
||||
@use './buttonVariants' as base;
|
||||
|
||||
@include base.button-variants;
|
||||
|
||||
.baseIconButton {
|
||||
aspect-ratio: 1;
|
||||
|
||||
@@ -0,0 +1,144 @@
|
||||
@use '@/theme/themeTokens' as *;
|
||||
@use '@/theme/viewerDefs' as *;
|
||||
|
||||
@mixin button-variants {
|
||||
.subtle {
|
||||
background: $gray-1050;
|
||||
color: $blue-400;
|
||||
line-height: 1em;
|
||||
|
||||
&:hover:not(:disabled):not(:active) {
|
||||
background: $gray-1000;
|
||||
color: $blue-500;
|
||||
}
|
||||
|
||||
&:active:not(:disabled) {
|
||||
background: $gray-1100;
|
||||
border-color: $gray-1250;
|
||||
}
|
||||
}
|
||||
|
||||
.subtle-white {
|
||||
background: $gray-1050;
|
||||
color: $ui-white;
|
||||
|
||||
&:hover:not(:disabled):not(:active) {
|
||||
background: $gray-1000;
|
||||
color: $ui-white;
|
||||
}
|
||||
|
||||
&:active:not(:disabled) {
|
||||
background: $gray-1100;
|
||||
border-color: $gray-1250;
|
||||
}
|
||||
|
||||
&:disabled {
|
||||
background: $gray-1050;
|
||||
}
|
||||
}
|
||||
|
||||
.primary {
|
||||
background: $blue-700;
|
||||
color: $ui-white;
|
||||
|
||||
&:hover:not(:disabled):not(:active) {
|
||||
background: $blue-600;
|
||||
}
|
||||
|
||||
&:active:not(:disabled) {
|
||||
background: $blue-800;
|
||||
border-color: $blue-900;
|
||||
}
|
||||
|
||||
&:disabled {
|
||||
opacity: $viewer-opacity-disabled;
|
||||
}
|
||||
}
|
||||
|
||||
.destructive {
|
||||
background: $red-700;
|
||||
color: $ui-white;
|
||||
|
||||
&:hover:not(:disabled):not(:active) {
|
||||
background: $red-600;
|
||||
color: $ui-white;
|
||||
}
|
||||
|
||||
&:active:not(:disabled) {
|
||||
background: $red-800;
|
||||
border-color: $red-900;
|
||||
}
|
||||
}
|
||||
|
||||
.subtle-destructive {
|
||||
background: $gray-1050;
|
||||
color: $red-400;
|
||||
|
||||
&:hover:not(:disabled):not(:active) {
|
||||
background: $gray-1000;
|
||||
color: $red-500;
|
||||
}
|
||||
|
||||
&:active:not(:disabled) {
|
||||
background: $gray-1100;
|
||||
border-color: $gray-1250;
|
||||
}
|
||||
}
|
||||
|
||||
.ghosted {
|
||||
background: transparent;
|
||||
color: $blue-500;
|
||||
|
||||
&:hover:not(:disabled):not(:active) {
|
||||
background: $gray-1000;
|
||||
color: $blue-500;
|
||||
}
|
||||
|
||||
&:active:not(:disabled) {
|
||||
background: $gray-1100;
|
||||
border-color: $gray-1250;
|
||||
}
|
||||
|
||||
&:disabled {
|
||||
opacity: $opacity-disabled;
|
||||
}
|
||||
}
|
||||
|
||||
.ghosted-white {
|
||||
background: transparent;
|
||||
color: $ui-white;
|
||||
|
||||
&:hover:not(:disabled):not(:active) {
|
||||
background: $gray-1000;
|
||||
color: $ui-white;
|
||||
}
|
||||
|
||||
&:active:not(:disabled) {
|
||||
background: $gray-1100;
|
||||
border-color: $gray-1250;
|
||||
}
|
||||
|
||||
&:disabled {
|
||||
opacity: $opacity-disabled;
|
||||
}
|
||||
}
|
||||
|
||||
.ghosted-destructive {
|
||||
background: transparent;
|
||||
color: $red-500;
|
||||
|
||||
&:hover:not(:disabled):not(:active) {
|
||||
background: $gray-1000;
|
||||
color: $red-500;
|
||||
}
|
||||
|
||||
&:active:not(:disabled) {
|
||||
background: $gray-1100;
|
||||
border-color: $gray-1250;
|
||||
}
|
||||
|
||||
&:disabled {
|
||||
opacity: $opacity-disabled;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
import { IoCheckmark } from 'react-icons/io5';
|
||||
import { Checkbox as BaseCheckbox } from '@base-ui-components/react/checkbox';
|
||||
import { Checkbox as BaseCheckbox } from '@base-ui/react/checkbox';
|
||||
|
||||
import style from './Checkbox.module.scss';
|
||||
|
||||
|
||||
@@ -5,10 +5,10 @@ import { navigatorConstants } from '../../../viewerConfig';
|
||||
import { setClientRemote } from '../../hooks/useSocket';
|
||||
import useUrlPresets from '../../hooks-query/useUrlPresets';
|
||||
import Button from '../buttons/Button';
|
||||
import Dialog from '../dialog/Dialog';
|
||||
import Info from '../info/Info';
|
||||
import Input from '../input/input/Input';
|
||||
import AppLink from '../link/app-link/AppLink';
|
||||
import Modal from '../modal/Modal';
|
||||
import Select from '../select/Select';
|
||||
|
||||
import style from './RedirectClientModal.module.scss';
|
||||
@@ -57,7 +57,7 @@ export function RedirectClientModal({ id, isOpen, name, currentPath, origin, onC
|
||||
];
|
||||
|
||||
return (
|
||||
<Modal
|
||||
<Dialog
|
||||
isOpen={isOpen}
|
||||
onClose={onClose}
|
||||
showCloseButton
|
||||
@@ -98,7 +98,10 @@ export function RedirectClientModal({ id, isOpen, name, currentPath, origin, onC
|
||||
fluid
|
||||
options={viewOptions}
|
||||
defaultValue={viewOptions[0].value}
|
||||
onValueChange={(value) => setSelected(value)}
|
||||
onValueChange={(value) => {
|
||||
if (value === null) return;
|
||||
setSelected(value);
|
||||
}}
|
||||
disabled={enabledPresets.length === 0}
|
||||
/>
|
||||
</label>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { PropsWithChildren, useRef, useState } from 'react';
|
||||
import { IoCheckmark, IoCopy } from 'react-icons/io5';
|
||||
|
||||
import copyToClipboard from '../../utils/copyToClipboard';
|
||||
import { copyToClipboard } from '../../utils/copyToClipboard';
|
||||
import { cx } from '../../utils/styleUtils';
|
||||
import Button from '../buttons/Button';
|
||||
import IconButton from '../buttons/IconButton';
|
||||
@@ -25,15 +25,19 @@ export default function CopyTag({
|
||||
const [copied, setCopied] = useState(false);
|
||||
const timeoutRef = useRef<NodeJS.Timeout | null>(null);
|
||||
|
||||
const handleClick = () => {
|
||||
copyToClipboard(copyValue);
|
||||
setCopied(true);
|
||||
const handleClick = async () => {
|
||||
try {
|
||||
await copyToClipboard(copyValue);
|
||||
setCopied(true);
|
||||
|
||||
// reset copied state
|
||||
if (timeoutRef.current) {
|
||||
clearTimeout(timeoutRef.current);
|
||||
// reset copied state
|
||||
if (timeoutRef.current) {
|
||||
clearTimeout(timeoutRef.current);
|
||||
}
|
||||
timeoutRef.current = setTimeout(() => setCopied(false), 2000);
|
||||
} catch {
|
||||
// ignore errors
|
||||
}
|
||||
timeoutRef.current = setTimeout(() => setCopied(false), 2000);
|
||||
};
|
||||
|
||||
return (
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
transform: translateX(-50%);
|
||||
|
||||
padding-inline: 1rem;
|
||||
min-width: min(420px, 90vw);
|
||||
min-width: min(600px, 90vw);
|
||||
|
||||
background-color: $gray-1250;
|
||||
color: $ui-white;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { ReactNode } from 'react';
|
||||
import { IoClose } from 'react-icons/io5';
|
||||
import { Dialog as BaseDialog } from '@base-ui-components/react/dialog';
|
||||
import { Dialog as BaseDialog } from '@base-ui/react/dialog';
|
||||
|
||||
import IconButton from '../buttons/IconButton';
|
||||
|
||||
@@ -12,7 +12,7 @@ interface DialogProps {
|
||||
showCloseButton?: boolean;
|
||||
showBackdrop?: boolean;
|
||||
bodyElements: ReactNode;
|
||||
footerElements: ReactNode;
|
||||
footerElements?: ReactNode;
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { PropsWithChildren } from 'react';
|
||||
import { IconType } from 'react-icons';
|
||||
import { Menu as BaseMenu } from '@base-ui-components/react/menu';
|
||||
import { Menu as BaseMenu } from '@base-ui/react/menu';
|
||||
|
||||
import style from './DropdownMenu.module.scss';
|
||||
|
||||
@@ -31,7 +31,13 @@ export function DropdownMenu({ items, children, ...triggerProps }: PropsWithChil
|
||||
return <BaseMenu.Separator key={index} className={style.separator} />;
|
||||
}
|
||||
return (
|
||||
<BaseMenu.Item key={index} className={style.item} onClick={item.onClick} disabled={item.disabled} data-type={item.type}>
|
||||
<BaseMenu.Item
|
||||
key={index}
|
||||
className={style.item}
|
||||
onClick={item.onClick}
|
||||
disabled={item.disabled}
|
||||
data-type={item.type}
|
||||
>
|
||||
{item.icon && <item.icon />}
|
||||
{item.label}
|
||||
</BaseMenu.Item>
|
||||
|
||||
@@ -59,3 +59,10 @@
|
||||
height: 0.75em;
|
||||
}
|
||||
}
|
||||
|
||||
.panel {
|
||||
position: relative;
|
||||
border-radius: $panel-border-radius;
|
||||
background-color: $bg-container-l2;
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
@@ -55,3 +55,7 @@ interface SeparatorProps extends HTMLAttributes<HTMLDivElement> {
|
||||
export function Separator({ className, orientation = 'vertical', ...elementProps }: SeparatorProps) {
|
||||
return <div className={cx([style.separator, style[orientation], className])} role='separator' {...elementProps} />;
|
||||
}
|
||||
|
||||
export function Panel({ className, ...props }: HTMLAttributes<HTMLDivElement>) {
|
||||
return <div className={cx([style.panel, className])} {...props} />;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Radio } from '@base-ui-components/react/radio';
|
||||
import { RadioGroup as BaseRadioGroup } from '@base-ui-components/react/radio-group';
|
||||
import { Radio } from '@base-ui/react/radio';
|
||||
import { RadioGroup as BaseRadioGroup } from '@base-ui/react/radio-group';
|
||||
|
||||
import style from './BlockRadio.module.scss';
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { PropsWithChildren } from 'react';
|
||||
import { HexAlphaColorPicker, HexColorInput } from 'react-colorful';
|
||||
import { Popover } from '@base-ui-components/react/popover';
|
||||
import { Popover } from '@base-ui/react/popover';
|
||||
|
||||
import PopoverContents from '../../popover/Popover';
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { ReactNode } from 'react';
|
||||
import { IoClose } from 'react-icons/io5';
|
||||
import { Dialog as BaseDialog } from '@base-ui-components/react/dialog';
|
||||
import { Dialog as BaseDialog } from '@base-ui/react/dialog';
|
||||
|
||||
import IconButton from '../buttons/IconButton';
|
||||
|
||||
@@ -31,7 +31,7 @@ export default function Modal({
|
||||
onOpenChange={(isOpen) => {
|
||||
if (!isOpen) onClose();
|
||||
}}
|
||||
dismissible={false}
|
||||
disablePointerDismissal
|
||||
>
|
||||
<BaseDialog.Portal>
|
||||
{showBackdrop && <BaseDialog.Backdrop className={style.backdrop} />}
|
||||
|
||||
@@ -2,11 +2,11 @@ import { memo } from 'react';
|
||||
import { IoClose, IoContract, IoExpand, IoLockClosedOutline, IoSwapVertical } from 'react-icons/io5';
|
||||
import { LuCoffee } from 'react-icons/lu';
|
||||
import { useLocation } from 'react-router';
|
||||
import { Dialog } from '@base-ui-components/react/dialog';
|
||||
import { Dialog } from '@base-ui/react/dialog';
|
||||
import { useDisclosure, useFullscreen } from '@mantine/hooks';
|
||||
|
||||
import { isLocalhost, supportsFullscreen } from '../../../externals';
|
||||
import { useKeepAwakeOptions } from '../../../features/keep-awake/KeepAwake';
|
||||
import { canUseWakeLock, useKeepAwakeOptions } from '../../../features/keep-awake/useWakeLock';
|
||||
import { navigatorConstants } from '../../../viewerConfig';
|
||||
import { useIsSmallScreen } from '../../hooks/useIsSmallScreen';
|
||||
import { useClientStore } from '../../stores/clientStore';
|
||||
@@ -69,7 +69,7 @@ function NavigationMenu({ isOpen, onClose }: NavigationMenuProps) {
|
||||
<IoSwapVertical />
|
||||
{mirror && <span className={style.note}>Active</span>}
|
||||
</NavigationMenuItem>
|
||||
{window.isSecureContext && (
|
||||
{canUseWakeLock && (
|
||||
<NavigationMenuItem active={keepAwake} onClick={toggleKeepAwake}>
|
||||
Keep Awake
|
||||
<LuCoffee />
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { PropsWithChildren } from 'react';
|
||||
import { Popover } from '@base-ui-components/react/popover';
|
||||
import { Popover } from '@base-ui/react/popover';
|
||||
|
||||
import style from './Popover.module.scss';
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Radio } from '@base-ui-components/react/radio';
|
||||
import { RadioGroup as BaseRadioGroup } from '@base-ui-components/react/radio-group';
|
||||
import { Radio } from '@base-ui/react/radio';
|
||||
import { RadioGroup as BaseRadioGroup } from '@base-ui/react/radio-group';
|
||||
|
||||
import { cx } from '../../utils/styleUtils';
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { IoCheckmark } from 'react-icons/io5';
|
||||
import { LuChevronsUpDown } from 'react-icons/lu';
|
||||
import { Select as BaseSelect } from '@base-ui-components/react/select';
|
||||
import { Select as BaseSelect } from '@base-ui/react/select';
|
||||
|
||||
import { cx } from '../../utils/styleUtils';
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Switch as BaseSwitch } from '@base-ui-components/react/switch';
|
||||
import { Switch as BaseSwitch } from '@base-ui/react/switch';
|
||||
|
||||
import { cx } from '../../utils/styleUtils';
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { PropsWithChildren } from 'react';
|
||||
import { Tooltip as BaseTooltip } from '@base-ui-components/react/tooltip';
|
||||
import { Tooltip as BaseTooltip } from '@base-ui/react/tooltip';
|
||||
|
||||
import style from './Tooltip.module.scss';
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { ComponentProps, useEffect, useState } from 'react';
|
||||
import { useSearchParams } from 'react-router';
|
||||
|
||||
import { isStringBoolean } from '../../../features/viewers/common/viewUtils';
|
||||
import { isStringBoolean } from '../../../views/common/viewUtils';
|
||||
import Checkbox from '../checkbox/Checkbox';
|
||||
import Input from '../input/input/Input';
|
||||
import Select, { SelectOption } from '../select/Select';
|
||||
@@ -160,7 +160,16 @@ function ControlledSelect({ id, initialValue, options }: ControlledSelectProps)
|
||||
}, [initialValue]);
|
||||
|
||||
return (
|
||||
<Select size='large' name={id} options={options} value={selected} onValueChange={(value) => setSelected(value)} />
|
||||
<Select
|
||||
size='large'
|
||||
name={id}
|
||||
options={options}
|
||||
value={selected}
|
||||
onValueChange={(value) => {
|
||||
if (value === null) return;
|
||||
setSelected(value);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { FormEvent, memo } from 'react';
|
||||
import { IoClose } from 'react-icons/io5';
|
||||
import { useSearchParams } from 'react-router';
|
||||
import { Dialog } from '@base-ui-components/react/dialog';
|
||||
import { Dialog } from '@base-ui/react/dialog';
|
||||
import { OntimeView } from 'ontime-types';
|
||||
|
||||
import { useIsSmallScreen } from '../../hooks/useIsSmallScreen';
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { useMemo } from 'react';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { EntryId, OntimeEntry, Rundown } from 'ontime-types';
|
||||
|
||||
@@ -8,8 +8,6 @@ import { fetchCurrentRundown } from '../api/rundown';
|
||||
import { useSelectedEventId } from '../hooks/useSocket';
|
||||
import { ExtendedEntry, getFlatRundownMetadata, getRundownMetadata } from '../utils/rundownMetadata';
|
||||
|
||||
import useProjectData from './useProjectData';
|
||||
|
||||
// revision is -1 so that the remote revision is higher
|
||||
const cachedRundownPlaceholder: Rundown = {
|
||||
id: 'default',
|
||||
@@ -27,9 +25,9 @@ export default function useRundown() {
|
||||
const { data, status, isError, refetch, isFetching } = useQuery<Rundown>({
|
||||
queryKey: RUNDOWN,
|
||||
queryFn: fetchCurrentRundown,
|
||||
placeholderData: (previousData, _previousQuery) => previousData,
|
||||
refetchInterval: queryRefetchIntervalSlow,
|
||||
});
|
||||
|
||||
return { data: data ?? cachedRundownPlaceholder, status, isError, refetch, isFetching };
|
||||
}
|
||||
|
||||
@@ -46,30 +44,13 @@ export function useRundownWithMetadata() {
|
||||
*/
|
||||
export function useFlatRundown() {
|
||||
const { data, status } = useRundown();
|
||||
const { data: projectData } = useProjectData();
|
||||
|
||||
const loadedProject = useRef<string>('');
|
||||
const [prevRevision, setPrevRevision] = useState<number>(-1);
|
||||
const [flatRundown, setFlatRundown] = useState<OntimeEntry[]>([]);
|
||||
|
||||
// update data whenever the revision changes
|
||||
useEffect(() => {
|
||||
if (data.revision !== -1 || data.revision !== prevRevision) {
|
||||
const flatRundown = data.flatOrder.map((id) => data.entries[id]);
|
||||
setFlatRundown(flatRundown);
|
||||
setPrevRevision(data.revision);
|
||||
const flatRundown = useMemo(() => {
|
||||
if (data.revision === -1) {
|
||||
return [];
|
||||
}
|
||||
}, [data.entries, data.flatOrder, data.revision, prevRevision]);
|
||||
|
||||
// TODO: should we have a project id field?
|
||||
// TODO(v4): cleanup as part of load multiple rundowns
|
||||
// invalidate current version if project changes
|
||||
useEffect(() => {
|
||||
if (projectData?.title !== loadedProject.current) {
|
||||
setPrevRevision(-1);
|
||||
loadedProject.current = projectData?.title ?? '';
|
||||
}
|
||||
}, [projectData]);
|
||||
return data.flatOrder.map((id) => data.entries[id]).filter((entry): entry is OntimeEntry => entry !== undefined);
|
||||
}, [data]);
|
||||
|
||||
return { data: flatRundown, rundownId: data.id, status };
|
||||
}
|
||||
@@ -84,6 +65,10 @@ export function useFlatRundownWithMetadata() {
|
||||
|
||||
/**
|
||||
* Provides access to a partial rundown based on a filter callback
|
||||
*
|
||||
* Callers MUST memoize the callback with useCallback to prevent
|
||||
* re-filtering on every render.
|
||||
*
|
||||
*/
|
||||
export function usePartialRundown(cb: (event: ExtendedEntry<OntimeEntry>) => boolean) {
|
||||
const { data, status } = useFlatRundownWithMetadata();
|
||||
@@ -100,11 +85,6 @@ export function usePartialRundown(cb: (event: ExtendedEntry<OntimeEntry>) => boo
|
||||
export function useEntry(entryId: EntryId | null): OntimeEntry | null {
|
||||
const { data: rundown } = useRundown();
|
||||
|
||||
// track the specific entry we care about
|
||||
const entry = useMemo(() => {
|
||||
if (entryId === null) return null;
|
||||
return rundown.entries[entryId];
|
||||
}, [entryId, rundown.entries]);
|
||||
|
||||
return entry;
|
||||
if (entryId === null) return null;
|
||||
return rundown.entries[entryId] ?? null;
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ import { useCallback } from 'react';
|
||||
import { useMutation, useQueryClient } from '@tanstack/react-query';
|
||||
import {
|
||||
EntryId,
|
||||
InsertOptions,
|
||||
isOntimeEvent,
|
||||
isOntimeGroup,
|
||||
MaybeString,
|
||||
@@ -173,7 +174,8 @@ export const useEntryActions = () => {
|
||||
* @private
|
||||
*/
|
||||
const { mutateAsync: cloneEntryMutation } = useMutation({
|
||||
mutationFn: ([rundownId, entryId]: Parameters<typeof postCloneEntry>) => postCloneEntry(rundownId, entryId),
|
||||
mutationFn: ([rundownId, entryId, options]: Parameters<typeof postCloneEntry>) =>
|
||||
postCloneEntry(rundownId, entryId, options),
|
||||
onMutate: () => queryClient.cancelQueries({ queryKey: RUNDOWN }),
|
||||
onSettled: () => queryClient.invalidateQueries({ queryKey: RUNDOWN }),
|
||||
});
|
||||
@@ -182,14 +184,14 @@ export const useEntryActions = () => {
|
||||
* Clone an entry
|
||||
*/
|
||||
const clone = useCallback(
|
||||
async (entryId: EntryId) => {
|
||||
async (entryId: EntryId, options?: InsertOptions) => {
|
||||
try {
|
||||
const rundownId = getCurrentRundownData()?.id;
|
||||
if (!rundownId) {
|
||||
throw new Error('Rundown not initialised');
|
||||
}
|
||||
|
||||
await cloneEntryMutation([rundownId, entryId]);
|
||||
await cloneEntryMutation([rundownId, entryId, options]);
|
||||
} catch (error) {
|
||||
logAxiosError('Error cloning entry', error);
|
||||
}
|
||||
|
||||
@@ -1,68 +0,0 @@
|
||||
import { EndAction, EntryCustomFields, OntimeEvent, SupportedEntry, TimerType, TimeStrategy } from 'ontime-types';
|
||||
|
||||
import { cloneEvent } from '../clone';
|
||||
|
||||
describe('cloneEvent()', () => {
|
||||
it('creates a stem from a given event', () => {
|
||||
const original: OntimeEvent = {
|
||||
id: 'unique',
|
||||
type: SupportedEntry.Event,
|
||||
flag: false,
|
||||
title: 'title',
|
||||
cue: 'cue',
|
||||
note: 'note',
|
||||
timeStart: 0,
|
||||
duration: 10,
|
||||
timeEnd: 10,
|
||||
timerType: TimerType.CountDown,
|
||||
timeStrategy: TimeStrategy.LockEnd,
|
||||
parent: 'test',
|
||||
linkStart: false,
|
||||
countToEnd: false,
|
||||
endAction: EndAction.None,
|
||||
skip: false,
|
||||
colour: 'F00',
|
||||
revision: 10,
|
||||
timeWarning: 120000,
|
||||
timeDanger: 60000,
|
||||
delay: 0,
|
||||
dayOffset: 0,
|
||||
gap: 0,
|
||||
triggers: [],
|
||||
custom: {
|
||||
lighting: '3',
|
||||
} as EntryCustomFields,
|
||||
};
|
||||
|
||||
const cloned = cloneEvent(original);
|
||||
expect(cloned).not.toBe(original);
|
||||
expect(cloned.custom).not.toBe(original.custom);
|
||||
expect(cloned.triggers).not.toBe(original.triggers);
|
||||
|
||||
expect(cloned).toMatchObject({
|
||||
type: SupportedEntry.Event,
|
||||
flag: original.flag,
|
||||
title: original.title,
|
||||
note: original.note,
|
||||
timeStart: original.timeStart,
|
||||
duration: original.duration,
|
||||
timeEnd: original.timeEnd,
|
||||
timerType: original.timerType,
|
||||
timeStrategy: original.timeStrategy,
|
||||
parent: 'test',
|
||||
countToEnd: original.countToEnd,
|
||||
linkStart: original.linkStart,
|
||||
endAction: original.endAction,
|
||||
skip: original.skip,
|
||||
colour: original.colour,
|
||||
revision: 0,
|
||||
delay: original.delay,
|
||||
dayOffset: original.dayOffset,
|
||||
gap: 0,
|
||||
timeWarning: original.timeWarning,
|
||||
timeDanger: original.timeDanger,
|
||||
triggers: original.triggers,
|
||||
custom: original.custom,
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,36 +0,0 @@
|
||||
import { OntimeEvent, SupportedEntry } from 'ontime-types';
|
||||
|
||||
/**
|
||||
* @description Creates a safe duplicate of an event
|
||||
* @param {OntimeEvent} event
|
||||
* @param {string} [after]
|
||||
* @return {OntimeEvent} clean event
|
||||
*/
|
||||
type ClonedEvent = Omit<OntimeEvent, 'id' | 'cue'>;
|
||||
export const cloneEvent = (event: OntimeEvent): ClonedEvent => {
|
||||
return {
|
||||
type: SupportedEntry.Event,
|
||||
flag: event.flag,
|
||||
title: event.title,
|
||||
note: event.note,
|
||||
timeStart: event.timeStart,
|
||||
duration: event.duration,
|
||||
timeEnd: event.timeEnd,
|
||||
timerType: event.timerType,
|
||||
timeStrategy: event.timeStrategy,
|
||||
countToEnd: event.countToEnd,
|
||||
linkStart: event.linkStart,
|
||||
endAction: event.endAction,
|
||||
skip: event.skip,
|
||||
colour: event.colour,
|
||||
parent: event.parent,
|
||||
revision: 0,
|
||||
delay: event.delay, // the events will be collocated, so having the same metadata is a good start
|
||||
dayOffset: event.dayOffset,
|
||||
gap: 0,
|
||||
timeWarning: event.timeWarning,
|
||||
timeDanger: event.timeDanger,
|
||||
triggers: structuredClone(event.triggers),
|
||||
custom: structuredClone(event.custom),
|
||||
};
|
||||
};
|
||||
@@ -1,4 +1,18 @@
|
||||
// we need to this as a promise because safari
|
||||
export default async function copyToClipboard(text: string) {
|
||||
setTimeout(async () => await navigator.clipboard?.writeText(text));
|
||||
/**
|
||||
* copy text to clipboard
|
||||
* @throws if not supported or permission denied
|
||||
*/
|
||||
export async function copyToClipboard(text: string) {
|
||||
await navigator.clipboard?.writeText(text);
|
||||
}
|
||||
|
||||
/**
|
||||
* Copy to clipboard but safely ignore errors
|
||||
*/
|
||||
export async function safeCopyToClipboard(text: string): Promise<void> {
|
||||
try {
|
||||
await copyToClipboard(text);
|
||||
} catch {
|
||||
// Silently ignore errors
|
||||
}
|
||||
}
|
||||
|
||||
@@ -230,10 +230,18 @@ export default function AutomationForm({ automation, onClose }: AutomationFormPr
|
||||
<div key={key} className={style.filterSection}>
|
||||
<label>
|
||||
Runtime data source
|
||||
<Select
|
||||
value={watch(`filters.${index}.field`)}
|
||||
onValueChange={(value) => setValue(`filters.${index}.field`, value, { shouldDirty: true })}
|
||||
options={fieldList.map(({ value, label }) => ({ value, label }))}
|
||||
<Select<string | null>
|
||||
// need to normalize '' to null for the Select to show the placeholder
|
||||
value={watch(`filters.${index}.field`) || null}
|
||||
onValueChange={(value) => {
|
||||
if (value === null) return;
|
||||
setValue(`filters.${index}.field`, value, { shouldDirty: true });
|
||||
}}
|
||||
options={fieldList.map(({ value, label }) => ({
|
||||
value,
|
||||
label,
|
||||
disabled: value === null,
|
||||
}))}
|
||||
aria-label='Event field'
|
||||
/>
|
||||
<Panel.Error>{errors.filters?.[index]?.field?.message}</Panel.Error>
|
||||
@@ -242,11 +250,14 @@ export default function AutomationForm({ automation, onClose }: AutomationFormPr
|
||||
Matching condition
|
||||
<Select
|
||||
value={watch(`filters.${index}.operator`)}
|
||||
onValueChange={(value) =>
|
||||
setValue(`filters.${index}.operator`, value as 'equals' | 'not_equals' | 'contains', {
|
||||
shouldDirty: true,
|
||||
})
|
||||
}
|
||||
onValueChange={(value: string | null) => {
|
||||
if (value === null) return;
|
||||
setValue(
|
||||
`filters.${index}.operator`,
|
||||
value as 'equals' | 'not_equals' | 'greater_than' | 'less_than' | 'contains' | 'not_contains',
|
||||
{ shouldDirty: true },
|
||||
);
|
||||
}}
|
||||
options={[
|
||||
{ value: 'equals', label: 'equals' },
|
||||
{ value: 'not_equals', label: 'not equals' },
|
||||
@@ -282,7 +293,8 @@ export default function AutomationForm({ automation, onClose }: AutomationFormPr
|
||||
<div className={style.innerColumn}>
|
||||
<h3>Outputs</h3>
|
||||
<Info>
|
||||
Automation outputs can be used to send data from Ontime to external software.
|
||||
Automation outputs can be used to send data from Ontime to external software <br />
|
||||
or to change properties of Ontime itself.
|
||||
<ExternalLink href={integrationsDocsUrl}>See the documentation for templates</ExternalLink>
|
||||
</Info>
|
||||
|
||||
|
||||
+19
-10
@@ -44,8 +44,9 @@ export default function OntimeActionForm({
|
||||
<label>
|
||||
Action
|
||||
<Select
|
||||
onValueChange={(value) => {
|
||||
handleSetAction(value as OntimeActionKey);
|
||||
onValueChange={(value: OntimeActionKey | null) => {
|
||||
if (value === null) return;
|
||||
handleSetAction(value);
|
||||
}}
|
||||
value={watch(`outputs.${index}.action`)}
|
||||
options={[
|
||||
@@ -97,13 +98,13 @@ export default function OntimeActionForm({
|
||||
Visibility
|
||||
<Select
|
||||
onValueChange={(value) => {
|
||||
// we need to translate the undefined value to 'untouched'
|
||||
const translatedValue = value === 'untouched' ? undefined : (value as boolean | undefined);
|
||||
// we need to translate the null to undefined so it becomes 'untouched'
|
||||
const translatedValue = value === null ? undefined : value;
|
||||
setValue(`outputs.${index}.visible`, translatedValue, { shouldDirty: true });
|
||||
}}
|
||||
value={watch(`outputs.${index}.visible`) === undefined ? 'untouched' : watch(`outputs.${index}.visible`)}
|
||||
value={watch(`outputs.${index}.visible`)}
|
||||
options={[
|
||||
{ value: 'untouched', label: 'Untouched' },
|
||||
{ value: null, label: 'Untouched' },
|
||||
{ value: true, label: 'Show' },
|
||||
{ value: false, label: 'Hide' },
|
||||
]}
|
||||
@@ -116,9 +117,16 @@ export default function OntimeActionForm({
|
||||
{selectedAction === 'message-secondary' && (
|
||||
<label>
|
||||
Timer secondary source
|
||||
<Select
|
||||
<Select<SecondarySource | 'null' | null>
|
||||
onValueChange={(value) => {
|
||||
setValue(`outputs.${index}.secondarySource`, value as SecondarySource, { shouldDirty: true });
|
||||
// null -> no selection
|
||||
if (value === null) return;
|
||||
// 'null' -> clear the secondary source
|
||||
if (value === 'null') {
|
||||
setValue(`outputs.${index}.secondarySource`, null, { shouldDirty: true });
|
||||
return;
|
||||
}
|
||||
setValue(`outputs.${index}.secondarySource`, value, { shouldDirty: true });
|
||||
}}
|
||||
value={watch(`outputs.${index}.secondarySource`)}
|
||||
options={[
|
||||
@@ -126,13 +134,14 @@ export default function OntimeActionForm({
|
||||
{ value: 'aux1', label: 'Auxiliary timer 1' },
|
||||
{ value: 'aux2', label: 'Auxiliary timer 2' },
|
||||
{ value: 'aux3', label: 'Auxiliary timer 3' },
|
||||
{ value: 'external', label: 'External' },
|
||||
{ value: 'null', label: 'None' },
|
||||
{ value: 'secondary', label: 'Secondary' },
|
||||
{ value: 'null', label: 'None' }, // allow the user to clear the secondary source
|
||||
]}
|
||||
/>
|
||||
<Panel.Error>{rowErrors?.secondarySource?.message}</Panel.Error>
|
||||
</label>
|
||||
)}
|
||||
|
||||
<div className={style.test}>{children}</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -106,7 +106,10 @@ export default function TriggerForm({
|
||||
Lifecycle trigger
|
||||
<Select
|
||||
value={watch('trigger')}
|
||||
onValueChange={(value) => setValue('trigger', value as TimerLifeCycle, { shouldDirty: true })}
|
||||
onValueChange={(value) => {
|
||||
if (value === null) return;
|
||||
setValue('trigger', value as TimerLifeCycle, { shouldDirty: true });
|
||||
}}
|
||||
options={cycles.map((cycle) => ({ value: cycle.value, label: cycle.label }))}
|
||||
aria-label='Lifecycle trigger'
|
||||
/>
|
||||
@@ -116,7 +119,10 @@ export default function TriggerForm({
|
||||
Automation title
|
||||
<Select
|
||||
value={watch('automationId')}
|
||||
onValueChange={(value) => setValue('automationId', value, { shouldDirty: true })}
|
||||
onValueChange={(value: string | null) => {
|
||||
if (value === null) return;
|
||||
setValue('automationId', value, { shouldDirty: true });
|
||||
}}
|
||||
options={automationSelect}
|
||||
aria-label='Automation title'
|
||||
/>
|
||||
|
||||
@@ -27,6 +27,7 @@ export function isAutomation(automation: AutomationDTO | Automation): automation
|
||||
}
|
||||
|
||||
const staticSelectProperties = [
|
||||
{ value: null, label: 'Select field' },
|
||||
{ value: 'eventNow.id', label: 'ID' },
|
||||
{ value: 'eventNow.title', label: 'Title' },
|
||||
{ value: 'eventNow.cue', label: 'Cue' },
|
||||
@@ -42,7 +43,7 @@ const staticNextSelectProperties = [
|
||||
];
|
||||
|
||||
type SelectableField = {
|
||||
value: string; // string encodes path in runtime state object
|
||||
value: string | null; // string encodes path in runtime state object
|
||||
label: string;
|
||||
};
|
||||
|
||||
|
||||
+4
-1
@@ -142,7 +142,10 @@ export default function URLPresetForm({ urlPreset, onClose }: URLPresetFormProps
|
||||
options={targetOptions}
|
||||
{...register('target', { required: 'Target is required' })}
|
||||
value={watch('target')}
|
||||
onValueChange={(value) => setValue('target', value, { shouldDirty: true })}
|
||||
onValueChange={(value: OntimeViewPresettable | null) => {
|
||||
if (value === null) return;
|
||||
setValue('target', value, { shouldDirty: true });
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
|
||||
+12
-3
@@ -51,7 +51,10 @@ export default function RundownDefaultSettings() {
|
||||
/>
|
||||
<Select
|
||||
value={defaultTimeStrategy}
|
||||
onValueChange={(value) => setTimeStrategy(value as TimeStrategy)}
|
||||
onValueChange={(value: TimeStrategy | null) => {
|
||||
if (value === null) return;
|
||||
setTimeStrategy(value);
|
||||
}}
|
||||
options={[
|
||||
{ value: TimeStrategy.LockDuration, label: 'Duration' },
|
||||
{ value: TimeStrategy.LockEnd, label: 'End Time' },
|
||||
@@ -73,7 +76,10 @@ export default function RundownDefaultSettings() {
|
||||
<Panel.Field title='Timer type' description='Default type of timer for new events' />
|
||||
<Select
|
||||
value={defaultTimerType}
|
||||
onValueChange={(value) => setDefaultTimerType(value as TimerType)}
|
||||
onValueChange={(value: TimerType | null) => {
|
||||
if (value === null) return;
|
||||
setDefaultTimerType(value);
|
||||
}}
|
||||
options={[
|
||||
{ value: TimerType.CountDown, label: 'Count down' },
|
||||
{ value: TimerType.CountUp, label: 'Count up' },
|
||||
@@ -86,7 +92,10 @@ export default function RundownDefaultSettings() {
|
||||
<Panel.Field title='End Action' description='Default end action for new events' />
|
||||
<Select
|
||||
value={defaultEndAction}
|
||||
onValueChange={(value) => setDefaultEndAction(value as EndAction)}
|
||||
onValueChange={(value: EndAction | null) => {
|
||||
if (value === null) return;
|
||||
setDefaultEndAction(value);
|
||||
}}
|
||||
options={[
|
||||
{ value: EndAction.None, label: 'None' },
|
||||
{ value: EndAction.LoadNext, label: 'Load next' },
|
||||
|
||||
+4
-3
@@ -162,9 +162,10 @@ export default function ImportMapForm({
|
||||
<Select
|
||||
id={importName as string}
|
||||
value={watch(label as keyof NamedImportMap) as string}
|
||||
onValueChange={(value: string) =>
|
||||
setValue(label as keyof NamedImportMap, value, { shouldDirty: true })
|
||||
}
|
||||
onValueChange={(value: string | null) => {
|
||||
if (value === null) return;
|
||||
setValue(label as keyof NamedImportMap, value, { shouldDirty: true });
|
||||
}}
|
||||
options={worksheetNames?.map((name) => ({ value: name, label: name })) || []}
|
||||
/>
|
||||
</td>
|
||||
|
||||
@@ -153,7 +153,10 @@ export default function GeneralSettings() {
|
||||
/>
|
||||
<Select
|
||||
value={watch('timeFormat')}
|
||||
onValueChange={(value) => setValue('timeFormat', value as '12' | '24', { shouldDirty: true })}
|
||||
onValueChange={(value: '12' | '24' | null) => {
|
||||
if (value === null) return;
|
||||
setValue('timeFormat', value, { shouldDirty: true });
|
||||
}}
|
||||
defaultValue='24'
|
||||
options={[
|
||||
{ value: '12', label: '12 hours 11:00:10 PM' },
|
||||
@@ -169,7 +172,10 @@ export default function GeneralSettings() {
|
||||
/>
|
||||
<Select
|
||||
value={watch('language')}
|
||||
onValueChange={(value) => setValue('language', value, { shouldDirty: true })}
|
||||
onValueChange={(value: string | null) => {
|
||||
if (value === null) return;
|
||||
setValue('language', value, { shouldDirty: true });
|
||||
}}
|
||||
disabled={disableInputs}
|
||||
defaultValue='en'
|
||||
options={[
|
||||
|
||||
@@ -75,7 +75,10 @@ export default function QuickStart({ isOpen, onClose }: QuickStartProps) {
|
||||
/>
|
||||
<Select
|
||||
value={watch('settings.timeFormat')}
|
||||
onValueChange={(value: '12' | '24') => setValue('settings.timeFormat', value, { shouldDirty: true })}
|
||||
onValueChange={(value: '12' | '24' | null) => {
|
||||
if (value === null) return;
|
||||
setValue('settings.timeFormat', value, { shouldDirty: true });
|
||||
}}
|
||||
defaultValue='24'
|
||||
options={[
|
||||
{ value: '12', label: '12 hours 11:00:10 PM' },
|
||||
@@ -91,7 +94,10 @@ export default function QuickStart({ isOpen, onClose }: QuickStartProps) {
|
||||
/>
|
||||
<Select
|
||||
value={watch('settings.language')}
|
||||
onValueChange={(value: string) => setValue('settings.language', value, { shouldDirty: true })}
|
||||
onValueChange={(value: string | null) => {
|
||||
if (value === null) return;
|
||||
setValue('settings.language', value, { shouldDirty: true });
|
||||
}}
|
||||
defaultValue='en'
|
||||
options={[
|
||||
{ value: 'en', label: 'English' },
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
@import "./MessageControl.module.scss";
|
||||
|
||||
.inputItems {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr auto;
|
||||
@@ -8,5 +6,10 @@
|
||||
}
|
||||
|
||||
.label {
|
||||
@extend .label;
|
||||
font-size: $inner-section-text-size;
|
||||
color: $label-gray;
|
||||
|
||||
&.active {
|
||||
color: $action-text-color;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -42,7 +42,7 @@ export default function InputRow(props: PropsWithChildren<InputRowProps>) {
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={style.inputRow}>
|
||||
<div>
|
||||
<label className={cx([style.label, visible ?? style.active])} htmlFor={label}>
|
||||
{label}
|
||||
</label>
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
.growPanel {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.contentLayout {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: $section-spacing;
|
||||
color: $ui-white;
|
||||
padding-top: $panel-top-padding;
|
||||
}
|
||||
@@ -1,34 +1,32 @@
|
||||
import { memo } from 'react';
|
||||
|
||||
import { CornerExtract } from '../../../common/components/editor-utils/EditorUtils';
|
||||
import * as Editor from '../../../common/components/editor-utils/EditorUtils';
|
||||
import ErrorBoundary from '../../../common/components/error-boundary/ErrorBoundary';
|
||||
import ViewNavigationMenu from '../../../common/components/navigation-menu/ViewNavigationMenu';
|
||||
import ProtectRoute from '../../../common/components/protect-route/ProtectRoute';
|
||||
import { handleLinks } from '../../../common/utils/linkUtils';
|
||||
import { cx } from '../../../common/utils/styleUtils';
|
||||
import { getIsNavigationLocked } from '../../../externals';
|
||||
|
||||
import MessageControl from './MessageControl';
|
||||
|
||||
import style from '../../../views/editor/Editor.module.scss';
|
||||
import style from './MessageControlExport.module.scss';
|
||||
|
||||
export default memo(MessageControlExport);
|
||||
function MessageControlExport() {
|
||||
const isExtracted = window.location.pathname.includes('/messagecontrol');
|
||||
const classes = cx([style.content, style.contentColumnLayout]);
|
||||
|
||||
return (
|
||||
<ProtectRoute permission='editor'>
|
||||
<div className={style.messages} data-testid='panel-messages-control'>
|
||||
{!isExtracted && <CornerExtract onClick={(event) => handleLinks('messagecontrol', event)} />}
|
||||
<Editor.Panel className={style.growPanel} data-testid='panel-messages-control'>
|
||||
{!isExtracted && <Editor.CornerExtract onClick={(event) => handleLinks('messagecontrol', event)} />}
|
||||
{isExtracted && <ViewNavigationMenu suppressSettings isNavigationLocked={getIsNavigationLocked()} />}
|
||||
|
||||
<div className={classes}>
|
||||
<div className={style.contentLayout}>
|
||||
<ErrorBoundary>
|
||||
<MessageControl />
|
||||
</ErrorBoundary>
|
||||
</div>
|
||||
</div>
|
||||
</Editor.Panel>
|
||||
</ProtectRoute>
|
||||
);
|
||||
}
|
||||
|
||||
+6
-31
@@ -1,18 +1,3 @@
|
||||
.label {
|
||||
font-size: $inner-section-text-size;
|
||||
color: $label-gray;
|
||||
|
||||
&.active {
|
||||
color: $action-text-color;
|
||||
}
|
||||
}
|
||||
|
||||
.previewContainer {
|
||||
display: grid;
|
||||
gap: $element-spacing;
|
||||
grid-template-columns: 3fr 2fr;
|
||||
}
|
||||
|
||||
.preview {
|
||||
background-color: $ui-black;
|
||||
display: grid;
|
||||
@@ -21,21 +6,6 @@
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.options {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: $element-spacing;
|
||||
}
|
||||
|
||||
.eventStatus {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
margin: 0.5rem 0.25rem;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.25rem;
|
||||
}
|
||||
|
||||
.mainContent {
|
||||
font-size: 1rem;
|
||||
font-weight: 600;
|
||||
@@ -61,9 +31,13 @@
|
||||
display: none;
|
||||
}
|
||||
|
||||
.timerIndicators {
|
||||
.eventStatus {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
margin: 0.5rem 0.25rem;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.25rem;
|
||||
}
|
||||
|
||||
.statusIcon {
|
||||
@@ -73,3 +47,4 @@
|
||||
color: $active-indicator;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,7 +10,7 @@ import { handleLinks } from '../../../common/utils/linkUtils';
|
||||
import { cx, timerPlaceholder } from '../../../common/utils/styleUtils';
|
||||
import PipRoot from '../../../views/editor/pip-timer/PipRoot';
|
||||
|
||||
import style from './MessageControl.module.scss';
|
||||
import style from './TimerPreview.module.scss';
|
||||
|
||||
const secondarySourceLabels: Record<string, string> = {
|
||||
aux1: 'Aux 1',
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
.previewContainer {
|
||||
display: grid;
|
||||
gap: $element-spacing;
|
||||
grid-template-columns: 3fr 2fr;
|
||||
}
|
||||
|
||||
|
||||
.options {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: $element-spacing;
|
||||
}
|
||||
@@ -8,7 +8,7 @@ import { setMessage, useTimerViewControl } from '../../../common/hooks/useSocket
|
||||
|
||||
import TimerPreview from './TimerPreview';
|
||||
|
||||
import style from './MessageControl.module.scss';
|
||||
import style from './TimerViewControl.module.scss';
|
||||
|
||||
export default function TimerControlsPreview() {
|
||||
const { blackout, blink } = useTimerViewControl();
|
||||
@@ -71,12 +71,13 @@ function SecondarySourceControl() {
|
||||
{ value: 'aux3', label: 'Aux 3' },
|
||||
{ value: 'secondary', label: 'Secondary message' },
|
||||
]}
|
||||
onValueChange={(value) => {
|
||||
onValueChange={(value: SecondarySource | null) => {
|
||||
if (value === null) return;
|
||||
// we can only update the remote if it is enabled
|
||||
if (secondarySource !== null) {
|
||||
setMessage.timerSecondarySource(value as SecondarySource);
|
||||
setMessage.timerSecondarySource(value);
|
||||
}
|
||||
setValue(value as SecondarySource);
|
||||
setValue(value);
|
||||
}}
|
||||
/>
|
||||
<Button
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
.content {
|
||||
padding-top: $panel-top-padding;
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
import { memo } from 'react';
|
||||
|
||||
import { CornerExtract } from '../../../common/components/editor-utils/EditorUtils';
|
||||
import * as Editor from '../../../common/components/editor-utils/EditorUtils';
|
||||
import ErrorBoundary from '../../../common/components/error-boundary/ErrorBoundary';
|
||||
import ViewNavigationMenu from '../../../common/components/navigation-menu/ViewNavigationMenu';
|
||||
import ProtectRoute from '../../../common/components/protect-route/ProtectRoute';
|
||||
@@ -9,7 +9,7 @@ import { getIsNavigationLocked } from '../../../externals';
|
||||
|
||||
import PlaybackControl from './PlaybackControl';
|
||||
|
||||
import style from '../../../views/editor/Editor.module.scss';
|
||||
import style from './TimerControlExport.module.scss';
|
||||
|
||||
export default memo(TimerControlExport);
|
||||
function TimerControlExport() {
|
||||
@@ -17,8 +17,8 @@ function TimerControlExport() {
|
||||
|
||||
return (
|
||||
<ProtectRoute permission='editor'>
|
||||
<div className={style.playback} data-testid='panel-timer-control'>
|
||||
{!isExtracted && <CornerExtract onClick={(event) => handleLinks('timercontrol', event)} />}
|
||||
<Editor.Panel data-testid='panel-timer-control'>
|
||||
{!isExtracted && <Editor.CornerExtract onClick={(event) => handleLinks('timercontrol', event)} />}
|
||||
{isExtracted && <ViewNavigationMenu suppressSettings isNavigationLocked={getIsNavigationLocked()} />}
|
||||
|
||||
<div className={style.content}>
|
||||
@@ -26,7 +26,7 @@ function TimerControlExport() {
|
||||
<PlaybackControl />
|
||||
</ErrorBoundary>
|
||||
</div>
|
||||
</div>
|
||||
</Editor.Panel>
|
||||
</ProtectRoute>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,84 +1,10 @@
|
||||
import { use, useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { useSearchParams } from 'react-router';
|
||||
|
||||
import { PresetContext } from '../../common/context/PresetContext';
|
||||
|
||||
/** @url https://developer.mozilla.org/en-US/docs/Web/API/WakeLock */
|
||||
export default function KeepAwake() {
|
||||
const { keepAwake } = useKeepAwakeOptions();
|
||||
const [wakeLockSentinel, setWakeLockSentinel] = useState<WakeLockSentinel | null>(null);
|
||||
|
||||
const removeLock = () => {
|
||||
if (wakeLockSentinel) wakeLockSentinel.release().finally(() => setWakeLockSentinel(null));
|
||||
};
|
||||
|
||||
const acquireLock = () => {
|
||||
if (!wakeLockSentinel || wakeLockSentinel.released) {
|
||||
setWakeLockSentinel(null);
|
||||
navigator.wakeLock
|
||||
.request('screen')
|
||||
.then((sentinel) => {
|
||||
setWakeLockSentinel(sentinel);
|
||||
})
|
||||
.catch(console.error);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
const controller = new AbortController();
|
||||
if (keepAwake) {
|
||||
acquireLock();
|
||||
document.addEventListener(
|
||||
'visibilitychange',
|
||||
() => {
|
||||
if (wakeLockSentinel !== null && document.visibilityState === 'visible') {
|
||||
acquireLock();
|
||||
}
|
||||
},
|
||||
{ signal: controller.signal },
|
||||
);
|
||||
} else {
|
||||
removeLock();
|
||||
}
|
||||
|
||||
return () => {
|
||||
controller.abort();
|
||||
removeLock();
|
||||
};
|
||||
}, [keepAwake]);
|
||||
|
||||
return <></>;
|
||||
}
|
||||
|
||||
const keepAwakeKey = 'keep-awake';
|
||||
|
||||
function getOptionsFromParams(searchParams: URLSearchParams, defaultValues?: URLSearchParams) {
|
||||
// Helper to get value from either source, prioritizing defaultValues
|
||||
return defaultValues?.has(keepAwakeKey) || searchParams.has(keepAwakeKey);
|
||||
}
|
||||
import { useWakelock } from './useWakeLock';
|
||||
|
||||
/**
|
||||
* Hook exposes the keep awake options
|
||||
* Composes the wakelock hook to contain re-renders
|
||||
*/
|
||||
export function useKeepAwakeOptions() {
|
||||
const [searchParams, setSearchParams] = useSearchParams();
|
||||
const maybePreset = use(PresetContext);
|
||||
export default function KeepAwake() {
|
||||
useWakelock();
|
||||
|
||||
const keepAwake = useMemo(() => {
|
||||
const defaultValues = maybePreset ? new URLSearchParams(maybePreset.search) : undefined;
|
||||
return getOptionsFromParams(searchParams, defaultValues);
|
||||
}, [maybePreset, searchParams]);
|
||||
|
||||
const toggleKeepAwake = useCallback(() => {
|
||||
setSearchParams((searchParams) => {
|
||||
if (keepAwake) {
|
||||
searchParams.delete(keepAwakeKey);
|
||||
} else {
|
||||
searchParams.set(keepAwakeKey, '1');
|
||||
}
|
||||
return searchParams;
|
||||
});
|
||||
}, [keepAwake]);
|
||||
|
||||
return { keepAwake, toggleKeepAwake };
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,96 @@
|
||||
import { use, useCallback, useEffect, useMemo, useRef } from 'react';
|
||||
import { useSearchParams } from 'react-router';
|
||||
|
||||
import { PresetContext } from '../../common/context/PresetContext';
|
||||
|
||||
// wakelock is only available in secure contexts
|
||||
// however, that is covered by the navigator check
|
||||
export const canUseWakeLock = typeof window !== 'undefined' && 'wakeLock' in navigator;
|
||||
|
||||
/** @url https://developer.mozilla.org/en-US/docs/Web/API/WakeLock */
|
||||
export function useWakelock() {
|
||||
const { keepAwake } = useKeepAwakeOptions();
|
||||
const wakeLockSentinelRef = useRef<WakeLockSentinel | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
const removeLock = () => {
|
||||
if (wakeLockSentinelRef.current) {
|
||||
wakeLockSentinelRef.current.release().finally(() => {
|
||||
wakeLockSentinelRef.current = null;
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const acquireLock = () => {
|
||||
const sentinel = wakeLockSentinelRef.current;
|
||||
if (!sentinel || sentinel.released) {
|
||||
wakeLockSentinelRef.current = null;
|
||||
|
||||
if (!canUseWakeLock) {
|
||||
return;
|
||||
}
|
||||
|
||||
navigator.wakeLock
|
||||
.request('screen')
|
||||
.then((sentinel) => {
|
||||
wakeLockSentinelRef.current = sentinel;
|
||||
})
|
||||
.catch(console.error);
|
||||
}
|
||||
};
|
||||
|
||||
const controller = new AbortController();
|
||||
if (keepAwake) {
|
||||
acquireLock();
|
||||
document.addEventListener(
|
||||
'visibilitychange',
|
||||
() => {
|
||||
if (wakeLockSentinelRef.current !== null && document.visibilityState === 'visible') {
|
||||
acquireLock();
|
||||
}
|
||||
},
|
||||
{ signal: controller.signal },
|
||||
);
|
||||
} else {
|
||||
removeLock();
|
||||
}
|
||||
|
||||
return () => {
|
||||
controller.abort();
|
||||
removeLock();
|
||||
};
|
||||
}, [keepAwake]);
|
||||
}
|
||||
|
||||
const keepAwakeKey = 'keep-awake';
|
||||
|
||||
function getOptionsFromParams(searchParams: URLSearchParams, defaultValues?: URLSearchParams) {
|
||||
// Helper to get value from either source, prioritizing defaultValues
|
||||
return defaultValues?.has(keepAwakeKey) || searchParams.has(keepAwakeKey);
|
||||
}
|
||||
|
||||
/**
|
||||
* Hook exposes the keep awake options
|
||||
*/
|
||||
export function useKeepAwakeOptions() {
|
||||
const [searchParams, setSearchParams] = useSearchParams();
|
||||
const maybePreset = use(PresetContext);
|
||||
|
||||
const keepAwake = useMemo(() => {
|
||||
const defaultValues = maybePreset ? new URLSearchParams(maybePreset.search) : undefined;
|
||||
return getOptionsFromParams(searchParams, defaultValues);
|
||||
}, [maybePreset, searchParams]);
|
||||
|
||||
const toggleKeepAwake = useCallback(() => {
|
||||
setSearchParams((searchParams) => {
|
||||
if (keepAwake) {
|
||||
searchParams.delete(keepAwakeKey);
|
||||
} else {
|
||||
searchParams.set(keepAwakeKey, '1');
|
||||
}
|
||||
return searchParams;
|
||||
});
|
||||
}, [keepAwake, setSearchParams]);
|
||||
|
||||
return { keepAwake, toggleKeepAwake };
|
||||
}
|
||||
@@ -173,7 +173,7 @@ function Operator({ rundown, rundownMetadata, customFields, settings }: Operator
|
||||
if (isOntimeGroup(entry)) {
|
||||
const { isPast } = rundownMetadata[entry.id];
|
||||
|
||||
const isCurrentParent = selectedEventId ? rundownMetadata[selectedEventId].groupId === entry.id : false;
|
||||
const isCurrentParent = selectedEventId ? rundownMetadata[selectedEventId]?.groupId === entry.id : false;
|
||||
|
||||
if (hidePast && isPast && !isCurrentParent) {
|
||||
return null;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Fragment, useRef, useState } from 'react';
|
||||
import { IoClose } from 'react-icons/io5';
|
||||
import { Dialog } from '@base-ui-components/react/dialog';
|
||||
import { Dialog } from '@base-ui/react/dialog';
|
||||
import { OntimeEvent } from 'ontime-types';
|
||||
|
||||
import Button from '../../../common/components/buttons/Button';
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
@use '@/theme/viewerDefs' as *;
|
||||
@import '../Operator.module.scss';
|
||||
|
||||
.event {
|
||||
opacity: 1;
|
||||
|
||||
@@ -5,8 +5,8 @@ import DelayIndicator from '../../../common/components/delay-indicator/DelayIndi
|
||||
import { useLongPress } from '../../../common/hooks/useLongPress';
|
||||
import { cx, getAccessibleColour } from '../../../common/utils/styleUtils';
|
||||
import { formatDuration, formatTime, useTimeUntilExpectedStart } from '../../../common/utils/time';
|
||||
import RunningTime from '../../viewers/common/running-time/RunningTime';
|
||||
import SuperscriptPeriod from '../../viewers/common/superscript-time/SuperscriptPeriod';
|
||||
import RunningTime from '../../../views/common/running-time/RunningTime';
|
||||
import SuperscriptPeriod from '../../../views/common/superscript-time/SuperscriptPeriod';
|
||||
import type { EditEvent, Subscribed } from '../operator.types';
|
||||
|
||||
import style from './OperatorEvent.module.scss';
|
||||
|
||||
@@ -10,7 +10,7 @@ import {
|
||||
makeOptionsFromCustomFields,
|
||||
} from '../../common/components/view-params-editor/viewParams.utils';
|
||||
import { PresetContext } from '../../common/context/PresetContext';
|
||||
import { isStringBoolean } from '../viewers/common/viewUtils';
|
||||
import { isStringBoolean } from '../../views/common/viewUtils';
|
||||
|
||||
export const getOperatorOptions = (customFields: CustomFields, timeFormat: string): ViewOption[] => {
|
||||
const fieldOptions = makeOptionsFromCustomFields(customFields, [
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { CustomFields, MaybeString, OntimeEvent } from 'ontime-types';
|
||||
|
||||
import { getPropertyValue } from '../viewers/common/viewUtils';
|
||||
import { getPropertyValue } from '../../views/common/viewUtils';
|
||||
|
||||
import type { Subscribed } from './operator.types';
|
||||
|
||||
|
||||
@@ -28,7 +28,7 @@ import { useEntry } from '../../../common/hooks-query/useRundown';
|
||||
import { getOffsetState, getOffsetText } from '../../../common/utils/offset';
|
||||
import { cx, enDash, timerPlaceholder } from '../../../common/utils/styleUtils';
|
||||
import { formatTime } from '../../../common/utils/time';
|
||||
import SuperscriptPeriod from '../../viewers/common/superscript-time/SuperscriptPeriod';
|
||||
import SuperscriptPeriod from '../../../views/common/superscript-time/SuperscriptPeriod';
|
||||
import { calculateEndAndDaySpan, formatDueTime } from '../overview.utils';
|
||||
|
||||
import { OverUnder, TimeColumn, WrappedInTimeColumn } from './TimeLayout';
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Fragment, lazy, useCallback, useEffect, useRef, useState } from 'react';
|
||||
import { Fragment, lazy, useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { TbFlagFilled } from 'react-icons/tb';
|
||||
import {
|
||||
closestCenter,
|
||||
@@ -36,7 +36,6 @@ import { useEntryActions } from '../../common/hooks/useEntryAction';
|
||||
import useFollowComponent from '../../common/hooks/useFollowComponent';
|
||||
import { useRundownEditor } from '../../common/hooks/useSocket';
|
||||
import { useEntryCopy } from '../../common/stores/entryCopyStore';
|
||||
import { cloneEvent } from '../../common/utils/clone';
|
||||
import { lastMetadataKey, RundownMetadataObject } from '../../common/utils/rundownMetadata';
|
||||
import { AppMode, sessionKeys } from '../../ontimeConfig';
|
||||
|
||||
@@ -58,6 +57,8 @@ interface RundownProps {
|
||||
}
|
||||
|
||||
export default function Rundown({ data, rundownMetadata }: RundownProps) {
|
||||
'use memo';
|
||||
|
||||
const { order, entries, id } = data;
|
||||
// we create a copy of the rundown with a data structured aligned with what dnd-kit needs
|
||||
const featureData = useRundownEditor();
|
||||
@@ -68,17 +69,20 @@ export default function Rundown({ data, rundownMetadata }: RundownProps) {
|
||||
key: `rundown.${id}-editor-collapsed-groups`,
|
||||
defaultValue: [],
|
||||
});
|
||||
const collapsedGroupSet = useMemo(() => new Set(collapsedGroups), [collapsedGroups]);
|
||||
|
||||
const { addEntry, deleteEntry, move, reorderEntry } = useEntryActions();
|
||||
|
||||
const { entryCopyId, setEntryCopyId } = useEntryCopy();
|
||||
const { addEntry, clone, deleteEntry, move, reorderEntry } = useEntryActions();
|
||||
const setEntryCopyId = useEntryCopy((state) => state.setEntryCopyId);
|
||||
|
||||
// cursor
|
||||
const [editorMode] = useSessionStorage<AppMode>({
|
||||
key: sessionKeys.editorMode,
|
||||
defaultValue: AppMode.Edit,
|
||||
});
|
||||
const { clearSelectedEvents, setSelectedEvents, cursor } = useEventSelection();
|
||||
|
||||
const clearSelectedEvents = useEventSelection((state) => state.clearSelectedEvents);
|
||||
const setSelectedEvents = useEventSelection((state) => state.setSelectedEvents);
|
||||
const cursor = useEventSelection((state) => state.cursor);
|
||||
|
||||
const cursorRef = useRef<HTMLDivElement | null>(null);
|
||||
const scrollRef = useRef<HTMLDivElement | null>(null);
|
||||
@@ -105,20 +109,30 @@ export default function Rundown({ data, rundownMetadata }: RundownProps) {
|
||||
);
|
||||
|
||||
const insertCopyAtId = useCallback(
|
||||
(atId: string | null, copyId: string | null, above = false) => {
|
||||
const adjustedCursor = above ? getPreviousNormal(entries, order, atId ?? '').entry?.id ?? null : atId;
|
||||
if (copyId === null) {
|
||||
(atId: string | null, above = false) => {
|
||||
// lazily get the value from the store
|
||||
const { entryCopyId } = useEntryCopy.getState();
|
||||
if (entryCopyId === null || !entries[entryCopyId]) {
|
||||
// we cant clone without selection
|
||||
return;
|
||||
}
|
||||
const cloneEntry = entries[copyId];
|
||||
if (cloneEntry?.type === SupportedEntry.Event) {
|
||||
//if we don't have a cursor add the new event on top
|
||||
const newEvent = cloneEvent(cloneEntry);
|
||||
addEntry(newEvent, { after: adjustedCursor ?? undefined });
|
||||
|
||||
let normalisedAtId = atId;
|
||||
|
||||
const elementToCopy = entries[entryCopyId];
|
||||
const refElement = atId ? entries[atId] : undefined;
|
||||
|
||||
if (refElement && 'parent' in refElement && refElement.parent && elementToCopy.type === SupportedEntry.Group) {
|
||||
normalisedAtId = refElement.parent;
|
||||
}
|
||||
|
||||
clone(entryCopyId, {
|
||||
after: above ? undefined : normalisedAtId ?? undefined,
|
||||
// if we don't have a cursor add the new event on top
|
||||
before: above ? normalisedAtId ?? undefined : undefined,
|
||||
});
|
||||
},
|
||||
[addEntry, order, entries],
|
||||
[entries, clone],
|
||||
);
|
||||
|
||||
/**
|
||||
@@ -200,9 +214,9 @@ export default function Rundown({ data, rundownMetadata }: RundownProps) {
|
||||
*/
|
||||
const getIsCollapsed = useCallback(
|
||||
(groupId: EntryId): boolean => {
|
||||
return Boolean(collapsedGroups.find((id) => id === groupId));
|
||||
return collapsedGroupSet.has(groupId);
|
||||
},
|
||||
[collapsedGroups],
|
||||
[collapsedGroupSet],
|
||||
);
|
||||
|
||||
/**
|
||||
@@ -300,12 +314,8 @@ export default function Rundown({ data, rundownMetadata }: RundownProps) {
|
||||
],
|
||||
|
||||
['mod + C', () => setEntryCopyId(cursor)],
|
||||
['mod + V', () => insertCopyAtId(cursor, entryCopyId)],
|
||||
[
|
||||
'mod + shift + V',
|
||||
() => insertCopyAtId(cursor, entryCopyId, true),
|
||||
{ preventDefault: true, usePhysicalKeys: true },
|
||||
],
|
||||
['mod + V', () => insertCopyAtId(cursor)],
|
||||
['mod + shift + V', () => insertCopyAtId(cursor, true), { preventDefault: true, usePhysicalKeys: true }],
|
||||
|
||||
['alt + backspace', () => deleteAtCursor(cursor), { preventDefault: true, usePhysicalKeys: true }],
|
||||
]);
|
||||
@@ -395,7 +405,7 @@ export default function Rundown({ data, rundownMetadata }: RundownProps) {
|
||||
}
|
||||
|
||||
// keep copy of the current state in case we need to revert
|
||||
const currentEntries = structuredClone(sortableData);
|
||||
const currentEntries = [...sortableData];
|
||||
// we keep a copy of the state as a hack to handle inconsistencies between dnd-kit and async store updates
|
||||
setSortableData((currentEntries) => {
|
||||
return reorderArray(currentEntries, fromIndex, toIndex);
|
||||
@@ -438,9 +448,12 @@ export default function Rundown({ data, rundownMetadata }: RundownProps) {
|
||||
return <RundownEmpty handleAddNew={(type: SupportedEntry) => addEntry({ type })} />;
|
||||
}
|
||||
|
||||
// 1. gather presentation options
|
||||
// gather presentation options
|
||||
const isEditMode = editorMode === AppMode.Edit;
|
||||
|
||||
// gather rundown wide data
|
||||
const lastEntryId = order.at(-1);
|
||||
|
||||
return (
|
||||
<div className={style.rundownContainer} ref={scrollRef} data-testid='rundown'>
|
||||
<DndContext
|
||||
@@ -509,7 +522,7 @@ export default function Rundown({ data, rundownMetadata }: RundownProps) {
|
||||
const groupColour = entryMetadata.groupColour === '' ? '#9d9d9d' : entryMetadata.groupColour;
|
||||
|
||||
const isFirst = index === 0;
|
||||
const isLast = entryId === order.at(-1);
|
||||
const isLast = entryId === lastEntryId;
|
||||
|
||||
/**
|
||||
* We need to provide the parent ID for the QuickAdd components
|
||||
|
||||
@@ -1,16 +1,4 @@
|
||||
import {
|
||||
isOntimeDelay,
|
||||
isOntimeEvent,
|
||||
isOntimeMilestone,
|
||||
OntimeEntry,
|
||||
OntimeEvent,
|
||||
Playback,
|
||||
SupportedEntry,
|
||||
} from 'ontime-types';
|
||||
|
||||
import { useEntryActions } from '../../common/hooks/useEntryAction';
|
||||
import useMemoisedFn from '../../common/hooks/useMemoisedFn';
|
||||
import { cloneEvent } from '../../common/utils/clone';
|
||||
import { isOntimeDelay, isOntimeEvent, isOntimeMilestone, OntimeEntry, Playback, SupportedEntry } from 'ontime-types';
|
||||
|
||||
import RundownDelay from './rundown-delay/RundownDelay';
|
||||
import RundownEvent from './rundown-event/RundownEvent';
|
||||
@@ -44,13 +32,6 @@ export default function RundownEntry({
|
||||
totalGap,
|
||||
isLinkedToLoaded,
|
||||
}: RundownEntryProps) {
|
||||
const { addEntry } = useEntryActions();
|
||||
|
||||
const createCloneEvent = useMemoisedFn(() => {
|
||||
const newEvent = cloneEvent(data as OntimeEvent);
|
||||
addEntry(newEvent, { after: data.id });
|
||||
});
|
||||
|
||||
if (isOntimeEvent(data)) {
|
||||
return (
|
||||
<RundownEvent
|
||||
@@ -83,7 +64,6 @@ export default function RundownEntry({
|
||||
dayOffset={data.dayOffset}
|
||||
totalGap={totalGap}
|
||||
isLinkedToLoaded={isLinkedToLoaded}
|
||||
createCloneEvent={createCloneEvent}
|
||||
hasTriggers={data.triggers.length > 0}
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
@use '../../views/editor/EditorMixin' as editor;
|
||||
|
||||
.rundownExport {
|
||||
height: 100%;
|
||||
flex: 1 1 auto; /* flex-grow: 1, flex-shrink: 1, flex-basis: auto */
|
||||
@@ -23,7 +21,7 @@
|
||||
}
|
||||
|
||||
.list {
|
||||
@include editor.panel;
|
||||
display: flex;
|
||||
height: inherit;
|
||||
padding-inline: 0;
|
||||
box-shadow: $box-shadow-right;
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { memo } from 'react';
|
||||
import { useSessionStorage } from '@mantine/hooks';
|
||||
|
||||
import { CornerExtract } from '../../common/components/editor-utils/EditorUtils';
|
||||
import * as Editor from '../../common/components/editor-utils/EditorUtils';
|
||||
import ErrorBoundary from '../../common/components/error-boundary/ErrorBoundary';
|
||||
import ViewNavigationMenu from '../../common/components/navigation-menu/ViewNavigationMenu';
|
||||
import ProtectRoute from '../../common/components/protect-route/ProtectRoute';
|
||||
@@ -58,14 +58,14 @@ function RundownExport() {
|
||||
<FinderPlacement />
|
||||
{isExtracted && <ViewNavigationMenu suppressSettings isNavigationLocked={getIsNavigationLocked()} />}
|
||||
<div className={style.rundown}>
|
||||
<div className={style.list}>
|
||||
<Editor.Panel className={style.list}>
|
||||
<ErrorBoundary>
|
||||
{!isExtracted && <CornerExtract onClick={(event) => handleLinks('rundown', event)} />}
|
||||
{!isExtracted && <Editor.CornerExtract onClick={(event) => handleLinks('rundown', event)} />}
|
||||
<RundownContextMenu>
|
||||
<RundownWrapper />
|
||||
</RundownContextMenu>
|
||||
</ErrorBoundary>
|
||||
</div>
|
||||
</Editor.Panel>
|
||||
{!hideSideBar && (
|
||||
<div className={style.side}>
|
||||
<ErrorBoundary>
|
||||
|
||||
@@ -102,7 +102,10 @@ function EventEditorTimes({
|
||||
<Editor.Label htmlFor='endAction'>End Action</Editor.Label>
|
||||
<Select
|
||||
value={endAction}
|
||||
onValueChange={(value) => handleSubmit('endAction', value)}
|
||||
onValueChange={(value: EndAction | null) => {
|
||||
if (value === null) return;
|
||||
handleSubmit('endAction', value);
|
||||
}}
|
||||
options={[
|
||||
{ value: EndAction.None, label: 'None' },
|
||||
{ value: EndAction.LoadNext, label: 'Load next event' },
|
||||
@@ -139,7 +142,10 @@ function EventEditorTimes({
|
||||
<Editor.Label htmlFor='timerType'>Timer Type</Editor.Label>
|
||||
<Select
|
||||
value={timerType}
|
||||
onValueChange={(value) => handleSubmit('timerType', value)}
|
||||
onValueChange={(value: TimerType | null) => {
|
||||
if (value === null) return;
|
||||
handleSubmit('timerType', value);
|
||||
}}
|
||||
options={[
|
||||
{ value: TimerType.CountDown, label: 'Count down' },
|
||||
{ value: TimerType.CountUp, label: 'Count up' },
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { memo } from 'react';
|
||||
import { IoAdd } from 'react-icons/io5';
|
||||
import { Toolbar } from '@base-ui-components/react/toolbar';
|
||||
import { Toolbar } from '@base-ui/react/toolbar';
|
||||
import { MaybeString, SupportedEntry } from 'ontime-types';
|
||||
|
||||
import Button from '../../../../common/components/buttons/Button';
|
||||
|
||||
@@ -56,7 +56,6 @@ interface RundownEventProps {
|
||||
dayOffset: number;
|
||||
totalGap: number;
|
||||
isLinkedToLoaded: boolean;
|
||||
createCloneEvent: () => void;
|
||||
hasTriggers: boolean;
|
||||
}
|
||||
|
||||
@@ -91,10 +90,9 @@ export default function RundownEvent({
|
||||
totalGap,
|
||||
isLinkedToLoaded,
|
||||
hasTriggers,
|
||||
createCloneEvent,
|
||||
}: RundownEventProps) {
|
||||
const { selectedEventId, setSelectedEventId, clearSelectedEventId } = useEventIdSwapping();
|
||||
const { updateEntry, batchUpdateEvents, deleteEntry, groupEntries, swapEvents } = useEntryActions();
|
||||
const { updateEntry, batchUpdateEvents, clone, deleteEntry, groupEntries, swapEvents } = useEntryActions();
|
||||
|
||||
const { selectedEvents, unselect, setSelectedEvents, clearSelectedEvents } = useEventSelection();
|
||||
const handleRef = useRef<null | HTMLSpanElement>(null);
|
||||
@@ -172,7 +170,7 @@ export default function RundownEvent({
|
||||
type: 'item',
|
||||
label: 'Clone',
|
||||
icon: IoDuplicateOutline,
|
||||
onClick: createCloneEvent,
|
||||
onClick: () => clone(eventId, { after: eventId }),
|
||||
},
|
||||
{ type: 'divider' },
|
||||
{
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { memo } from 'react';
|
||||
import { Toggle } from '@base-ui-components/react/toggle';
|
||||
import { ToggleGroup } from '@base-ui-components/react/toggle-group';
|
||||
import { Toolbar } from '@base-ui-components/react/toolbar';
|
||||
import { Toggle } from '@base-ui/react/toggle';
|
||||
import { ToggleGroup } from '@base-ui/react/toggle-group';
|
||||
import { Toolbar } from '@base-ui/react/toolbar';
|
||||
import { useSessionStorage } from '@mantine/hooks';
|
||||
import { OffsetMode } from 'ontime-types';
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { memo } from 'react';
|
||||
import { Toggle } from '@base-ui-components/react/toggle';
|
||||
import { ToggleGroup } from '@base-ui-components/react/toggle-group';
|
||||
import { Toolbar } from '@base-ui-components/react/toolbar';
|
||||
import { Toggle } from '@base-ui/react/toggle';
|
||||
import { ToggleGroup } from '@base-ui/react/toggle-group';
|
||||
import { Toolbar } from '@base-ui/react/toolbar';
|
||||
import { useSessionStorage } from '@mantine/hooks';
|
||||
import { OffsetMode } from 'ontime-types';
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { memo, useCallback } from 'react';
|
||||
import { IoTrash } from 'react-icons/io5';
|
||||
import { Toolbar } from '@base-ui-components/react/toolbar';
|
||||
import { Toolbar } from '@base-ui/react/toolbar';
|
||||
import { useDisclosure, useSessionStorage } from '@mantine/hooks';
|
||||
|
||||
import Button from '../../../common/components/buttons/Button';
|
||||
|
||||
@@ -13,7 +13,7 @@ import Input from '../../common/components/input/input/Input';
|
||||
import Select from '../../common/components/select/Select';
|
||||
import Switch from '../../common/components/switch/Switch';
|
||||
import { useUpdateUrlPreset } from '../../common/hooks-query/useUrlPresets';
|
||||
import copyToClipboard from '../../common/utils/copyToClipboard';
|
||||
import { safeCopyToClipboard } from '../../common/utils/copyToClipboard';
|
||||
import { preventEscape } from '../../common/utils/keyEvent';
|
||||
import { isUrlSafe } from '../../common/utils/regex';
|
||||
import { isOntimeCloud, serverURL } from '../../externals';
|
||||
@@ -125,7 +125,7 @@ export default function GenerateLinkForm({ hostOptions, pathOptions, presets, is
|
||||
lockNav: options.lockNav,
|
||||
preset: urlPreset.alias,
|
||||
});
|
||||
await copyToClipboard(url);
|
||||
await safeCopyToClipboard(url);
|
||||
setUrl(url);
|
||||
} else {
|
||||
const presetPath = options.path.startsWith('preset-') ? options.path.replace('preset-', '') : undefined;
|
||||
@@ -143,7 +143,7 @@ export default function GenerateLinkForm({ hostOptions, pathOptions, presets, is
|
||||
preset: presetPath,
|
||||
});
|
||||
|
||||
await copyToClipboard(url);
|
||||
await safeCopyToClipboard(url);
|
||||
setUrl(url);
|
||||
}
|
||||
reset(options, {
|
||||
@@ -179,7 +179,10 @@ export default function GenerateLinkForm({ hostOptions, pathOptions, presets, is
|
||||
<Select
|
||||
options={hostOptions}
|
||||
value={watch('baseUrl')}
|
||||
onValueChange={(value) => setValue('baseUrl', value)}
|
||||
onValueChange={(value: string | null) => {
|
||||
if (value === null) return;
|
||||
setValue('baseUrl', value);
|
||||
}}
|
||||
/>
|
||||
</Panel.ListItem>
|
||||
)}
|
||||
@@ -191,7 +194,10 @@ export default function GenerateLinkForm({ hostOptions, pathOptions, presets, is
|
||||
<Select
|
||||
options={pathOptions}
|
||||
value={watch('path')}
|
||||
onValueChange={(value) => setValue('path', value, { shouldDirty: true })}
|
||||
onValueChange={(value: OntimeView | string | null) => {
|
||||
if (value === null) return;
|
||||
setValue('path', value, { shouldDirty: true });
|
||||
}}
|
||||
/>
|
||||
</Panel.ListItem>
|
||||
)}
|
||||
|
||||
@@ -7,6 +7,10 @@ $component-border-radius-md: 3px;
|
||||
$component-border-radius-sm: 2px;
|
||||
$component-border-radius-full: 99px;
|
||||
|
||||
$panel-border-radius: 8px;
|
||||
$panel-gap: 0.5rem;
|
||||
$panel-top-padding: 1.5rem;
|
||||
|
||||
// indexes
|
||||
$zindex-floating: 100;
|
||||
$zindex-nav: 200;
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
@forward 'ontimeColours';
|
||||
@forward 'ontimeStyles';
|
||||
@forward 'mixins';
|
||||
@@ -14,10 +14,10 @@ import { useBackstageSocket, useClock } from '../../common/hooks/useSocket';
|
||||
import { useWindowTitle } from '../../common/hooks/useWindowTitle';
|
||||
import { cx, timerPlaceholderMin } from '../../common/utils/styleUtils';
|
||||
import { formatTime, getDefaultFormat } from '../../common/utils/time';
|
||||
import SuperscriptTime from '../../features/viewers/common/superscript-time/SuperscriptTime';
|
||||
import { useTranslation } from '../../translation/TranslationProvider';
|
||||
import Loader from '../common/loader/Loader';
|
||||
import ScheduleExport from '../common/schedule/ScheduleExport';
|
||||
import SuperscriptTime from '../common/superscript-time/SuperscriptTime';
|
||||
|
||||
import { getBackstageOptions, useBackstageOptions } from './backstage.options';
|
||||
import { getCardData, getIsPendingStart, getShowProgressBar, isOvertime } from './backstage.utils';
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { MaybeNumber, OntimeEvent, Playback, TimerPhase } from 'ontime-types';
|
||||
|
||||
import { enDash } from '../../common/utils/styleUtils';
|
||||
import { getPropertyValue } from '../../features/viewers/common/viewUtils';
|
||||
import { getPropertyValue } from '../common/viewUtils';
|
||||
|
||||
/**
|
||||
* Whether the current time is in overtime
|
||||
|
||||
+2
-2
@@ -4,8 +4,8 @@
|
||||
|
||||
import { MaybeNumber } from 'ontime-types';
|
||||
|
||||
import { formatTime } from '../../../../common/utils/time';
|
||||
import { FORMAT_12, FORMAT_24 } from '../../../../viewerConfig';
|
||||
import { formatTime } from '../../../common/utils/time';
|
||||
import { FORMAT_12, FORMAT_24 } from '../../../viewerConfig';
|
||||
import SuperscriptTime from '../superscript-time/SuperscriptTime';
|
||||
|
||||
interface ClockTimeProps {
|
||||
+1
-1
@@ -5,7 +5,7 @@
|
||||
import { MaybeNumber } from 'ontime-types';
|
||||
import { removeLeadingZero, removeSeconds } from 'ontime-utils';
|
||||
|
||||
import { formattedTime } from '../../../overview/overview.utils';
|
||||
import { formattedTime } from '../../../features/overview/overview.utils';
|
||||
|
||||
interface RunningTimeProps {
|
||||
value: MaybeNumber;
|
||||
@@ -1,4 +1,14 @@
|
||||
import { createContext, PropsWithChildren, RefObject, use, useEffect, useLayoutEffect, useRef, useState } from 'react';
|
||||
import {
|
||||
createContext,
|
||||
PropsWithChildren,
|
||||
RefObject,
|
||||
use,
|
||||
useCallback,
|
||||
useEffect,
|
||||
useLayoutEffect,
|
||||
useRef,
|
||||
useState,
|
||||
} from 'react';
|
||||
import { EntryId, isOntimeEvent, OntimeEntry, OntimeEvent } from 'ontime-types';
|
||||
|
||||
import { usePartialRundown } from '../../../common/hooks-query/useRundown';
|
||||
@@ -22,14 +32,20 @@ interface ScheduleProviderProps {
|
||||
|
||||
export const ScheduleProvider = ({ children, selectedEventId }: PropsWithChildren<ScheduleProviderProps>) => {
|
||||
const { cycleInterval, stopCycle, filter } = useScheduleOptions();
|
||||
const { data: events } = usePartialRundown((entry: ExtendedEntry<OntimeEntry>) => {
|
||||
if (filter) {
|
||||
// custom keys are prepended with custom-
|
||||
const customKey = filter.startsWith('custom-') ? filter.slice('custom-'.length) : filter;
|
||||
return isOntimeEvent(entry) && Boolean(entry.custom[customKey]);
|
||||
}
|
||||
return isOntimeEvent(entry);
|
||||
});
|
||||
|
||||
const filterCallback = useCallback(
|
||||
(entry: ExtendedEntry<OntimeEntry>) => {
|
||||
if (filter) {
|
||||
// custom keys are prepended with custom-
|
||||
const customKey = filter.startsWith('custom-') ? filter.slice('custom-'.length) : filter;
|
||||
return isOntimeEvent(entry) && Boolean(entry.custom[customKey]);
|
||||
}
|
||||
return isOntimeEvent(entry);
|
||||
},
|
||||
[filter],
|
||||
);
|
||||
|
||||
const { data: events } = usePartialRundown(filterCallback);
|
||||
|
||||
const [firstIndex, setFirstIndex] = useState(-1);
|
||||
const [numPages, setNumPages] = useState(0);
|
||||
|
||||
@@ -5,7 +5,7 @@ import { getOffsetState } from '../../../common/utils/offset';
|
||||
import { ExtendedEntry } from '../../../common/utils/rundownMetadata';
|
||||
import { cx } from '../../../common/utils/styleUtils';
|
||||
import { formatTime, getExpectedTimesFromExtendedEvent } from '../../../common/utils/time';
|
||||
import SuperscriptPeriod from '../../../features/viewers/common/superscript-time/SuperscriptPeriod';
|
||||
import SuperscriptPeriod from '../superscript-time/SuperscriptPeriod';
|
||||
|
||||
import { useScheduleOptions } from './schedule.options';
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ import { useSearchParams } from 'react-router';
|
||||
import { SelectOption } from '../../../common/components/select/Select';
|
||||
import { OptionTitle } from '../../../common/components/view-params-editor/constants';
|
||||
import type { ViewOption } from '../../../common/components/view-params-editor/viewParams.types';
|
||||
import { isStringBoolean } from '../../../features/viewers/common/viewUtils';
|
||||
import { isStringBoolean } from '../viewUtils';
|
||||
|
||||
export const getScheduleOptions = (customFieldOptions: SelectOption[]): ViewOption => ({
|
||||
title: OptionTitle.Schedule,
|
||||
|
||||
+3
-11
@@ -1,8 +1,8 @@
|
||||
import { MaybeNumber, MaybeString, OntimeEvent, TimerState, TimerType } from 'ontime-types';
|
||||
import { MILLIS_PER_MINUTE, MILLIS_PER_SECOND, millisToString, removeLeadingZero, removeSeconds } from 'ontime-utils';
|
||||
|
||||
import { timerPlaceholder, timerPlaceholderMin } from '../../../common/utils/styleUtils';
|
||||
import { formatTime } from '../../../common/utils/time';
|
||||
import { timerPlaceholder, timerPlaceholderMin } from '../../common/utils/styleUtils';
|
||||
import { formatTime } from '../../common/utils/time';
|
||||
|
||||
/**
|
||||
* Gathers all options that affect which timer is displayed and selects the correct data source to display
|
||||
@@ -11,7 +11,6 @@ import { formatTime } from '../../../common/utils/time';
|
||||
export function getTimerByType(
|
||||
freezeEnd: boolean,
|
||||
timerTypeNow: TimerType,
|
||||
countToEndNow: boolean,
|
||||
clock: number,
|
||||
timerObject: Pick<TimerState, 'current' | 'elapsed'>,
|
||||
timerTypeOverride?: TimerType,
|
||||
@@ -22,13 +21,6 @@ export function getTimerByType(
|
||||
|
||||
const viewTimerType = timerTypeOverride ?? timerTypeNow;
|
||||
|
||||
if (countToEndNow) {
|
||||
if (timerObject.current === null) {
|
||||
return null;
|
||||
}
|
||||
return freezeEnd ? Math.max(timerObject.current, 0) : timerObject.current;
|
||||
}
|
||||
|
||||
switch (viewTimerType) {
|
||||
case TimerType.CountDown:
|
||||
if (timerObject.current === null) {
|
||||
@@ -36,7 +28,7 @@ export function getTimerByType(
|
||||
}
|
||||
return freezeEnd ? Math.max(timerObject.current, 0) : timerObject.current;
|
||||
case TimerType.CountUp:
|
||||
return Math.abs(timerObject.elapsed ?? 0);
|
||||
return timerObject.elapsed;
|
||||
case TimerType.Clock:
|
||||
return clock;
|
||||
case TimerType.None:
|
||||
@@ -11,9 +11,9 @@ import { useClock } from '../../common/hooks/useSocket';
|
||||
import { useWindowTitle } from '../../common/hooks/useWindowTitle';
|
||||
import { ExtendedEntry } from '../../common/utils/rundownMetadata';
|
||||
import { formatTime, getDefaultFormat } from '../../common/utils/time';
|
||||
import SuperscriptTime from '../../features/viewers/common/superscript-time/SuperscriptTime';
|
||||
import { useTranslation } from '../../translation/TranslationProvider';
|
||||
import Loader from '../common/loader/Loader';
|
||||
import SuperscriptTime from '../common/superscript-time/SuperscriptTime';
|
||||
|
||||
import { getCountdownOptions, useCountdownOptions } from './countdown.options';
|
||||
import { getOrderedSubscriptions } from './countdown.utils';
|
||||
|
||||
@@ -6,7 +6,7 @@ import { EntryId, PlayableEvent } from 'ontime-types';
|
||||
import Button from '../../common/components/buttons/Button';
|
||||
import { ExtendedEntry } from '../../common/utils/rundownMetadata';
|
||||
import { cx } from '../../common/utils/styleUtils';
|
||||
import ClockTime from '../../features/viewers/common/clock-time/ClockTime';
|
||||
import ClockTime from '../common/clock-time/ClockTime';
|
||||
|
||||
import { makeSubscriptionsUrl } from './countdown.utils';
|
||||
|
||||
|
||||
@@ -13,9 +13,9 @@ import { ExtendedEntry } from '../../common/utils/rundownMetadata';
|
||||
import { cx } from '../../common/utils/styleUtils';
|
||||
import { throttle } from '../../common/utils/throttle';
|
||||
import FollowButton from '../../features/operator/follow-button/FollowButton';
|
||||
import ClockTime from '../../features/viewers/common/clock-time/ClockTime';
|
||||
import SuperscriptTime from '../../features/viewers/common/superscript-time/SuperscriptTime';
|
||||
import { getPropertyValue } from '../../features/viewers/common/viewUtils';
|
||||
import ClockTime from '../common/clock-time/ClockTime';
|
||||
import SuperscriptTime from '../common/superscript-time/SuperscriptTime';
|
||||
import { getPropertyValue } from '../common/viewUtils';
|
||||
|
||||
import { useCountdownOptions } from './countdown.options';
|
||||
import {
|
||||
|
||||
@@ -8,8 +8,8 @@ import { useExpectedStartData } from '../../common/hooks/useSocket';
|
||||
import useReport from '../../common/hooks-query/useReport';
|
||||
import { ExtendedEntry } from '../../common/utils/rundownMetadata';
|
||||
import { cx } from '../../common/utils/styleUtils';
|
||||
import SuperscriptTime from '../../features/viewers/common/superscript-time/SuperscriptTime';
|
||||
import { getPropertyValue } from '../../features/viewers/common/viewUtils';
|
||||
import SuperscriptTime from '../common/superscript-time/SuperscriptTime';
|
||||
import { getPropertyValue } from '../common/viewUtils';
|
||||
|
||||
import { useCountdownOptions } from './countdown.options';
|
||||
import { useSubscriptionDisplayData } from './countdown.utils';
|
||||
|
||||
@@ -7,7 +7,7 @@ import { OptionTitle } from '../../common/components/view-params-editor/constant
|
||||
import { ViewOption } from '../../common/components/view-params-editor/viewParams.types';
|
||||
import { makeOptionsFromCustomFields } from '../../common/components/view-params-editor/viewParams.utils';
|
||||
import { PresetContext } from '../../common/context/PresetContext';
|
||||
import { isStringBoolean } from '../../features/viewers/common/viewUtils';
|
||||
import { isStringBoolean } from '../common/viewUtils';
|
||||
|
||||
export const getCountdownOptions = (
|
||||
timeFormat: string,
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
@use './cuesheetRows' as rows;
|
||||
|
||||
$table-font-size: 1rem;
|
||||
$table-header-font-size: calc(1rem - 2px);
|
||||
|
||||
@include rows.cuesheet-row-columns($table-header-font-size);
|
||||
|
||||
.cuesheet {
|
||||
font-size: $table-font-size;
|
||||
font-weight: 400;
|
||||
@@ -49,27 +53,6 @@ $table-header-font-size: calc(1rem - 2px);
|
||||
}
|
||||
}
|
||||
|
||||
.actionColumn {
|
||||
padding-inline: 0.5rem;
|
||||
padding-top: 0.5rem !important; // fighting the table styles
|
||||
background-color: transparent;
|
||||
display: flex;
|
||||
align-items: start;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.indexColumn {
|
||||
display: flex;
|
||||
align-items: start;
|
||||
justify-content: end;
|
||||
|
||||
min-width: 3em; // allow for 3-digit numbers
|
||||
font-size: $table-header-font-size;
|
||||
line-height: 2rem; // match input height
|
||||
background-color: $gray-1300; // will be overridden inline
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.tableHeader {
|
||||
line-height: 2rem;
|
||||
position: sticky;
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
@use '@/theme/themeTokens' as *;
|
||||
|
||||
@mixin cuesheet-row-columns($header-font-size: calc(1rem - 2px)) {
|
||||
.actionColumn {
|
||||
padding-inline: 0.5rem;
|
||||
padding-top: 0.5rem !important; // fighting the table styles
|
||||
background-color: transparent;
|
||||
display: flex;
|
||||
align-items: start;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.indexColumn {
|
||||
display: flex;
|
||||
align-items: start;
|
||||
justify-content: end;
|
||||
|
||||
min-width: 3em; // allow for 3-digit numbers
|
||||
font-size: $header-font-size;
|
||||
line-height: 2rem; // match input height
|
||||
background-color: $gray-1300; // will be overridden inline
|
||||
font-weight: 600;
|
||||
}
|
||||
}
|
||||
+2
-1
@@ -1,4 +1,5 @@
|
||||
@import '../CuesheetTable.module.scss';
|
||||
@use '../cuesheetRows' as rows;
|
||||
@include rows.cuesheet-row-columns;
|
||||
|
||||
.delayRow {
|
||||
color: $ontime-delay-text;
|
||||
|
||||
+2
-1
@@ -1,4 +1,5 @@
|
||||
@import "../CuesheetTable.module.scss";
|
||||
@use '../cuesheetRows' as rows;
|
||||
@include rows.cuesheet-row-columns;
|
||||
|
||||
.eventRow {
|
||||
background: color-mix(in srgb, transparent 92%, var(--user-bg, $gray-500) 8%);
|
||||
|
||||
+2
-1
@@ -1,4 +1,5 @@
|
||||
@import '../CuesheetTable.module.scss';
|
||||
@use '../cuesheetRows' as rows;
|
||||
@include rows.cuesheet-row-columns;
|
||||
|
||||
.groupRow {
|
||||
margin-top: 2em;
|
||||
|
||||
+2
-1
@@ -1,4 +1,5 @@
|
||||
@import "../CuesheetTable.module.scss";
|
||||
@use '../cuesheetRows' as rows;
|
||||
@include rows.cuesheet-row-columns;
|
||||
|
||||
.milestoneRow {
|
||||
background: color-mix(in srgb, transparent 98%, var(--user-bg, $gray-500) 2%);
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
import { Toolbar } from '@base-ui-components/react/toolbar';
|
||||
import { Toolbar } from '@base-ui/react/toolbar';
|
||||
import { useDisclosure } from '@mantine/hooks';
|
||||
import { OntimeView } from 'ontime-types';
|
||||
|
||||
|
||||
+4
-4
@@ -1,9 +1,9 @@
|
||||
import { ReactNode, use } from 'react';
|
||||
import { IoChevronDown, IoOptions, IoSettingsOutline } from 'react-icons/io5';
|
||||
import { Popover } from '@base-ui-components/react/popover';
|
||||
import { Toggle } from '@base-ui-components/react/toggle';
|
||||
import { ToggleGroup } from '@base-ui-components/react/toggle-group';
|
||||
import { Toolbar } from '@base-ui-components/react/toolbar';
|
||||
import { Popover } from '@base-ui/react/popover';
|
||||
import { Toggle } from '@base-ui/react/toggle';
|
||||
import { ToggleGroup } from '@base-ui/react/toggle-group';
|
||||
import { Toolbar } from '@base-ui/react/toolbar';
|
||||
import { useSessionStorage } from '@mantine/hooks';
|
||||
import type { Column } from '@tanstack/react-table';
|
||||
|
||||
|
||||
@@ -1,24 +1,5 @@
|
||||
@use './EditorMixin' as editor;
|
||||
|
||||
$min-playback-width: 27rem;
|
||||
$max-playback-width: 30rem;
|
||||
$panel-gap: 0.5rem;
|
||||
|
||||
.mainContainer {
|
||||
background-color: $ui-black;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
color: $ui-white;
|
||||
padding: 0.5rem;
|
||||
|
||||
display: grid;
|
||||
grid-template-columns: auto;
|
||||
grid-template-rows: 3.5rem 1fr;
|
||||
grid-template-areas:
|
||||
'overview'
|
||||
'main';
|
||||
gap: $panel-gap;
|
||||
}
|
||||
|
||||
.panelContainer {
|
||||
grid-area: main;
|
||||
@@ -26,15 +7,6 @@ $panel-gap: 0.5rem;
|
||||
display: flex;
|
||||
gap: $panel-gap;
|
||||
overflow: hidden;
|
||||
|
||||
.rundown,
|
||||
.playback,
|
||||
.messages {
|
||||
position: relative;
|
||||
border-radius: var(--editor--panel__br);
|
||||
background-color: $bg-container-l2;
|
||||
padding: 1rem;
|
||||
}
|
||||
}
|
||||
|
||||
.left {
|
||||
@@ -45,18 +17,3 @@ $panel-gap: 0.5rem;
|
||||
flex-direction: column;
|
||||
gap: $panel-gap;
|
||||
}
|
||||
|
||||
.messages {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.content {
|
||||
padding-top: 1.5rem;
|
||||
}
|
||||
|
||||
.contentColumnLayout {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: $section-spacing;
|
||||
color: $ui-white;
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user